A host with a fixed startup command (a game panel running `./horizon-server run`) can't set
HORIZON_PARALLEL_TICK. `[engine] parallel_tick = true` makes horizon-server start the game with the
variable set to 1, the way `low_memory` sets MIMALLOC_ALLOW_THP. Off leaves the variable alone, so one
already in the environment still applies.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A fox that finishes chorus fruit teleports within half the item's
diameter on each axis (item data) and then falls to the first ground
below, so no margin bounds its destination. Islands::plan now returns
None, in production and verify, while any mob holds such an item in
either hand or is using one; it checks the item, not the species.
Adds a destination-herd regression and a plan() case, and documents the
fallback and the audit of other self-moves in docs/mob-islands.md.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Goat.finalizeSpawn, GoatAi.initMemories and Fox.populateDefaultEquipmentSlots
said that Ctx::finalize and Ctx::populate call goat_spawn and
fox::populate_default_equipment, and that goat_spawn calls goat_init_memories.
They do not: Ctx::finalize and Ctx::populate have their own copies of the same
draws, and goat_spawn draws the two cooldowns itself.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- th_th.json (game and lang_review copy): drop the HDR peak hint en_us
no longer has, as b36e25ab16 did for Ukrainian;
bundled_files_are_served_and_have_no_stale_or_repeated_keys failed on it.
- booleans_are_read_as_bytes: the sign side in the has_glowing_text case had
no messages, so it now loads empty (4787069c5b); give it four lines.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Update stale test call sites and the player RNG field to the current APIs without changing seeds, draws or assertions.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: timing spans around every untimed call of a level's pass (about 200 ms of the SMP's 250-330 ms overworld pass had none, #15349); spans read nothing while off, and no behaviour changes.
🤖 Agent: the watchdog now reads the stuck server thread's stack (a SIGURG handler installed only when a tick hangs, 5 s wait, else the report says why), so the next SMP freeze (#15349) names the function it was in; nothing changes until a tick hangs.
tests/test_command_exec_group_cleanup.py fails on main since #13823: the new 'sulfur cube size attributes' group kills its cubes after its last tick without shrinking them, so a size 2 cube would split inside the next group. Same shrink as the other sulfur cube groups; both commands are setup commands, so no record changes.
en_us dropped "Your display's reported peak is often too high: calibrate to set it up." in 1c93067327; #14706 added its Ukrainian text back today, so lang::tests::bundled_files_are_served_and_have_no_stale_or_repeated_keys fails on main and on every pull request tested against it.
The whole-word match turned GPLv3, LGPLv3, AGPLv3 and MPLv2 into custom: their token is
gplv3, not gpl. A token now matches a family when it is the family's name followed by
nothing, or by an optional v and digits. Tests for those spellings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Address the maintainer request on PR #14373 by relocating the loaded and spawnable block-position helpers into a sibling module. Keep both implementations and their game-module visibility unchanged.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Polygon, ModelNormals and Part move byte for byte; only visibility changes
(the private members the sibling modules reach become pub(super)), and mobs.rs
declares the module and re-exports Part and Polygon. A port map row follows the
move. mobs.rs goes from 4931 to 4720 lines without test modules, so a change
to Seen has room.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
naga's GLSL frontend did not know the GL_KHR_shader_subgroup_quad swaps, so any program using
them (FidelityFX SPD in several packs) failed with Unknown function. They map to naga's QuadSwap gather.
naga rejected gl_SubgroupID and gl_SubgroupInvocationID when the workgroup had more than one
dimension, a WGSL rule GLSL does not have, so pack compute passes with a 2-D workgroup were skipped.
A pack's core shader could read only vanilla's blocks and samplers. A
pipeline that gives it more (the improved-transparency stages' depth bounds
and coefficients for core/text, Polytone's blocks) translates it with
translate_core_with and the names it provides; CoreProgram::other lists
them in binding order, at other_binding(i). A uniform block declared on one
line is bound like one over several lines, where it came out unbound and
the shader was refused. An unprovided one is named as the pack declared it.
Part of #11486
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moving the data folder was refused when any folder above a data root was a symbolic link (a
~/.local/share on another disk, Fedora Atomic's /home). Only the roots are copied, so only the
root itself may not be a link.
Split out of #15247 at review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Vanilla 26.3 draws text displays, name tags and their backgrounds with
core/text (TEXT_POLYGON_OFFSET, and TEXT_SEE_THROUGH with
IS_SEE_THROUGH), so a pack's text shader reaches them, not only the
GUI's. The Globals and fog blocks the pack shaders read are made once a
frame for core/rendertype_lines, core/sky and core/text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pastes an #include every time, as shaderc does (vanilla's includes carry
guards), and a #moj_import once, as 26.2's GlslPreprocessor did; a
shader may paste at most 1024 files and grow to 1 MiB, so includes that
multiply are refused instead of exhausting memory. Keeps a 26.3
shader's own varying locations on both stages, even where another
branch gives the same name another one, so branch-local varyings no
longer push a program past OpenGL's 16. Takes NAME VALUE defines, as
RenderPipeline.Builder.withShaderDefine(name, int).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The flat world lies below y 40 and seed 12345 has slime chunks within
the view distance, so either server may spawn a slime
(Slime.checkSlimeSpawnRules) during the minute the steps take. On
Horizon one did: it pushed the boat off the bobber's line before the
cast and killed Alice, so the boat was never hooked. Fishing hooks do
not depend on the difficulty; peaceful keeps every monster out.
Fixes#15323
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The table said 47 exceptions at d5504cd328 and still listed edges that
are gone (the horizon-client-input row; -theme, -bench, -telemetry and
-mod-api-gen for the render crate). Rebuild it from the 30 entries in
'allowed' at a89f338 and point at tools/check_layers.py for the live
count.
Closes#15408
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official SMP's slow-tick lines spend ~200 ms of a 300 ms overworld
pass outside every named span. Spans now wrap each untimed call of the
level loop (enter_level, the mods' level hooks, streaming, update_world),
of tick/tick_world and of update_world, and level_tick wraps the world
tick, so the line names where that time goes.
Refs #15349
Co-Authored-By: Claude Opus 5.5 <[email protected]>
new_rocket now takes the rocket's own random (upstream); the rocket's splash and move helpers stay before it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerWatchdog's report only had the last 50 log lines, so a tick stuck
for 60 s on the official server (#15349) never said where. The watchdog now
interrupts the server thread with a signal (Linux, Android, Apple) whose
handler captures that thread's backtrace, and the report carries it as its
stack, as vanilla's report carries the server thread's stack. A thread that
does not answer in 5 s, or a system without the signal, is named in the
report's details instead.
Refs #15349
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#11899 left the shade of a world block entity to the entity shader,
which reads Vertex::normal, but the decorated pot's arm still
overwrote every normal with CULL_BACK (a baked-colour quad) to cull
the back faces of its flat top and bottom: the pot had neither a shade
nor a normal. mesher::cull_back keeps a shaded quad's normal with
SHADED_CULL_W (shade and cull, as the bat does) and gives CULL_BACK to
a baked one. The harness accepts SHADED_CULL_W as a shaded normal.
Fixes#15322
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Waypoint.Icon is written to the player's tag when it has data (a style
other than the default, or a colour) and read back when nothing is held
in memory. A tag without the key gives the default icon.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ClientboundCustomQueryPacket and ServerboundCustomQueryAnswerPacket read what is
left of the packet as the payload and throw "Payload may not be larger than
1048576 bytes" past MAX_PAYLOAD_SIZE. Horizon read it with Rest, of any size.
RestN<N> is Rest with that limit; both packets use it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
BossAction, BossEvent, NumberFormat, SetDisplayObjective, ResetScore,
SetScore, SetObjective, TeamParameters and SetPlayerTeam, with their
codecs and the boss bar's two flag helpers, move byte for byte into
packets/play/hud.rs. play.rs re-exports the nine public items, so every
`packets::play::…` path still resolves.
The port map follows: the two rest-012 rows that name boss_flags and
SetPlayerTeam::write point at the new file.
Part of #13063.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI leaves most pull requests' harnesses to parity-main and runs them on the pull request only when it changes a harness, an oracle, a probe or the runner (parity-scope in ci.yml). pr_ready ran every affected harness locally whenever the vanilla data was there: about 80 harness-minutes per pull request by the repository's own numbers. It now follows CI's scope and lists the others; --harnesses runs them anyway.
The conflict was the AcceptTeleportation arm in lan/hub.rs: the handler stays in
hub/accept_teleport.rs, which takes main's change of the guest's prediction
record (g.record instead of g.anticheat.record).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entity.hurt only calls hurtServer on a server level. The rider's client
ticks the horse it drives with the server's mob code, so a fall, lava or
suffocation hurt and killed the client's copy: it lay dead on the ground
while the server's horse lived, and could be ridden again (#15294).
Fixes#15294
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Goat.finalizeSpawn, GoatAi.initMemories and Fox.populateDefaultEquipmentSlots
are reached by live spawns; Fox.finalizeSpawn keeps the one gap that is left.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
CombatTracker.getMessageForAssistedFall and DamageSource.getLocalizedDeathMessage
are ported with their .item forms; the rows now say what the code does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Wolf.getLeashOffset, ArmorStand.brokenByAnything and HangingEntity.canCoexist
are ported as vanilla's bytecode reads; the rows now say what the code does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Match vanilla saved boat bounds and void removal, and make chunk and collision coordinate arithmetic safe at integer boundaries.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Qualcomm's Adreno Vulkan driver (Adreno 740, build 0676.41 of 2023-08-14) fails to link a
shader that indexes a constant array by vertex or pixel, which naga makes a private array, and
wgpu reports the failed pipeline as a lost device: the renderer never starts. The occlusion
test's box corners (cull.wgsl, gpu_occlusion.wgsl), the panorama cube (panorama.wgsl) and the
fading tiles' Bayer matrix (terrain.wgsl) are packed into u32 words read through a switch
instead, with tests that the packed tables are the old ones.
With the early depth test, occlusion culling on that driver now and then left sections out for
a frame (the world flashing black or see-through as a block broke); Adreno on Vulkan tests the
boxes in the shader, as Metal and DirectX 12 do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.onEquippedItemBroken awards Stats.ITEM_BROKEN whatever the
slot. Award it in player_equipment_broke instead of at two call sites, so
worn armour, shields and enchantment-driven breaks count too.
- Realms downloads and uploads used predictable names in the shared temp folder and
File::create, which follows a planted link or reuses a planted file. They now use an
unguessable name and a new owner-only file (create_new, 0600).
- A pax record whose length was shorter than its own prefix ("1 x") sliced backwards and
panicked the Realms download worker; it is now refused.
- The Windows boot-screen painter restored its fonts in the wrong order, leaving one selected so
DeleteObject failed: one HFONT leaked per repaint. It now restores the DC's own font.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
vanilla-data.sh moves whatever a preparation adds to .local/ into
shared-local/prepared/<key> and links it back. Since the preparer locks
.local/ (.horizon-prepare.lock), that took the lock file too: .local/'s
lock became a link into one preparation. Once that preparation is
pruned (four kept, two runners share them) the link dangles, the
`.local/*` glob that drops old links skips dotfiles, and the next
preparation fails: ".local/.horizon-prepare.lock: No such file or
directory".
The lock now stays a plain file: it is left out of the entries, a link
to it is removed, and old .entries that list it are skipped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A modpack's NeoForge version (modrinth.index.json dependencies.neoforge, CurseForge, modlist)
went unchecked into loader_jar's file name and URL. A version such as
"21.1.1/neoforge-21.1.1-universal.jar#/../../../../<dir>/x" made fetch create every directory
on that path and rename a real NeoForge jar over <dir>/x-universal.jar, with no checksum
(the .sha1 lookup got the jar and failed).
loader_jar now takes only [A-Za-z0-9._+-]{1,64} without "..", and the loader jar is downloaded
only with a 40-hex SHA-1 from Maven.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
game/lan.rs is over 3000 lines and may not grow (docs/layers.md#file-size): a refused payload is
now logged at most once a second, kept in lan_quiet.rs, instead of once per connection through a
LanHost field.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
SnowGolem.registerGoals builds its WaterAvoidingRandomStrollGoal with the
three-argument constructor and 1.0000001E-5F, not the default 0.001 that
Stroll::avoiding_water uses.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ServerGamePacketListenerImpl.handleAcceptTeleportPacket: an accept of the last
teleport id sent with no teleport awaited is invalid player movement. The
handler moves into hub/accept_teleport.rs so hub.rs does not grow.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Spider.finalizeSpawn (Spider.java:160-172) rolls
nextFloat() < 0.1F * getSpecialMultiplier() on HARD, then nextInt(5) for
Speed, Strength, Regeneration or Invisibility, and applies it for good
(MobEffectInstance(effect, -1)), for every spawn reason. Only the chunk
generation port did that; the runtime finalizer (summon, spawn eggs,
spawners, natural spawning) had no spider arm. Add one, share the
setRandomEffect mapping (Spider.java:207-217) between both paths, and pin
the odds, duration, effect set and the CaveSpider exception with a test.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WorldCreationUiState.findResultFolder trims the name (String.trim), takes the translated
selectWorld.newWorld when nothing is left, and asks FileUtil.findAvailableName for a free folder:
sanitizeName turns only SharedConstants.ILLEGAL_FILE_CHARACTERS and . / " into _, a name Windows
reserves is wrapped in underscores, a name ending in " (n)" counts on from n, the name is cut to
255 characters with its count, and a count that can't be parsed falls back to "World".
Horizon turned every character but letters, digits, space, - and _ into _ (Bob's world! became
Bob_s world_), named a blank world's folder World, and counted every taken name up from 1. The
rules move out of title.rs, a big file, into title/world_folder.rs with their test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerCommonPacketListenerImpl.handleKeepAlive accepts an answer whose id is
the pending challenge; any other answer disconnects the player with
disconnect.timeout unless they are the singleplayer owner.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Send lit resident far columns with complete light layers, scan past blocked entries within the existing budget, and retain missing chunks for retry without player movement.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
IntegratedPlayerList.canPlayerLogin returns multiplayer.disconnect.name_taken
when the joining name is the singleplayer owner's (ignoring case) and a player
of that name is online.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ClientPacketListener.handlePlayerAbilities sets the local player's walking speed from the packet
(Abilities.setWalkingSpeed), and AbstractClientPlayer.getFieldOfViewModifier divides the movement
speed by it. Horizon kept the packet but always divided by 0.1, so a server that sends another
walking speed got a different field of view (walking speed 0.2 with a movement speed of 0.2: 1.5
instead of 1.0).
The abilities code moves out of game/remote.rs, a big file, into game/remote_abilities.rs, with the
walking speed the field of view now reads (default 0.1 before any packet) and its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Match Tooltip.splitTooltip, WidgetTooltipHolder and BelowOrAboveWidgetTooltipPositioner. Keep real hover distinct from the hidden keyboard/controller pointer and test priority label/value/packet mapping.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Keep main's takes_food extraction and canAgeUp baby check, and apply the contributor's pacified-hoglin breeding gate in ai/feeding.rs. Normalize two test citations to the vanilla method names.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
- A jar's licence text (and file name) went into a `#` comment of the carried mod's
horizon-mod.toml as it was: a line break ended the comment and wrote keys of its own. Control
characters are now replaced with spaces.
- license_kind matched families by substring, so "limited" read as MIT (permissive), "template"
as MPL (copyleft) and "Discord" as ISC. It now matches whole words of the text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
`portmap::default_gateway` runs `route print` on Windows. A program with no
console of its own, such as the launcher, got a console window for it each
time. The command now starts with CREATE_NO_WINDOW, as the other crates'
commands do.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- Backup zips (which hold server.toml and server.properties with the API token, RCON password
and management secret) are created owner-only.
- Pruning automatic backups deletes only `<stamp>-auto.zip`, not a backup labelled by hand
"nightly-auto".
- backups.every no longer panics on a wide last character ("6é") or overflows on a huge number.
- A Discord rate-limit header of "NaN" no longer ends the Discord thread.
- world.name may not be ".", nor backups, logs or .horizon, which pointed the world at the server
folder or the supervisor's own folders.
- init stops instead of replacing a whitelist.json or ops.json it could not read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Follow ItemStack.hurtAndBreak and keep custom-durable stacks at their damage limit intact when effective wear is zero. Extend the survival mining regression and disambiguate a Rust field from a vanilla ledger citation.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
dtolnay/rust-toolchain@stable (a branch) and Swatinem/rust-cache@v2 (a movable tag) run inside
the jobs that build the archives players download, before the upload Nightly signs: whoever
could move those refs could change released binaries. Both are pinned to full commit SHAs
(rust-toolchain's stable branch head and rust-cache v2.9.3), with toolchain: stable passed
explicitly since rust-toolchain reads the toolchain from its ref otherwise.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
servers::lock() keeps a ping storing an icon and a save of the list from undoing each other,
but adding or editing an entry on the screen, and the gameplay saves of a server's resource
pack choice and Code of Conduct, loaded and saved without it: a favicon saved meanwhile could
drop the edit, or the edit the icon. Every writer also used the same servers.dat_tmp, so
overlapping writes could rename an interleaved file into place, which then loads as an empty
list and is saved as one.
Those three now hold the lock (the screen releases it before reloading the list), and the
temporary file is named per process.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
tick_blocks built the random-tick list from the world map's keys, an
FxHashMap filled as the streaming workers finish, so two runs of one world
random-ticked their chunks in different orders and the level random's draws
landed on different blocks. Game::random_tick_chunks now sorts the list by
chunk position. Vanilla walks its simulation tracker's fastutil table, whose
slot order follows that table's history; position order is deterministic.
Fixes#15164
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#5879 lists these as the unknown blocks around the water flags of the official server. water_play now
builds each one beside the player (the server only fills loaded chunks), checks through the client that
the blocks are there as built, and plays in them: sprint-swimming down and up a kelp forest and holding
jump at its surface; jump held at a waterfall's foot, walking into it across the water it spreads,
sinking down it from the top, and standing at its foot while it stops and starts again; standing and
walking in a channel whose source goes and comes back; standing on waterlogged slabs, walking into
waterlogged fences and up waterlogged stairs.
The check's client renders offscreen when HORIZON_E2E_HEADLESS is set, as agent_control_e2e.py does,
so a run on a desktop opens no window that takes the focus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The supervisor matched "Stopping server" and "Done (" anywhere in a stdout line, so chat
saying "/INFO]: Stopping server" told management clients the server was stopping and hid the
real notice. It now looks at the message after the first "/INFO]: " only.
- A quiet-commands payload's mod id went unescaped into the dedicated server's stdout and log:
a newline in it forged whole lines. decode now refuses control characters, DEL and the
section sign in the mod id and every line, as a chat command must pass.
- An empty batch (3 bytes) was queued and logged as a summary line, before any permission check,
hundreds of times a tick; it is now ignored, a batch with no room is counted on the player's
last waiting one, and a refused payload is logged once per connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Keep main's minecart index/display extraction and expose cart_at to the below-world discard helper. Vehicle discard behavior is unchanged.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
For CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT and CTRL_SHUTDOWN_EVENT, Windows ends the process as
soon as the console handler returns, so the supervisor died right after counting the request
and never stopped its game. The game runs in its own process group and gets no event: it kept
the world and the port, unsupervised, until it was killed without a save.
The handler now waits, within what Windows allows (4 s for a close, 15 s for a logoff or
shutdown), until the supervisor reports with signals::stopped() that its game has stopped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Play loads accounts.json, asks the services about each account (several requests, 20 s
timeouts), then saved that whole old copy without the lock: a sign-in finished meanwhile was
dropped (its secrets orphaned in the keychain) and a sign-out came back. save_to also wrote in
place, so a crash or full disk mid-write left a file read as no accounts.
save_to now writes a new file and renames it over the old one, and the launch paths save with
save_renewed: under the accounts' lock, the renewed names and skins go into accounts.json as it
is now, keeping accounts added or removed since.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
RealmsAvailability: GET mco/client/compatible, then GET mco/available for
a compatible client, once per account and shared by every caller. Nothing
consumes it yet: the Realms screen's gate and its refusal screens follow.
- realms/client.rs: client_compatible, has_parental_consent, and
RealmsError::message (vanilla's errorMessage, split out of text()).
- realms/availability.rs: Availability, Pending, AvailabilityCache, get,
current. Non-blocking; only an unexpected error is checked again.
- docs/realms-availability.md: how a screen consumes it (#14986).
- realms.rs: the module line, and a stale ResetKind comment made one line
so the file does not grow.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
- server.toml was read, changed and written in place with no lock by management clients, the
API, the CLI and sync_back: two overlapping writes dropped one change, and a crash after the
truncate left an empty file, which reads as every default and was then written into
server.properties. Writers in the process now hold one lock, and server.toml and
server.properties are written to a temporary file renamed over the old one.
- A whole-world restore whose last rename failed returned with the world moved aside and no
world in its place, and the game then made a new one. The old world is now moved back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- A restore or update asked for while the game was stopped (or while it was already
stopping) was only consumed on Exit::Restart, so the game started on the current world and
the request waited for some later, unrelated restart, and a stuck update kept every later
one refused. Both are now applied at the head of the loop, before every start.
- An update timeout is clamped to a day (API, supervisor) and refused past it by the CLI:
Instant + a huge Duration panicked the supervisor after it had switched releases.
- A repeated stop from the API or the management protocol no longer kills the game while it
saves; only a second signal from a person does, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
join_direct gave the handshake only to the first endpoint that answered the hello, and returned
its error with `?`, so the invite's other endpoints and its relay were never tried. Invites
carry the host's LAN addresses on the default port, and on the guest's own network another
Horizon host at the same private address (a housemate's game) can answer first: a valid code
failed as "The code doesn't match".
Each answering endpoint now gets the handshake in turn until one takes the code. When none
does, the relay is tried as before; without a relay, the wrong code (else the first failure)
is reported.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Initial velocity does not bound movement after MoveControl reads movement and flight attributes. Conservatively exclude non-default locomotion and movement effects, reject non-finite velocity, and cover a stationary fast cow crossing into a distant herd in production and verify.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
The management server spawned a thread for every connection with no cap, and read the
upgrade request before authentication with only a 10 s timeout per read, so a byte every few
seconds held a connection forever, and a 1 MiB Content-Length was allocated up front. An
unauthenticated client could open thousands.
http::read_request now has one deadline for the whole request (REQUEST_DEADLINE, 10 s), grows
the body as it arrives, and both the management listener and http::serve hold at most
MAX_OPEN (64) connections at once through a Slot given back on drop; past it a new connection
is closed unanswered.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
AbstractScrollArea.extractScrollbar requests POINTING_HAND over a scrollable
bar and RESIZE_NS while it is held. The pack screen drew its scroller without
asking, so the pointer stayed an arrow over both lists. The draw now requests
the hand or the resize cursor from the held state it already tracks.
`SerializableChunkData.parse` reads the section list with
`getListOrEmpty("sections")`, so a chunk that does not carry one is a chunk with
no sections rather than an unreadable one. `ChunkData::new` refused it, so an
empty or hand-written chunk could not be opened; it now keeps an empty list in
the tag, which is what the writer would put there and what the accessors below
look for.
`Zombie.isUnderWaterConverting` and the `ConversionTracker` it drives had no
row, nor did the two husk overrides that send the conversion to a zombie; the
behaviour is `undead.rs::custom_server_ai_step`, and the row cites it for
each method the jar declares there.
Names read from `javap -p` on 26.3: `Zombie.isUnderWaterConverting`,
`Zombie.convertsInWater`, `Zombie.convertsToWhenDrowning`,
`Zombie.getConversionSound`, `ConversionTracker.tick`, `Husk.convertsInWater`,
`Husk.convertsToWhenDrowning`, `Husk.getConversionSound` and
`ZombieVillager.convertsInWater` (false in the jar, so it is not part of the
arm).
`map-check`: 165 rows, no problems.
`SerializableChunkData.parse` builds the position with `getIntOr("xPos", 0)`
and `getIntOr("zPos", 0)`, so a chunk that does not carry either tag is the
chunk at 0, 0 and `Tag.asInt` accepts the narrower integer tags as well as
`Int`. `ChunkData::new` refused the whole chunk, so an edited or hand-written
chunk without them could not be opened at all.
AbstractWidget.WithInactiveMessage.getMessage returns
ComponentUtils.mergeStyles(message, Style.EMPTY.withColor(-6250336)) while the
widget is inactive, so a disabled button's message is drawn grey (0xFFA0A0A0).
screen_draw::button drew the same white label in both states; it now colours
the label when inactive.
The documented `bufferObject.<n> = <size> <file>` form was refused:
the loader read the value as a size and nothing else, so a pack with
`bufferObject.2 = 16 data/seed.bin` lost its buffer with the note
"not a buffer object".
The loader now reads that form: the file's bytes come from the pack,
relative to `shaders`, and the buffer starts with them. A file that is
not there, that leaves the pack, or that is bigger than the buffer it
fills is refused with a note, as the other pack properties are.
Co-Authored-By: OpenCode <[email protected]>
Font.PreparedTextBuilder.markBackground reaches one pixel left of the first
glyph and one above it, the glyph advance to the right and nine pixels down.
Horizon drew two pixels on every side (right and bottom included), so every
HUD title, action bar, boss bar and selected-item backdrop was a couple of
pixels too large. The fill now matches the glyph box.
Without the change the four length cases fail with `Err(InvalidData, "invalid
chunk payload length")` and the cut-off case with `Err(UnexpectedEof, "failed
to fill whole buffer")`.
`RegionFile.getChunkDataInputStream` answers `null` whenever the file cannot
hand over the chunk: a header cut off by the end of the file ("Chunk header is
truncated"), a declared length of 0 ("allocated, but stream is missing"), a
length past the chunk's sectors ("stream is truncated") or a payload shorter
than its own header. Horizon returned an `Err` for all four, so a reader that
walks a region gave up on the whole file instead of skipping the chunk.
A zombie at 3.0 absorption converts through `single` into a drowned: the new
mob reads 3.0. Without the copy the new mob has 0.0, the effect having been
copied but not the field the effect filled.
`ConversionType.convertCommon` copies the effects, then
`to.setAbsorptionAmount(from.getAbsorptionAmount())` - the golden hearts an
absorption effect left on the mob are a plain field, and the effect copy does
not rebuild them.
`conversion::single` copied the effects but never the field, so a zombie with
three absorption hearts that turned into a drowned came out with none.
Three tools took their first argument as a path to write into or
download into, so "--help" created ./--help: the pad glyphs wrote
14 PNGs into it, and fetch_dxc.py downloaded Microsoft's 22 MB archive
into it. runner_pause.py, a watcher the service runs with no
arguments, started watching whatever it was given and then crashed on
a folder that was not there yet.
They print their usage and exit instead, and the shared dev-tool test
covers the two that write.
Co-Authored-By: OpenCode <[email protected]>
Cover real wolf, cat and parrot teleports between separated herds in production and verify. Document conservative reach maintenance and implementation/performance acceptance requirements.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Keep older stable and prerelease Mesa drivers on the conservative first_instance workaround. Exercise the release boundary, distribution suffix, unknown versions, earlier Intel families and backend/vendor guards.
Review fix by rdbtCVS's AI agent (OpenCode, GPT 6.1 Sol).
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Allow indirect multi-draw and GPU culling on Intel Gen12 Raptor Lake
adapters when running on Mesa 26.0+. Tested on Intel Iris Xe Graphics
(RPL-U, device 0xa7a1) under Mesa 26.0.8: verified zero terrain flicker
or attribute corruption while cutting frame times by 28%.
Older Mesa versions and earlier Intel generations (Ice Lake, Tiger Lake,
Rocket Lake, Alder Lake) retain the workaround.
Co-Authored-By: Antigravity <[email protected]>
`LevelReader.getMaxLocalRawBrightness(pos)` answers 15 when the position is
outside `x >= -30000000 && z >= -30000000 && x < 30000000 && z < 30000000`,
because there is no chunk to ask there; inside it is `getRawBrightness(pos,
skyDarken)`. Horizon always asked the level, so past the gate it returned
whatever the (unloaded, dark) level answered: saplings and mushrooms never grow
and nothing that reads the brightness sees daylight.
ClientBundleTooltip.getEmptyBundleBackgroundHeight is
getEmptyBundleDescriptionTextHeight(font) + 13 + 8, and that height is
font.split(BUNDLE_EMPTY_DESCRIPTION, 96).size() * 9: the description's wrapped
line count at one decimal. Horizon hardcoded two lines (18), so a translation
whose description wraps to three lines drew the progress bar and the box's
bottom two pixels too high.
tooltip_layout and bundle_tooltip take the line count; the tooltip's probe and
draw compute it from the same font and 96 pixel width the description is drawn
at. SmithingScreen's slot icons moved to game/smithing_icons.rs so the
3,000-line stations.rs does not grow.
ServerData.validateIcon keeps any PNG whose width and height are at most 1024
(MAX_ICON_SIZE); FaviconTexture.upload still needs exactly 64x64 to draw one.
Horizon used icon_pixels (64x64 only) as the gate too, so a server whose
favicon was, say, 128x128 had its icon dropped from servers.dat and never
saved on join. valid_icon now answers the save question, and icon_pixels keeps
answering the draw one.
Build and select the gameplay library test executable for the automatic parity runner; retain the explicit artifact override. Cover argument-free invocation and Cargo artifact selection.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
`a_local_jigsaw_edit_applies_and_marks_the_chunk` sends a
`SetJigsawBlock` through the editor's Done path on an integrated game:
the block entity takes the new name and priorities and its chunk is
marked. Without the local application the name stays the default.
`ServerboundSetJigsawBlockPacket` is handled on the server
(`handleSetJigsawBlock`), which writes the block entity and marks it. The
editor's Done only builds that packet, and `Game::send` drops it when
there is no remote (the integrated server is this game), so a jigsaw edit
in single player changed nothing.
`block_editor_outcome` now applies the packet locally when `remote` is
none, through the same `apply_set_jigsaw_block` a guest's edit uses.
Default reads None; 2.0 and 6.0 seconds read the delay message with one
decimal; back to zero reads None. The slider test from the previous commit
still pins the tenths.
Options.chatDelay's message is options.chat.delay_none at zero and
options.chat.delay with the delay to one decimal otherwise (both label and
value_text). This folds the message half of #15068 into this PR, so one change
covers the control: the IntRange(0, 60) slider from the previous commits and
the message here.
For merge hygiene the new values module sits above mod shell_pages (not after
pub use Opt) and the ChatDelay arms lead their matches (not after the wheel
sensitivity arm), so the wheel-sensitivity PR's hunks do not touch these lines.
Options.chatDelay is IntRange(0, 60) xmap(v -> v / 10.0, v -> v * 10, true),
a slider from 0 to 6 seconds in tenths: toSliderValue is (tenths / 60) and
fromSliderValue is floor(map(v, 0, 1, 0, 61)) / 10. Horizon had no kind or
set_slider arm for it, so the Accessibility row drew a button whose click
changed nothing. The value labels (generic, on_off, chat_px) moved to
options_ui_values.rs with the mapping so options_ui.rs does not grow.
`a_gateway_that_ages_marks_the_chunk` ticks a gateway one server tick: the
age reads 1 and its chunk is in `world.modified`. Without the mark the
chunk stays clean.
`TheEndGatewayBlockEntity.portalTick` ages the block entity and ends with
`setChanged`, so the age the beam and the chunk save read is saved.
Horizon's `tick_end_gateway` copied the server age onto the block entity
but never marked the chunk.
A mob, an item and a vehicle kept the PortalCooldown they were loaded
with, so their countdown never showed in a save or in /data get entity.
The portal state ticks the countdown down every tick, as
Entity.processPortalCooldown does; the save now takes it from there, as
the projectile writer already did.
Fixes#14539
Co-Authored-By: OpenCode <[email protected]>
Measured on the mod: two particles of a type with exclusion_radius 2,
spawned a block apart in the same tick, both show, with or without
physics; one born inside a block (dropped) keeps no clear one from
spawning beside it. Horizon also counted the pending particles of the
tick, so the second was refused. The check now looks only at those that
joined; a later tick's spawn near them is still excluded.
Part of #11486
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The surface stage bound three whole-chunk density volumes for the ore veins
(copper's vein toggle, the shared richness, iron's) before its walk, although
each vein only reads the rows of its own y range and the richness is read in
about a quarter of the chunks.
A vein's rule now binds its densities with the rows it can read: the buffer
covers those rows widened to whole 16-block world rows, and it is sampled at
the first read. Other bindings (carvers, plain bind_density), and a volume
whose steps are not 1, are sampled whole when bound, as before. The per-block
read uses plain fields of the buffer, so the hot path keeps no copy of the
volume on the stack.
Same sampler, same positions inside the rows, same values: chunk dumps of
4 seeds x Overworld/Nether/End, terrain and decorated, are identical to main.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Measured on the mod: with limit 1, one particle with physics born inside a
block and one clear of it in the same tick, the clear one shows. Horizon
counted the pending one, which is dropped only when it joins, so the clear
one was refused and nothing showed. A pending particle with physics now
counts only once it joins: the limit is checked again at the join, after
the drop.
Part of #11486
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`a_doused_campfire_posts_block_change_from_the_potion_owner` puts a lit
campfire at (2, 4, 3) and douses it with a mob owner: the block reads
unlit and the posted events hold
`("minecraft:block_change", [2.5, 4.5, 3.5], Some(Who::Mob(7)))`.
With no source passed the event's source is None.
`AbstractThrownPotion.douseFire` passes `getOwner()` to
`CampfireBlock.douse(entity, level, pos, state)`, which calls
`level.gameEvent(entity, GameEvent.BLOCK_CHANGE, pos)`: the sculk sensor
hears the campfire go out from the potion's owner (possibly no owner at
all).
Horizon's `potion_hit_block`/`douse_fire` posted the event with no
source; the candle path stays sourceless, as vanilla's
`AbstractCandleBlock.extinguish(null, ...)` is.
`a_slot_toggled_in_the_menu_lands_on_the_crafter` opens a crafter with
slot 3 disabled, clicks it with an empty hand and checks the block
entity: the slot is enabled and the chunk is marked. Without the mark the
click leaves `world.modified` empty.
`CrafterBlockEntity.setSlotState` ends with `setChanged`, which is what
saves a toggled slot. Horizon applied the menu's slot states to the block
entity on every click (`menu_click`'s write-back) but never marked the
chunk, so a disabled slot could be lost on save.
The write-back is now `Game::apply_crafter_slot_states`: it copies the
menu's flags, and when one changed calls `container_changed` (the
container-marking path, which also re-reads comparators).
A 1.20.2-1.21.4 server's smithing-trim display (or trim recipe) is expanded into one
SmithingTrim per pattern, each with its own copy of the base and material displays. Those
can themselves be trim displays, so a few hundred bytes asked for billions of nodes.
Refuse a preview whose patterns x (base + material weight) exceeds MAX_TRIM_PREVIEW (4096),
counted with the new SlotDisplay::weight, both in SlotDisplay::read (1.21.2-1.21.4) and in
the 1.21.1 trim recipe translation; the 1.21.1 path also builds each display once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
text::plain flattens a server's Disconnect component into one String,
and format_java copies an argument once per %1$s in the server's own
`fallback` template. A fallback of 16 000 "%1$s" (64 KB) whose argument
is the same construct, nested three levels (well inside the depth-32
guard), asked for about 4e12 bytes; the allocation failed and the client
aborted instead of showing the reason. The login phase's json_text, a
second copy of the same expansion, had no depth limit either.
plain, format_java and json_text now stop at MAX_TEXT (1 MiB), cut at a
character boundary (push_capped). A disconnect screen needs a few
hundred bytes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
naga's GLSL front end refuses `const float[] w = float[](...)` and
`float w[] = ...`, which vanilla's GL compiler sizes from the initializer.
The size is written in, in the declarator and the constructor. Brackets
are matched in one pass and the declarator is looked for at most 256 bytes
back, so a long line stays linear.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
naga's GLSL front end refuses sampler2D function parameters that vanilla's
GL compiler takes. Each such function is copied once per set of samplers
its callers pass, the copy reading those samplers by name: overloads keep
their own bodies, nested calls are rewritten inside out, prototypes become
the copies' prototypes, and copies go after their samplers' declarations
(lucasmts59's finding in review).
Pack text is untrusted. Brackets are matched in one pass and the scans for
calls have a budget, so no text freezes a load. A sampler parameter with
no name, edits that would overlap, or anything else the rewrite can't
follow leave the whole shader as the pack wrote it, for naga to refuse.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
players/kick put the management client's reason, newlines included,
into "console kick <name> <reason>", which Control::ask writes as one
line to the game's control endpoint. The endpoint reads one request per
line, so a reason of "x\nconsole op Mallory" ran a second request: any
console command, or any other control verb, which the management
protocol never offers.
The kick reason now drops control characters, and Control::ask itself
refuses a request with \n or \r before sending anything, so every caller
is covered (the API's /command also refuses \r now, as it did \n).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
The current-head verify run 42944105269 used the old synthetic merge with the inherited CEM shadowing error. Main now includes the shared repair from PR #15038. Preserve both the contributor change and signed-byte regression fixup; no duplicated repair code.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
26.3 packs write gl_VertexIndex where older ones wrote gl_VertexID: both
mean vanilla's vertex number, four per quad. Only gl_VertexID read the
glue's expression, so on the text pipeline (six vertices per glyph) a
26.3 pack read the raw index. Both now read it; the glue's own code above
the pack's keeps the real index.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob.isSunBurnTick (Mob.java:500-506) counts isInPowderSnow and wasInPowderSnow
as not burnable, so a mob standing in powder snow never ignites; Horizon only
tested water and rain, so a zombie in powder snow ignited in daylight and
PowderSnowBlock.entityInside (PowderSnowBlock.java:88-99) then destroyed the
block under it with mobGriefing and put it out, leaving it to burn freely.
The scan at burn time is isInPowderSnow; m.in_powder_snow, the scan baseTick
made before the mob travelled, is wasInPowderSnow.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Zombie.doHurtTarget` (Zombie.java:315-325) rolls `effectiveDifficulty * 0.3`
for a burn of `2 * (int)effectiveDifficulty` seconds only when
`super.doHurtTarget` landed, the main hand is empty and the zombie is on fire,
and every species that extends `Zombie` inherits it: a burning husk, drowned,
zombie villager and zombified piglin now burn too, while a zombie holding a
weapon and a blow a shield took whole no longer do. The chance is drawn in
`do_hurt_target` where vanilla draws it, in the mob's own tick, and the game
lights the burn only for the blow it rides.
The turtle egg's break sound also draws its pitch from the level random
(Zombie.java:553-555): `0.9 + level.getRandom().nextFloat() * 0.2`.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A window cleared its GPU timings when its time was up, then wanted eight: at 10 FPS (five frames in
500 ms) or with timestamps arriving in gaps, every window started over and the scale never came
down in the scenes that need it most (review on #14815).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Match TagValueInput.getByteOr for wider numeric and fractional tags, and cover signed frame rotation through NBT round trips for both frame kinds.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
KeyBindsScreen.extractRenderState walks the mappings and sets
resetButton.active from KeyMapping.isDefault, so the button is inactive until
some binding differs from its default. Horizon drew it active and reset every
mapping on any click.
Restore the cursor test module's cfg(test) guard displaced by the new lectern regression test. Cite the actual pinned LecternBlock.resetBookState method instead of nonexistent setHasBook.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
A native explosion inside a TNT blast is Level.explode(null, ...): it
now takes blast_owner, blast_guest and blast_owner_player aside with
the rest of the blast's state. The group keeps its zombie persistent
and clears the zombie's drops.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client kept only the dialog tags of ClientboundUpdateTagsPacket and
read block and item tags from the game's own files, so a data pack's or
plugin's tags (climbable, mineable, ...) never reached a player on a
server. Dynamic now keeps every registry's tags; a server session builds
the game's Items with the server's block and item tags in place of its
own (by name, through the built-in registries), and Game::items() reads
it. The play-state packet after /reload replaces them the same way
(ClientPacketListener.handleUpdateTags).
The server tables (the synced enchantments from #9301 and now the items)
move to game/remote_tables.rs, so remote.rs shrinks.
Closes#7843
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt.getOwner names the mob a saved owner UUID points at, so
dropExperience (the indirect source is a player) is false. Keep
whether the owner is a player beside blast_owner (a chain reaction
hands a mob owner on as a mob), and compare only whether orbs came
out, since they may merge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockInput.place loads the tag into the comparator's block entity, and
ComparatorBlockEntity.loadAdditional reads getIntOr(OutputSignal, 0).
Horizon keeps a comparator's output beside the block entities and only
/data wrote it; /setblock and /fill now load it through the same
load_comparator_output.
Fixes#13870
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block.onExplosionHit passes dropExperience = the explosion's indirect
source is a player (a primed TNT's owner) to spawnAfterBreak, so ores,
sculk and spawners blasted by a player's TNT sample and pop their
experience while blocks drop. Horizon popped none.
Part of #11330
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The list of layer keys shadowed the own-file map, so main stopped compiling after #14658.
Identical to #15042, so the next merge of main takes it without a conflict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mobs.rs: main (#14857) moved the yaw draw out of living_random into
Mob::with_random. Main's version stays, returning the random as an
EntityRandom as before.
mobs/zombie_villager.rs: main split the test fixture and added tests
that hand Mob::with_random a plain LegacyRandom. Main's side stays; the
fixture seeds the mob's random with set_seed, as it did here, and the
new tests wrap theirs with EntityRandom::own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Includes unchanged test-only cherry-pick of 41a579d428 as 5cc88b517b. Preserve original implementation, contributor credits, and ordered-RNG dependency history.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
stations.rs is over the 3000-line limit and may not grow
(docs/layers.md#file-size): the test module hangs off stations/anvil.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain the 307-item corpus and add real boat and border cases, independently expected RNG advances, the environment gate and off/on/off comparison. Reject count, age and pickup-delay mutations independently and retain actual merge invalidation. Exercise 300 nonmerging opaque-component stacks in each native contact mode.
Repair the existing escape test's baseline chunk fixture and promote vanilla's 0.98F drag to double in its expected value; no production gameplay change.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: GPT 6.1 Sol <[email protected]>
`LecternBlock.resetBookState` calls `setBlockAndUpdate` and then posts
`gameEvent(BLOCK_CHANGE, Context.of(entity, state))` with no condition on the
state having changed; `placeBook` hands the placing entity to it and
`resetBookState`'s other caller passes none. Horizon's helper only posted when
the state differed, so the gate is gone: the post follows the property write.
Preserve helper test commit 41a579d428 and the item phase's original history and authors.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Vanilla scales coordinates between levels by DimensionType.getTeleportationScale
(NetherPortalBlock.getExitPortal, CommandSourceStack.withLevel), which reads each
dimension type's coordinate_scale. portal_plan, start_server_trip and /execute in
now take the two types' scales instead of hand-written 8 and 1/8.
/execute in looks the level up through known_level, which names the levels the
server runs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
dispenser.rs: main (#14603) moved the firework rocket out of the match
into launch_angled_rocket, which makes the rocket's own random. This
branch only renamed the random argument in that arm, so main's version
stays and the arm goes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A datapack dimension (`data/<ns>/dimension/`) gets its own `DimensionType`, and
vanilla's renderer reads the skybox, cardinal light, ambient light and
attributes from it, not from the level's key. The client's draw choices now
read the level's dimension type facts the same way.
- Cardinal light picks the diffuse lights and the section shading; the type's
`has_skylight` picks the section sky light.
- The skybox picks the sun, moon, stars and clouds, the End sky and its flashes,
the sun shadows and the shader sky type; the ambient light picks the entity
shadow darkening.
- The day timeline, `water_evaporates` and `piglins_zombify` pick the firefly
bush and creaking effects, the dripstone particle and the piglin conversion
shake; the minimap reads the level shape's ceiling.
- `Resources::environment_of` picks the environment table by dimension type id.
For the Overworld, the Nether and the End every site returns what it did before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
tnt.rs: main wrapped the silent bottle test in on_big_stack. Main's
side stays, and the test seeds the level's random with set_seed, as it
did here.
Tests main added since assign a plain LegacyRandom to World.random or
Mob.random (breezes, bred_draws, fireworks_events_tests, mounts,
turtle). They wrap it with LevelRandom::for_level or EntityRandom::own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Compare deferred and in-place metadata for 2,085 varied mobs, including pack-selected effect particles and switch-off vector reuse. Exercise live climbing, snapshot order and nearest-player invalidation for 293 mobs. Build 132 random-tick section masks ahead and retain exact event, state and random comparisons through edits and reloads.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: GPT 6.1 Sol <[email protected]>
(cherry picked from commit 41a579d428)
On a server whose data pack adds an enchantment, the anvil showed the red
cross for a book of it: the client's result prediction read the world's
own enchantment table, which lacks the entry. AnvilMenu.createResult on
the client reads the synced registry, so the station previews and the menu
engine now read Game::enchantments(), the server's table in a server
session and the world's otherwise.
Closes#8688
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain every scoped worker handle, release the first-lane guard, join all workers, then resume the first original worker payload. Extend the production-path injected spawn/setup/mid-turn regression to assert exact payloads as well as timeout-bounded release.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Inject later-spawn and worker-setup failure through the production island runner; keep first and pending guards inside the scope callback. Compare independent cloned Gaussian/long outcomes and final group snapshots. Keep raid-member ticks serial in both production and verify so the serial diagnostic copy cannot mutate shared hurt ordering. Cover one-CPU fallback and preserve the original ticked check.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Compare deferred and in-place metadata for 2,085 varied mobs, including pack-selected effect particles and switch-off vector reuse. Exercise live climbing, snapshot order and nearest-player invalidation for 293 mobs. Build 132 random-tick section masks ahead and retain exact event, state and random comparisons through edits and reloads.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Includes 0b21fcbf60, the unchanged cherry-pick of 9298d7961c. Preserve the original islands implementation and both contributors' history.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Treat LegacyRandom::set_seed as an ordered mutation: acquire the shared stream before replacing its LCG state and clearing the Gaussian cache, without discarding the gate. This also publishes a reseed-only item's state through end_item and keeps finish_lane and LaneDone working.
Add timeout-bounded multi-lane regressions for reseeding before/after draws, reseed-only items, cross-lane Gaussian state and panic cleanup, plus serial cache-reset coverage. The regression rejects the original gate loss, omitted synchronization and retained Gaussian cache.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
(cherry picked from commit 9298d7961c)
Cap waiting lanes at available hardware, release all unstarted lanes on spawn failure, reject invalid planner inputs, and verify full outputs including pending pushes and Gaussian state. Order new mob RNG allocations with level draws; record/replay diagnostic seeds without resetting the global counter or pinning other threads. Add resource-backed planning, visibility, moved-rider and actual verify coverage.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
InsideBlockEffectType.FREEZE raises Entity.ticksFrozen for every entity
box touching powder snow and Entity.saveWithoutId writes TicksFrozen
while positive. Boats and minecarts never tracked it.
A snapshot could copy an old cached height and then a stale bit another reader had just cleared, retaining the old height as clean. Acquire the stale-bit snapshot first: marked columns rescan, and cleared bits publish the refreshed height. A concurrent Chunk snapshot test covers both heightmaps and fails with the original ordering.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Treat LegacyRandom::set_seed as an ordered mutation: acquire the shared stream before replacing its LCG state and clearing the Gaussian cache, without discarding the gate. This also publishes a reseed-only item's state through end_item and keeps finish_lane and LaneDone working.
Add timeout-bounded multi-lane regressions for reseeding before/after draws, reseed-only items, cross-lane Gaussian state and panic cleanup, plus serial cache-reset coverage. The regression rejects the original gate loss, omitted synchronization and retained Gaussian cache.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
DecoratedPotBlock.getAnalogOutputSignal goes through
AbstractContainerMenu.getRedstoneSignalFromBlockEntity ->
getRedstoneSignalFromContainer, but Horizon keeps a pot's one slot in the
chunk's saved block entity rather than world.block_entities, so the
container signal found no container and read 0. The comparator path now
builds the pot's ContainerSingleItem view from the saved tag (the same one
hopper insertion already used) and reads the slot through it, so a pot
with items powers a comparator and a hopper insertion updates it.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`BeaconScreen.extractBackground` draws the payment row as netherite (x+20),
emerald (x+41), diamond (x+41+22), gold (x+42+44) and iron (x+42+66); the row
no longer follows the items' value. `BeaconPowerButton.setEffect` gives every
power button its effect's tooltip, and `BeaconUpgradePowerButton`'s
`createEffectDescription` appends " II". `BeaconSpriteScreenButton`'s tooltip
is its label (`CommonComponents.GUI_DONE` / `GUI_CANCEL`), hovered whether or
not the button is active.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`a_lectern_book_removal_posts_block_change` puts a lectern with a book
and clears `has_book` through the property helper: the block reads
`has_book=false` and the posted events hold
`("minecraft:block_change", [2.5, 4.5, 3.5], None)`. Without the post the
list is empty.
`LecternBlock.setHasBook` (through `placeBook` and `resetBookState`)
writes `has_book`, then calls `level.gameEvent(entity, GameEvent.BLOCK_CHANGE,
pos, Context.of(newState))`; the property drives the lectern's comparator
output and the item it drops.
Horizon's `set_lectern_property` wrote the state with flag 3 and posted
no game event, so a book put on or taken off a lectern was silent to
sculk sensors.
The placement path passes the placing player, a removal passes no source,
as vanilla's callers do.
SignText reads has_glowing_text through Codec.BOOL, and
NbtOps.getBooleanValue is doubleValue() != 0, so an int 256 is true
there, unlike getBooleanOr's byte test. It has its own read now.
getBooleanOr tests a number's byte value, so an int 256 is false. The
flag read every block entity's canonical form goes through tested the
whole number.
TheEndGatewayBlockEntity reads it with getBooleanOr: a number is true
when its byte value is not 0, so an int 256 is false. The decoder tested
the whole number; it now uses flag.
FallingBlockEntity reads HurtEntities, CancelDrop and DropItem with
getBooleanOr: a number is true when its byte value is not 0, so an int
256 is false. The loaders tested the whole number; they now use flag
and flag_or.
AbstractArrow.life is the saved despawn count; inGroundTime is not
saved and starts at 0 on load. The trident used one counter for both,
so a loaded negative life held back the Loyalty return (in_ground_time
> 4). life is now its own field, loaded and saved as it is, counted
where tickDespawn counts it (not for a Loyalty trident that can be
picked up) and reset by startFalling; in_ground_time starts at 0.
from_network writes the wire patch's consumable through the real registries;
consumable() then decoded the raw bytes again without them, dropping registered
effects and replacing a registered sound with the prototype's. Decode the raw
bytes only when no resolved component exists. The test now uses a registered
sound that differs from the honey bottle's and a registered effect.
resend_biomes filtered the guests twice (for the bodies and for sending);
it now collects the guests that watch once and zips them with their bodies.
Same receivers in the same order, and hub.rs, over the 3000-line limit,
no longer grows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings the branch up to date and lays out the new code and tests as
rustfmt wants them, which the verify job checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings the branch up to date and lays out the new code and tests as
rustfmt wants them, which the verify job checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings the branch up to date and lays out the new code and tests as
rustfmt wants them, which the verify job checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Summons paintings with several Rotation pitches on vanilla's dedicated
server and Horizon's, saves and reloads the world, and compares
/data get entity ... Rotation, facing and variant before and after.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entity.load reads Fire with getShortOr for every entity. The player's
loader read it, Air and TicksFrozen through one unnarrowed read; Fire
is now narrowed, the other two stay ints as vanilla reads them.
Checks the borrowed lookup against the clone-and-flatten expression it
replaces, over an empty slot, filled slots, slots past the end and an
empty inventory.
Refs #14939
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_guests cloned the whole ItemStack of every guest's selected slot
every tick, only to compare its item with play.attack_item. The new
held_attack_item helper borrows the item, and the Arc is cloned only
when the held item changed. Same result, no clone on the common path.
Refs #14939
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hint broken over several lines could have the next hint packed onto its
last line ("...the way you want it to RB Symbols"), reading as part of
it. The lines of a broken hint are now its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horse.addAdditionalSaveData writes typeVariant as it is, so Variant:9
reads back 9 although it shows coat 2. The save now keeps the loaded
int while the horse still shows that coat and those markings, and
packs the two ids once its look changed.
Random viewers and sections (20,000 cases, with an empty section checked
too), and every single block around 60 random viewers including the edge
of the reach, assert that reaches() keeps any viewer for which near()
holds. The sections-with-hits count and the blocks-near count guard
against a test that proves nothing.
Refs #14923
Co-Authored-By: Claude Opus 5.5 <[email protected]>
broadcast_block_changes tested every changed block of a section against
every recording viewer, although only a block inside the viewer's `near`
reach is ever recorded. bounds() takes the section's box once, and
reaches() rejects a viewer whose reach misses it, using the same
expressions as near() so the reject is conservative. note_changes() keeps
the per-block loop and its order, and moves the loop out of hub.rs.
Refs #14923
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horse.getVariant is Variant.byId(typeVariant & 255) and getMarkings
Markings.byId((typeVariant & 0xFF00) >> 8); both maps are
OutOfBoundsStrategy.WRAP. The loader clamped them, so Variant:9 was
dark brown where vanilla's horse is chestnut. The saved Variant still
comes from the two ids, not the loaded number.
bodies() encodes one body per set of columns. The test builds the packet
each receiver got before (cloned columns, encoded alone) for three
receivers sharing five columns, a receiver with a subset in another
order, and one with none, and compares the bytes.
Refs #14925
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
resend_biomes cloned each receiver's ChunkBiomeData and encoded the
ChunksBiomes packet again for every guest. Receivers that have the same
columns now share one encoded body, built by the new lan::biome_resend
module; hub.rs gets smaller. Each guest still gets the same bytes in
the same column order, and the fake-player filter is unchanged.
Refs #14925
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Axolotl.Variant.BY_ID is OutOfBoundsStrategy.ZERO: an id outside 0..4
is the first variant, lucy. The loader shared the parrot's arm, whose
CLAMP is right for the parrot but made Variant:7 the rare blue axolotl.
-32768 (never ages) and 32767 (never counts down), given as ints, and
the ints that narrow onto them, load as the sentinels the tick tests
for and save as the shorts vanilla writes.
LargeFireball.readAdditionalSaveData reads it with getByteOr, which
narrows any numeric tag (IntTag.byteValue is (byte)(value & 255)):
ExplosionPower:300 loads as 44. The loader kept the raw number, so
such a fireball exploded with power 300 while its save showed 44b.
Entity.load reads Fire with getShortOr, which narrows any numeric tag
(IntTag.shortValue is (short)(value & 65535)): Fire:40000 loads as
-25536. The arrow, the other projectiles and the boat already narrowed
it; the mob's and the armor stand's loaders kept the raw number.
LivingEntity.readAdditionalSaveData reads both with getShortOr, which
narrows any numeric tag (IntTag.shortValue is (short)(value & 65535)):
HurtTime:40000 loads as -25536 and never counts down. The loader kept
the raw number, so such a mob counted down from 40000 and was drawn
hurt all the while.
Replays pushes, duplicates, pops with re-queues during a run, deferred
events put back after the run, and retains against the old Vec model
(contains + push, remove(0)) and checks queue contents and pop order
after every step. Also checks 5000 distinct events pop in queue order
and a duplicate is dropped. No game resources needed.
Refs #14873
Co-Authored-By: Claude <[email protected]>
run_block_events and client_drain_block_events popped with Vec::remove(0),
which shifts the whole tail per event, and block_event deduped with a
linear contains. The queue is now a VecDeque (pop_front) with an
FxHashSet mirror for the dedupe. BlockEventData derives Hash.
All queue writes go through Ticks::queue_block_event, pop_block_event
and retain_block_events so the set cannot drift. Queue order, dedupe
results and the re-queue and reschedule paths are unchanged.
Refs #14873
Co-Authored-By: Claude <[email protected]>
getShortOr floors a float or double tag before the short narrowing. The
item's loader did that for Health only; Age and PickupDelay now go
through the same read.
ChatComponent.setVisibleMessageFilter rewraps through refreshTrimmedMessages,
which leaves chatScrollbarPos alone; only rescaleChat resets it first
(resetChatScroll). Horizon's rewrap always reset the scroll and
newMessageSinceScroll, so changing Chat Visibility threw a scrolled-back
reader to the bottom.
ItemEntity.readAdditionalSaveData reads both with getShortOr, which
narrows any numeric tag (IntTag.shortValue is (short)(value & 65535)):
Age:40000 loads as -25536. The loader narrowed Health this way but kept
Age and PickupDelay as the raw number, so such an item was past 6000
and despawned on its first tick.
Read the minecraft:consumable component from saved components or the wire patch on an ItemStack rather than only checking the prototype definition. This ensures custom consumable items properly start eating, tick use duration, emit consume particles and sound, and complete without desynchronizing movement.
Breaking a too-wide hint at its spaces could leave the button's print
alone on a line ("B", then the label under it). The print now stays with
the hint's first word, and that line is drawn smaller if it is still
wider than the panel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Summons pigs with several AbsorptionAmount values on vanilla's dedicated
server and Horizon's, saves and reloads the world, and compares
/data get entity ... AbsorptionAmount before and after the reload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
unpack() rejected a profile's textures all-or-nothing: any texture URL
not on textures.minecraft.net dropped every texture, so a head's skin
was lost whenever another texture (cape, elytra) named a disallowed URL.
Vanilla's SkinManager judges each texture per URL: disallowed ones are
omitted and the rest are kept. unpack() now skips an invalid texture
instead of rejecting the profile, so custom player heads keep their skin
on servers whose profiles carry extra textures with foreign URLs (e.g.
hub.fadecloud.com).
The head-profile cache test expected the all-or-nothing behaviour; its
cape assertion now matches vanilla's per-URL semantics, and a regression
test covers a mixed valid-skin + disallowed-cape profile.
Refs #5376
AbstractArrow reads life as the saved short, negative too, and shakeTime
as the saved byte & 255. The arrow already did; the trident's own copy
kept both unsigned, so a loaded life:-1s or shake:-1b saved back as 0.
Both are signed on the trident now, read as the arrow reads them, with
vanilla's 'if (shakeTime > 0) --shakeTime' and pickup at shakeTime <= 0.
A single hint wider than the whole panel (a long translation) ran past it:
it now breaks at its spaces onto lines of its own, and only a line that is
one word wider than the panel is drawn smaller, to fit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Off by default. Resolution Scale becomes the floor, and the scale rises toward 100% while the
GPU keeps Target FPS (30 to 240 in steps of 5, 60 by default), falling back when frames take
longer. A verdict is the median GPU time of a window of frames; a step up waits until the
frame would still fit if its cost grew with the pixels. Frames of a world still loading don't
count.
Each change starts TAA's and a shader pack's history over, so under a shader pack it moves
gently (3 s windows, 10% steps, 8 s between changes) and without one quickly (0.5 s, 5%, 1 s).
F3, the performance overlay (`res 85%`) and horizon-ctl's status show the scale; changes log at
debug level.
GPU Culling's Auto moves with it out of game.rs into game/frame_autotune.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tag is right and the behaviour was wrong for the wrong reason: an iron
golem is not in can_breathe_under_water, and it stays under water without a
bubble because IronGolem.decreaseAirSupply hands back the air it is given
(`iload_1; ireturn`). Taking the golem out of the tag alone left nothing to
hold its air, so it drowned after 320 ticks where vanilla never hurts it.
The drown path now asks keeps_its_air beside can_breathe_under_water: a golem
under water stays in baseTick's drowning branch, as a type missing from the
tag does, but its consume is 0, so its air stays at 300, it never reaches the
-20 that hurts and it never refills either — exactly the three things the
override decides.
The test states both halves: the shipped data has no iron_golem in the tag (a
copper golem, which is in it, keeps its air through the tag and has no
decreaseAirSupply override), and 320 ticks with its eyes in water leave the
golem at (0.0, 300) where the same mob without the arm is (2.0, 0).
ai::air::tick_air on a mob with its eyes in water for 320 ticks: a guardian
takes 0.0 where an iron golem takes the drown's 2.0, an adult happy ghast 2.0
where its baby takes 0.0, and a sulfur cube 2.0 without a block in its body and
0.0 with one. Without the tag correction and the two overrides the iron golem
reads 0.0.
LivingEntity.canBreatheUnderWater is the can_breathe_under_water tag, and
Horizon's copy of it carried an iron golem that the tag does not list: an iron
golem standing in water never lost its air and never drowned. Two class
overrides were missing with it — HappyGhast.canBreatheUnderWater answers true
for a baby, and SulfurCube.canBreatheUnderWater true while it has a block in
its body — so both drowned where vanilla lets them breathe.
The tag's list loses the iron golem, and the drown predicate now asks
can_breathe_under_water, which adds the baby happy ghast and the sulfur cube
with a body. The air-supply block moves out of ai.rs into mobs/ai/air.rs, which
takes 60 lines of code out of ai.rs.
The mushroom cow's row said its mycelium rule lived only in the spawn check;
walk_target_value carries it now, and the enderman, the pillager and the
warden have their flat 0 beside the other walk-target rows.
walk_target_value at three positions that differ only in the block below: a
cow scores grass 10.0 and stone and mycelium by the light, a mushroom cow
mycelium 10.0 and grass by the light (never 10), a zombie the negated light,
and an enderman, a pillager and a warden 0.0 on all three. Without the two
rules the mushroom cow reads 0.5 for its mycelium, the light term, and the
three flat classes read the zombie's score.
PathfinderMob.getWalkTargetValue is what a random stroll ranks its ten
candidate positions by, and three of its class overrides had no port:
Enderman.getWalkTargetValue, Pillager's and Warden's answer 0 wherever the
position is, so their strolls pick one of the ten at random, while Horizon
scored them by the negated light and sent them to the dark. A mushroom cow's
override replaces Animal's outright — mycelium 10, everything else by the
light — so grass is worth nothing to it, while Horizon gave it the cow's grass
10 and nothing for its own mycelium.
walk_target_value moves out of ai.rs into mobs/ai/walk_target.rs with the two
rules, and ai.rs loses 75 lines of code to the move.
RangeChoiceSampler::sample_volume kept or replaced each value with a
conditional the compiler turned into one branch per value. Final density
runs it over a chunk's whole 16x384x16 volume (the noodle caves), where the
choice changes often, so the branch was mispredicted: about 4.5% of proto
generation time at one thread.
The select now runs in its own function over plain slices, on the values'
bits, which the compiler vectorizes. Each value is the one vanilla's
conditional store leaves; a test checks it bit for bit against a scalar
reference, with NaN bounds, NaN selectors and signed zeros.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MWgQD4Rbxknozw5LZ
PointedDripstoneBlock.animateTick, getFluidAboveStalactite and canFillCauldron
were unlisted on the client side: the drip they decide is
animate_tick.rs::dripstone_drip, and the 26.3 filter reads the fluid's type.
The half of `trySpawnDripParticles` that decides where a fluid drop lands had
no case: every fluid the zoo put a block over was air or stone, both full
cubes, so the branch that reads the fluid block's own face was never taken.
Two cases put a waterlogged bottom slab over a dry one - a fluid block with a
full down face and a dry half-height block under it, the arrangement the branch
exists for - at seeds 0 and 1057. The corpus goes from 2,188 cases and 462,718
lines to 2,190 and 462,960; the 2,188 cases keep their own seeds and print the
same 462,718 lines as before, byte for byte, on both sides.
Negative run, against the same corpus: the face back to `full_collision` makes
only these two cases differ, 78 lines against 67 at seed 0 and 81 against 76 at
seed 1057, both short of the dripping_water particle the client adds on the
slab's top face.
Two reads of the fluid a drip comes from were taken from the wrong accessor.
`ClientLevel.doAnimateTick` hands `trySpawnDripParticles` the fluid block's own
face, `state.isFaceSturdy(this, pos, Direction.DOWN)`, and its collision shape,
`state.getCollisionShape(this, pos).max(Y)`. Horizon passed `full_collision`,
the "collision shape is a full cube" fact, for the face: a waterlogged bottom
slab has a full down face and is not a full cube, so with the dry slab under it
the client put a dripping_water particle on that slab's top and Horizon put
none (78 lines against 67 at seed 0, 81 against 76 at seed 1057). The facts
already carry `isFaceSturdy(.., SupportType.FULL)` by direction ordinal, so the
down face is bit 0.
`PointedDripstoneBlock.canFillCauldron(Fluid)` is `fluid == Fluids.LAVA ||
fluid == Fluids.WATER`: the fluid above the root by its type. Horizon also
required it to still be a source (`fluid_level >= 8 && !fluid_falling`), which
drops the flowing fluid a stalactite drips in vanilla.
- Translate 96 missing strings including Controller Radial Menu, HDR, Shaders, and Recipes
- Reach 100% coverage (1,151/1,151 keys) with zero placeholders changed and zero empty values
- Record all 1,151 keys as reviewed in tools/lang_review/game/th_th.json
- Translate remaining English error and status strings
- Polish phrasing across Home, Settings, Mods, and Servers
- Record all 1,240 keys as reviewed in tools/lang_review/launcher/th.json
Entity.applyEffectsFromBlocks plays playEntityOnFireExtinguishedSound once a move
left the entity no longer on fire (Entity.java:992-1003, 1047-1061), which powder
snow and water cauldrons cause through InsideBlockEffectType.EXTINGUISH; the
server's copy of the player now plays it after the block changes. The same two
blocks gate their block change on mayInteract (PowderSnowBlock.java:85-98,
LayeredCauldronBlock.java:73-79), which for a player is ServerPlayer.mayInteract
(ServerPlayer.java:2108-2110) and ServerLevel.mayInteract (ServerLevel.java:874-876):
outside the world border or under spawn protection the powder snow stays and the
cauldron keeps its level, while the fire still goes out and the hiss still plays.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
`TamableAnimal.isOrderedToSit` is the saved flag an interaction flips;
`isInSittingPose` is bit 1 of `DATA_FLAGS_ID`, which only
`SitWhenOrderedToGoal.start` (once `canUse` allows it: not in water, on the
ground) and a cat's `CatSitOnBlockGoal`/`CatLieOnBlockGoal` write, and which
the models, `ChestBlock` and the metadata read. A wolf fed a bone while it
swims is no longer drawn sitting.
A cat's chest/bed pose now goes out in that bit (the metadata carried the
order instead), so the other players of a LAN world see it sitting, and
`readAdditionalSaveData` sets the pose from the saved order as
`TamableAnimal:81-82` does.
`RunAroundLikeCrazyGoal.canUse` takes its spot from
`DefaultRandomPos.getPos(horse, 5, 4)` -- ten attempts past the build-height,
home, stable-destination and malus filters, the best walk-target value -- and
`start` walks to it, instead of a raw offset into a hill or over a pond.
`Cat.registerGoals` strolls out of water one tick in 100000
(`1.0000001E-5F`), not one in 1000.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
`remove_entities` called `remote_set_passengers(id, &[])` for every removed
id, and each call walks every tracked entity (`seat_passengers`), the mobs,
the displays and the armor stands. With thousands of falling blocks removed
a tick, that was most of the packet's cost, for entities that carry nobody.
The packet's vehicles are now collected once, before the per-id cleanup:
the removed ids that some passenger, mob, display, armor stand, interaction,
controlled boat or the player's own ride names as its vehicle. Only those
get the call; every other per-id step keeps its order.
8,000 falling blocks, RemoveEntities of 256 ids (debug test build):
51.3 -> 1.3 ms.
Part of #13007
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's MinecraftServer.runServer catches what a tick throws and still calls
stopServer in its finally, so an integrated server saves its players and levels
after a crash. Horizon's dedicated server already does this (stop_after_crash);
single player's server thread did not: a failed tick returned before
stop_integrated, and a panic unwound the thread, so everything since the last
autosave was lost, for the host and for every guest.
The integrated tick loop now catches a panic or a failed tick, stops through
stop_after_crash (the guest whose turn was interrupted keeps its last file) and
stop_integrated, and then returns the original error.
Part of #14394
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first mob a fresh chunk generates (spawnOriginalMobs) read the structure mob
finalizer's data on the server's tick: every mob type, the biome tags, the spawn
registries, the enchantments and the item tags, from the prepared resources.
Where those files are slow to read, that one read stalled the server for 6 to
7 s, a minute after players first walked into new terrain (#13857).
The terrain now starts that read on a thread of its own when its level loads
(every dimension but the End, whose chunks generate no mobs). The cell stays a
OnceLock: a mob that needs the data before the read ends waits for it, and the
data is read once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The controller's radial menu and its page, the HDR options' tooltips and
the recipe viewer's fuel tooltip. Placeholders and edge spaces match
en_us.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stream template blocks from decompressed NBT instead of building the full
tag tree. Preserve list unwrapping, duplicate keys, limits and block order.
Follow StructureTemplate.load/loadPalette and ListTag.addAndUnwrap in
vanilla 26.3. Read numeric positions and state indices through the same
NumericTag conversions as CompoundTag.getIntOr and ListTag.getDoubleOr.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
armor_stand_to_nbt kept a saved `attributes: []` over the stand's live
speed_instance (or_insert), so a stand loaded with an empty list never saved
the movement_speed instance LivingEntity.aiStep -> removeFrost creates.
Append it to the saved list once the stand has ticked, keeping the other
saved instances. The mob corpus gains a stand summoned with `attributes:[]`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
FishingHook.getOpenWaterTypeForBlock tests !state.isAir()
(FishingHook.java:416), and isAir() covers air, cave_air and void_air;
ServerGamePacketListenerImpl.noBlocksAround filters on the same predicate
(ServerGamePacketListenerImpl.java:543). open_water_type now reads
BlockFacts::is_air, and no_blocks_in reads the hub's state table, so a hook
under a mineshaft's air counts as open water (treasure can be caught) and a
survival guest hovering there is kicked for flying after 80 ticks.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
A recipe viewer's transfer (JEI's "+", Menu::transfer_recipe) counted and
moved only the stacks vanilla's recipe book may use
(Inventory.isUsableForCrafting: not damaged, enchanted or renamed). A
recipe with a tool as an ingredient, such as a mod rod made from a worn
fishing rod, then moved nothing, though crafting it by hand works and JEI
moves such items. The viewer's transfer now counts and moves every stack
of the item; the recipe book's placement keeps vanilla's rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
The hint line under the keys had a fixed 30 dp strip, so a longer line (RT
Complete over chat, a translation, the pack's wider letters) ran past the
panel or had to shrink. The hints now wrap onto as many lines as they need
within the panel's width, in order, and the panel grows to fit them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The user's list for the controller's keyboard:
- Done (and Menu) on a book and quill's page or a sign was Enter, so it
made a new line. It is now the screen's own Done (the book saves and
closes, the sign closes with its text); a New Line key beside it makes
the line there.
- Over chat, RT is Tab (CommandSuggestions.keyPressed): it fills in the
selected suggestion and again the next, and the right stick picks the
one before or after (Up and Down), so commands complete without closing
the keyboard. The hint line shows RT Complete there.
- A key's label shrinks to fit when it is wider than its key (a long
translation, New Line in the pack's font).
pad_menu_e2e: osk_book_done, osk_sign_done, osk_chat_suggestions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractBoat.modifyPassengerFluidInteractionBox for mobs: a boat that isn't underwater keeps only the part of its passenger's box above the boat's top, and all of it when the boat reaches the passenger's head. Players had it (motion/fluids.rs); mobs measured their whole box, so a passenger's legs below the surface made it count as in the water: it never burned in daylight, the flow pushed it and boarding splashed.
`a_blocked_hit_counts_the_shield_as_used` holds a shield in the off hand
with the use action running and calls `after_block` twice: a hit it takes
whole (5.0 blocked) takes the `used` count from 0 to 1, and a hit it
blocks nothing of takes it to 2. Without the award both stay 0.
`BlocksAttacks.hurtBlockingItem` awards `Stats.ITEM_USED` for the item
before it applies the damage function, and `LivingEntity.hurtServer`
calls it before its `damageBlocked > 0` branch: every hit taken while
using a blocking item counts it, even one it blocks nothing of.
Horizon's `after_block` applied the damage and broke the item at its max
damage but never awarded the statistic. It now does, on the
authoritative side (`remote.is_none()`, vanilla's `!level.isClientSide`),
before the early return for a hit that blocked nothing.
Entity.load reads Rotation (its pitch clamped to -90..90 after % 360),
OnGround and fall_distance, and a spawner never moves or turns, so its
save writes them back. Horizon saved 0 for all three.
Fixes#14572
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.readAdditionalSaveData stores AbsorptionAmount through
internalSetAbsorptionAmount, which does not clamp; only setAbsorptionAmount
(the Absorption effect, the damage path) clamps to [0, max_absorption].
mob_from_nbt clamped it to at least 0.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entity.load sets xRot from Rotation; Painting.readAdditionalSaveData's
setDirection only writes the yaw, so Entity.saveWithoutId writes the
loaded pitch back. painting_to_nbt wrote a fixed 0.0. It now reads the
pitch from the retained compound with setXRot's rules (non-finite
ignored, xRot % 360 clamped to +-90).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The module runs `horizon-server run` as its own user in /var/lib/horizon-server, in a hardened unit
that stops it with SIGTERM (the world saves first) and turns the update check off, since Nix picks the
release. The first start writes server.toml with `horizon-server init` from `init`; every start then
sets `port`, `resources` and `settings` with `horizon-server config set`, which checks them and keeps
the owner's other edits. `openFirewall` opens the port.
It ships no game files and fetches none. The game files are either a folder the owner prepared
(`resources`, given as a string so they never enter the store), or `prepare`: horizon-prepare run before
the server on a server jar and a Minecraft folder the owner put on the machine, in a unit with no network.
nix/tests/server.nix checks it in a VM without any of Mojang's files. Fixes#13841.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.isInLava is false while firstTick holds; the rocket cleared its
first-tick flag before the lava check. The `rocket fall distance` group
gains a rocket summoned inside lava.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`uninstall --remove-data` only deleted backend::data_dir() when the folder was
called `horizon-launcher`, a check made for HORIZON_LAUNCHER_HOME ("may point
anywhere"). Once the player has moved the data (Settings > Data folder), the
data folder is the location's launcher root, which the move always names
`<folder>/launcher`, so the check failed for every moved install: nothing was
deleted, and the build folder, removed inside the same `if`, stayed too.
A moved location's launcher folder is now deleted when it is the configured one
and has the name the move gives it; its build folder when it is `<folder>/build`
or inside the launcher folder. The game folder is never touched, and a location
file edited by hand to name another folder is still refused. Under
HORIZON_LAUNCHER_HOME the old name check stays.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The cause line is four 6-pixel glyphs centred in 427; the last two carry an
open_url click event, and only the pointer inside a glyph of the line finds
it.
DeathScreen.mouseClicked runs a ClickableStyleFinder over the cause line and
opens an open_url click event through clickUrlAction; its render visits the
line with HoveredTextEffects.TOOLTIP_AND_CURSOR. Horizon drew the cause with
text_styled and ignored clicks and hover on it.
With the parallel tick on, tick_creature_entities splits its mobs into
islands: a mob's reach for the tick (its move, a teleport, what it reads
around itself, the village data, the mobs it is linked to by id) meets only
mobs of its own island. Islands are packed into one group per thread; each
group runs its mobs' turns in list order (creatures/mob_group.rs), draws the
level random from a lane of one OrderedDraws, and reads a mob of another
group by id as the serial loop would show it (Board/Foreign). The results
merge back in list order. HORIZON_PARALLEL_TICK=verify also runs the serial
loop on copies and logs a tick that differs.
Each turn still decides whether its mob is loaded and ticking, as the serial
loop does, so the plan gives every mob a ticking mob's reach. The loop stays
serial with native mods, on a sampled tick, under HORIZON_TEST_SEEDS, with
fewer than 256 ticking mobs, with a wither or warden, or when one island
holds three quarters of the ticking mobs. A group's thread holds its lane's
LaneDone, so a panic in one group lets the others finish and reaches the
caller.
Tests: the plan's islands and packing, and an end-to-end run of three herds
in three groups against the serial loop (mobs, level random, death marks).
From kacpherek's #13644 (mob_islands.rs); for #14457.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #14043: a pure move to entity_nbt/orb.rs, so the orb's
fall_distance (#14440) lands in the moved file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The per-mob turn of tick_creatures (its world view, brain, movement, events,
riders, deaths and pushes) moves into creatures/mob_group.rs as Group::tick, on
a Group that owns the mobs it ticks, their snapshot grid, pushes, events and the
level random. tick_creatures builds one group of every mob and runs it in list
order, so nothing changes: whether a mob is loaded and ticking, and the
per-chunk difficulty, are still decided at the mob's own turn. The parallel tick
(#14457) will run several groups. creatures.rs shrinks by 177 lines.
From kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MobSlot lets the mob loop's helpers (apply_brain_events, riders, a school's
recruits) work on a group's borrowed mobs as on the list's own; MobWorld.foreign
answers a lookup by id of a mob in another group (ForeignSeen); PendingPushes::
append puts one group's pushes after another's, each mob's in order; and a
SeenGrid of part of a list can answer as the whole list's grid would
(answering_as). Nothing sets them yet: every caller passes the list's own mobs
and foreign: None. For #14457.
From kacpherek's #13644; the grid follows main's scan threshold.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LaneDone, held by a lane's thread while it runs, marks the lane finished when
dropped, also while a panic unwinds: the other lanes run to their end instead of
waiting forever for its items, and the panic reaches the caller when it joins
them. Review finding 1 on #13644; the test times out without it.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OrderedDraws shares a LegacyRandom among lanes that each run their part of a list
in list order: an item's first draw waits until every other lane has passed it,
so every draw comes out as one thread would make it. A copy of a gated random
draws on its own. For the parallel mob tick (#14457).
Taken from kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While the parallel tick is on, each ticking item's move (ItemEntity.tick up to its
merge) is worked out on a copy on the helper threads before the item loop, and
put in place at its turn. The move reads only the item, the blocks, the obstacle
boxes and the border, none of which the loop changes; an item inside a block
escapes at its own random's speed, which the copy carries. A merge before its turn
that changed its count, age or pickup delay sends it back to the move at its turn,
as do native mods and a mod listening to the round. HORIZON_PARALLEL_TICK=verify
makes each move again at its turn on a copy and logs a difference.
The item's tick reads ItemTables (the blocks, collisions, motion tables, border)
so helpers run the same code the turn runs; ObstacleGrid moves to
entities/obstacles.rs and gains a query with the caller's scratch. A test ticks
307 items (escaping blocks, in water, falling, merging) both ways and checks the
items and the level's random come out the same.
From kacpherek's #13644, redone on main's item tick.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HoglinAi.setAttackTargetIfCloserThanCurrent compares the current and the
broadcast target by position (BehaviorUtils.getTargetNearestMe), players
included. The flush kept the current target whenever either side was
Who::Player, since Who::Player is each mob's own player. The broadcast now
carries the hoglin that sent it, and the flush places the new target as
that hoglin's player and the current one as the receiver's (host, or the
LAN guest in mob_players). A receiver that takes another mob's player is
bound to that player.
Part of #12788.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fork's workflow run sees no repository variable, so SELF_HOSTED_CI can't move it: a fork's pull
request whose number ends in 0 or 1 always asked for the desktop's horizon-checks runners, even with
SELF_HOSTED_CI=light (#14131 waited there for hours). With the desktop off, its required verify
would wait a day, fail, and wait again on a re-run, so the pull request could never merge. verify,
launcher, mod-registry and telemetry-server now pick our checks runners only for this repository's
own branches; a test pins it for every pull_request workflow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
extras.rs: main dropped DiscordPresence, this branch dropped the cosmetics
rows: both removals kept. layers.json: main dropped client-lib -> theme,
this branch dropped client-render -> cosmetics: both removals kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LaneDone, held by a lane's thread while it runs, marks the lane finished when
dropped, also while a panic unwinds: the other lanes run to their end instead of
waiting forever for its items, and the panic reaches the caller when it joins
them. Review finding 1 on #13644; the test times out without it.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OrderedDraws shares a LegacyRandom among lanes that each run their part of a list
in list order: an item's first draw waits until every other lane has passed it,
so every draw comes out as one thread would make it. A copy of a gated random
draws on its own. For the parallel mob tick (#14457).
Taken from kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`LeavesBlock.updateDistance`: DISTANCE is one more than the smallest of the six face neighbours', a
`#minecraft:logs` block counting 0, another leaf its own DISTANCE and anything else 7, spreading one
step per scheduled tick. A row of nine leaves reads 1..6 then the cap and returns to the cap when the
log goes; logs, stripped wood and nether stems count while planks, stone, fences and leaves at the cap
do not; the six sides count and the diagonals do not. Only a non-persistent leaf at 7 is removed by a
random tick (`decaying`). The existing test covered a single leaf at 7 and one log beside a leaf.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`StairBlock.getStairsShape`, recomputed by `updateShape` when a horizontal neighbour changes: a
same-half stair on the facing side, turned across, makes an OUTER corner (LEFT when it faces the
subject's counter-clockwise), else one behind it makes an INNER corner; either is refused when the
block across the corner is a stair like the subject (`canTakeShape`), and the outer test is first.
Twelve arrangements: both handednesses, same axis, other half, a non-stair, both neighbours at once,
and both refusals.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
SerializableChunkData.postLoadChunk adds a full chunk's own entities list
(ServerLevel.addLegacyChunkEntities): a chunk last saved before entities had
files of their own keeps them there. Horizon read only entities/, so an old
world's minecarts and mobs were gone. A re-save writes no such list and the
entities go to entities/, as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SignBlock.openTextEdit gives the sign to one player (`playerWhoMayEdit`,
kept on the block entity like vanilla's SignBlockEntity, not on the
connection): another player's click opens nothing, their write is dropped
(`SignBlockEntity.updateSignText`) and their dye, ink sac or honeycomb is
refused without being used (`SignBlock.useItemOn`). The write releases the
sign, and a sign tick drops a holder who walked out of the block
interaction range plus four blocks or left the world
(`SignBlockEntity.tick`, `playerIsTooFarAwayToEdit`).
The editor also opens only on a side whose lines are plain text
(`SignText.hasEditableText`): on the click and after placement
(`SignBlock.setPlacedBy`), so a translatable or score line is no longer
replaced by typing into it.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
mob_meta reads SyncData instead of the hub (SyncData::effect_meta, with main's
shown_color; Hub::effect_meta calls it), and the attribute instances read
AttrSources (the catalog, the items, /attribute's maps) instead of Game. While
the parallel tick is on, the mobs' entity data and attributes are built after
the pass on the pool and put in place (game/lan/mob_states.rs); with it off
they are filled in place as before, with no new Vecs. What is read in order
(idle state, lead, equipment network ids) stays in the pass. A test checks the
states put in place equal those built in place.
From kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Before the mob loop, each mob's climbing check, snapshot entry and nearest
player read only that mob, the blocks and the players, so they are mapped on the
pool from 256 mobs (creatures/prep.rs, out of creatures.rs). A mob that moved
before its turn is asked its nearest player again where it stands. The masks the
random ticks read are built on the pool before the random ticks; the draws and
the ticks stay serial. The random tick test runs a fourth path with its masks
built ahead and checks it gives vanilla's scan's events and random values.
From kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_client::parallel: one rayon pool made once (at most 8 threads, the server
thread takes parts too), par_map that gives every part's results in the items'
order, and the HORIZON_PARALLEL_TICK switch (off by default; 1 or verify turn it
on). With the switch off every phase runs its serial code on the server thread.
Goal is Send + Sync, so mobs can be read from helper threads.
From kacpherek's #13644.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The writer thread was detached, so the last lines handed to it could be lost when
the server stopped. LogWriter now closes the channel and joins the thread on drop.
The thread writes the lines waiting together and flushes once none is left.
Tests: every line reaches the file in order by the drop, and dropping the writer
waits for a thread that is behind.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_hooks::pages gains Row::Field (Row::field(label, text).max(n)),
vanilla's EditBox on a page drawn as its own screen, with Act::Edit (each
change, before any other act or redraw reaches the page) and Act::Submit
(Enter); Place::Title, the title screen's Friends icon slot, unchanged in
place and look; Page::back, the label of the button that leaves such a
page; and pages::connect, for a Title page to join a server.
The icon opens the first Title page as multiplayer::Screen::Page, a form
laid out as Join a Friend (multiplayer/page.rs); its fields go through the
title screen's caret like every field of its own (focus, typing, caret
keys, selection, copy, paste, the max length), Tab walks fields then
buttons. Without a Title page the icon opens Join a Friend while a
resolver is registered, else it stays inactive. The options shell skips
fields. title.rs loses lines.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`DoorBlock.neighborChanged` is `hasNeighborSignal(pos) || hasNeighborSignal(the other half)`. Power
written raw beside one half, then a block update that reaches only the other half, opens it and the
half that copies it. The existing door test could not see this term: the half with power beside it
always saw it itself, so dropping the other-half check left it green.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
The diagonal cases put power where it sends the hopper no block update, so the hopper never looked
and the case passed for any implementation: mutating the port to be quasi-connected left the test
green. Each such case now pokes the hopper with a block on a side no case uses, as the piston and
dispenser tests do, and the same mutation turns it red.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`FenceBlock.connectsTo` is `!isExceptionForConnection(state) && faceSolid || sameFence || gate`.
Eighteen neighbours: any full face joins (stone, dirt, glass) except leaves, barriers, pumpkins and
melons; torches and air do not; another fence joins only within its own family (wooden against
nether brick does not); a gate joins only when its facing lies across the line.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
The block a landing names (mainSupportingBlockPos, else getOnPosLegacy)
is worked out in one place for items, armor stands and rockets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleRespawn gives the new LocalPlayer
AttributeMap.assignAllValues of the old one when the packet keeps the
attribute modifiers (every level change), assignBaseValues after a death.
The new player here started empty, so the worn armour's modifiers and any
custom one were gone after an End or Nether portal until re-equipped.
Fixes#13541
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Every flag wrote its log line from the tick thread, piece by piece. With 40 load-test bots
(which fail the movement prediction every tick: 76,000 lines in 3.5 minutes) that was
lan_guests 7.7 ms a tick; a lagging player's flood costs the same. The lines now go, in order,
to a writer thread: lan_guests 1.9 ms on the same run.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#14318: a rocket's Entity.fallDistance builds up as it flies down and is
saved and loaded. Entity.baseTick's fluid step resets it in water (and
splashes on entry, and lets currents push the rocket); lava halves it.
Entity.move's long-move ray resets it past a fall-resetting block or water,
checkFallDamage adds the drop and posts HIT_GROUND on landing, and a
cobweb or powder snow makes it stuck. Adds the `rocket fall distance`
command-exec group.
Items now splash with their motion before the water's current pushes it,
as Entity.updateFluidInteraction does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A write two below the height asks about no block on the next read; a write at or
above it is found again down that column alone, and the other columns keep theirs.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block written at or above a column's height (grass spreading, a crop, a mob breaking a
block) used to drop the whole chunk's map, and the next spawn attempt there scanned all 256
columns again. The write now marks that column; its next read finds its height down that
column only. The heights are those a rebuild gives. Natural spawning on the bench: 4.8 ms a
tick to 4.0.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`NoteBlock.setInstrument`, run when it is placed: a mob head above decides the instrument
(`worksAboveNoteBlock`), else the block below does, except that a head below gives harp and a
block with no instrument is harp too. Twenty-four cases from `Blocks.java`'s `.instrument(...)`
registrations, the seven head types included.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`FallingBlock.isFree` is `isAir || FIRE tag || liquid || canBeReplaced`: sand falls through air, both
kinds of fire, any liquid and the blocks flagged replaceable (grass, vines, dead bushes, snow) and
rests on everything else, torches, carpets, slabs, saplings and cobwebs included.
Each block stands on what it needs: placing the sand above is a neighbour update, and a carpet,
sapling or lily pad with nothing to stand on is removed by it, which frees the sand for a reason
that is not the rule under test. (A first draft missed this, and read as Horizon letting sand fall
through carpet; the exported flag and vanilla agree that carpet is not replaceable.)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
Vanilla resolves the position with BlockPosArgument.getSpawnablePos: no
loaded-chunk or build-height check, only Level.isInSpawnableBounds, else
argument.pos.outofbounds.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`LavaFluid.spreadTo`: lava spreading down into water turns the water's block to stone. Water below a
lava source does not harden the source itself, since `LiquidBlock.shouldSpreadLiquid` skips the side
below; the existing test only covered water beside lava.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`LavaFluid.getDropOff` is 2 outside the Nether (1 in an ultrawarm dimension): lava loses two levels a
block and reaches only three, at levels 2, 4 and 6. Run long enough for its 30-tick steps, including
the random slowdown of a rising level, so it checks the extent and not the timing.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`WaterFluid.getTickDelay` is 5: a source's first spread is five ticks after it is placed and each
flowing block schedules the next five on, so the water reaches distance d on tick 5 * d, none
sooner, out to its seven blocks and never the eighth. The existing test checked only where the water
ended up, not when.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`getAnalogOutputSignal` of the blocks whose output is a function of their own state, read through a
comparator as in play: a beehive's honey level, a cake's `(7 - bites) * 2`, a candle cake's full 14,
a composter's level, an end portal frame's 15 with an eye, a lava cauldron's 3, a water or powder
snow cauldron's level, a respawn anchor's `floor(charges / 4 * 15)` (0, 3, 7, 11, 15) and a lit
copper bulb's 15. Sixty-four state rows, each reporting its own mismatch.
The comparator is placed first: it computes only when told a neighbour changed, and a block that
has an output tells it when it is placed. (Two earlier drafts got this wrong, and one misnamed the
end portal frame's property: it is `eye`, not `has_eye`.)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`DispenserBlock.neighborChanged`: TRIGGERED is `hasNeighborSignal(pos) || hasNeighborSignal(pos.above())`,
so power on any of its six sides, its front included (a piston skips that one), or beside the block
above it, triggers it, and losing the power un-triggers it. A dropper inherits all of it.
A source that is not beside it sends no block update, so each change is followed by a poke: a block
placed and removed beside it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`RedStoneWireBlock.getConnectingSide`: a wire climbs onto a block beside it only if nothing
conductive sits on top of the wire itself, and steps down to a wire below a gap only if the block
beside it is not conductive. The wire is placed by an item so its connections are computed, and
with none at all it is drawn as a cross, which tells a real connection from a cut one.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`RedStoneWireBlock` connects through each neighbour's `shouldRedstoneWireConnectTo`: any signal
source from any side, a repeater only along its axis, an observer only when it faces the wire. And
`getConnectionState` draws a wire whose only connection is on one axis straight, so the opposite
side connects too, where one with no connection stays a dot. Twelve neighbours.
(A first draft expected the opposite side to stay unconnected, and was wrong.)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonBaseBlock.getNeighborSignal` skips the side the piston pushes towards, then also takes what
powers the block above it: that block's neighbours in every direction but down, the pushing side
and the block two above included. Twelve placements of a redstone block around a piston facing
east: the asymmetric front exclusion and the upward-only quasi-connectivity among them.
A source that is not beside the piston sends it no block update, so it takes effect only when
something beside it changes (BUD, the reason quasi-connectivity is a quirk and not a power
source). Each case pokes the piston with a block beside it. A first draft did not, and expected
the piston to extend at once; vanilla does not either.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`TrapDoorBlock` and `FenceGateBlock` take POWERED and OPEN from `hasNeighborSignal` of their own
position and give both up with it. Oak and iron trapdoors, oak and bamboo fence gates.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`DoorBlock.neighborChanged` powers a door from a signal at either of its halves; the half with the
power beside it runs the check and the other copies the state through `updateShape`. Power beside
each half in turn, for wooden, iron and bamboo doors.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`HopperBlock.checkPoweredState` sets ENABLED to `!hasNeighborSignal`: power at any of its six sides
switches it off at once and removing it switches it back on, while power diagonally above does
nothing, since a hopper has no quasi-connectivity. Also checked at placement (`onPlace`).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`RedstoneLampBlock` lights the moment it is powered and loses its light on a tick scheduled four
ticks after the power goes: lit through the third tick, dark at the fourth, and power that returns
in time cancels it. The existing lever test only checked that it was dark after five.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PoweredRailBlock.findPoweredRailSignal` gives up at `searchDepth >= 8`: a rail takes power from a
powered neighbour up to eight rails away along the line, both ways, and not from the ninth.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`RepeaterBlock.isLocked` is `getAlternateSignal > 0` with `sideInputDiodesOnly`: a diode beside it
locks it only when it is powered and outputs into it. An unpowered repeater or comparator there does
not, and neither does a redstone block, which does feed a comparator's side input.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`RedstoneTorchBlock`: turning off adds a toggle; eight inside the last 60 ticks burn the torch out
and `isToggledTooFrequently` keeps it dark until the toggles age, with a restart tick scheduled 160
ticks out. Seven quick cycles leave it working, the eighth burns it out, and eight spread over 96
ticks never do.
The lever is clicked, not rewritten: `LeverBlock.pull` is what updates the neighbours of the block it
is attached to, so a plain setBlock of a powered lever never reaches the torch above it. (A first
draft rewrote it, saw the torch never toggle, and was wrong about why.)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`TargetBlock.updateRedstoneOutput` schedules the reset 20 ticks out for an arrow and 8 for anything
else, and a hit while the output is held changes nothing (`hasScheduledTick`), the new strength
included.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`TargetBlock.getRedstoneStrength` is `max(1, ceil(15 * clamp((0.5 - distance) / 0.5, 0, 1)))` from
the hit's distance to the face's centre. The expected values are vanilla's formula run in Java for
51 hits on three faces, double rounding included: the same offset gives 12 on one face and 13 on
another, because frac(6.6) and frac(5.6) are not the same double.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`CopperBulbBlock.checkAndFlip` flips LIT only on a rising edge of power and lets POWERED follow the
signal either way, so the light alternates on every power-up and ignores the power-downs. Five
variants (plain, exposed, weathered, oxidized, waxed), and one placed beside power, where onPlace
runs the same check.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
Part of #14043: a pure move to entity_nbt/rocket.rs, so the rocket's
fall_distance (#14318) lands in the moved file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The six cosmetics settings (my cosmetics, in single player, other players'
cosmetics, hide others' trails and auras, Horizon logos, show me as Horizon)
leave horizon-client-render's Extras for game/cosmetics/settings.rs:
- their horizon-extras.txt lines go through a settings Store (same keys, so
players and the launcher's Wardrobe keep their choice; others' line still
written only once chosen), always registered so a game without cosmetics
keeps the Wardrobe's lines;
- their rows are a Place::Extras page section, shown while cosmetics are
offered, in the same Cosmetics group at the end of the Extras page;
- the values live in the per-seat Seat, and the owner flag is read there
instead of being copied into Extras each frame.
horizon-client-render no longer depends on horizon-cosmetics: the allowed
entry "horizon-client-render -> horizon-cosmetics" is removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ComparatorBlock.calculateOutputSignal`: no input is no output; a side input above the input
silences it; otherwise COMPARE passes the input through and SUBTRACT outputs `input - side`.
`shouldTurnOn` agrees except that equal inputs turn COMPARE on and SUBTRACT off. Nine cases, read
from the wire in front of the comparator. Signal strengths come from lever-and-wire lines (a wire
gives up one level a block).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`ButtonBlock.press` powers the button and schedules a tick `ticksToStayPressed` out. Stone buttons
stay pressed 20 ticks and every wooden one 30 (oak, bamboo and cherry pinned). A button that is
already pressed answers a second click with nothing: the timer is not extended.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
A memory connection's outbox closed past MAX_PENDING (96 MiB), as a
socket's does. A memory connection gets every pending chunk at once
(PlayerChunkSender.collectChunksToSend), so a player joining where the
terrain is already loaded can queue more than that in one burst, and
the server closed its connection with no reason: "<name> could not
stay / End of stream".
The limit is for clients that read slowly over the network; those keep
it.
Fixes#12849.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Continues #1180. With ray queries on (`HORIZON_RAY_TRACING=1` in a horizon-extras
build), `ray_scene` keeps one BLAS per section's solid quads within 8 chunks of
the camera (`HORIZON_RT_CHUNKS`), compacted once built, and a TLAS of them, built
again only when a section's mesh changes or the camera enters another chunk
column (instances sit relative to that column). Nothing traces them yet.
The arena keeps its compact layout: a section about to get a BLAS has its solid
quads written out as vertices by a compute pass (`rt_unpack.wgsl`, appended to
terrain.wgsl to use its `section_corner`) into a reused buffer.
The off path is main's: `without_ray_queries_there_is_no_scene` opens a device
without ray queries and checks there is no scene and no extra buffer usage. The
renderer's side is three one-line calls in gfx.rs (`ray_mark`, `ray_clear`,
`ray_prepare`), the methods living in ray_scene.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- EndermanLeaveBlockGoal.canPlaceBlock: level.getEntities(this.enderman, cube)
must be empty, so the player (not a spectator), dropped items, armor
stands, boats, projectiles, frames and every other entity block the cube,
and the enderman's own box no longer does.
- Enderman.playStareSound: once per 400 ticks (lastStareSound), stamped
but silent for a Silent enderman, at getEyeY (2.55 up). It plays when
DATA_CREEPY changes while DATA_STARED_AT holds (onSyncedDataUpdated, value
by value in id order), so the end of a stare's anger plays it too.
- Enderman.setTarget stamps targetChangeTime for every target and zeroes it
on none, which customServerAiStep's daylight teleport reads.
- EndermanTakeBlockGoal.tick clips with ClipContext.Block.OUTLINE, the
shapes' random offset included: a flower or torch now hides a block.
The block random offset (BlockBehaviour offsetType) moves to
StateFacts::offset_at, shared by the mesher and the mob clip.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Run against the 26.3 SignBlockEntity: an entry that fails is dropped
(ListCodec's partial) and Util.fixedSize keeps the first 4 lines of a
longer list with the colour and glow; only fewer than 4 valid lines load
the empty side.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: every read of settings.extras.pvp outside the PvP module now goes through one accessor in game/extras/pvp.rs (pvp_legacy_look, pvp_mutes_sound, pvp_hides_particle and so on), same values at the same places, so nothing new runs per frame.
🤖 Agent: pure move of game/pvp.rs to game/extras/pvp.rs (git sees a rename), stacked on #13930; only super:: paths and visibility change, docs/pvp.md follows the path.
Frames are pickable (BlockAttachedEntity.isPickable), so a projectile
meets them and ItemFrame.hurtServer runs: a filled frame gives its item
up on any non-explosion hit, an empty one breaks, a fixed one refuses
unless the shooter is creative (and a refused arrow is turned back).
Horizon left frames out of every projectile's targets.
projectile_targets/vehicle_targets move to creatures/projectile_targets.rs
and list frames; attack_frame's body becomes hit_frame, shared with the
new projectile_hurts_frame, which arrows, tridents and hurt_vehicle's
callers reach. New harness test_frame_projectile_parity.py (27/27;
main: 22/27).
Part of #11383
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.checkBelowWorld acts below Level.getMinY() - 64. Horizon used the
chunks' column bottom, which is widened to the Overworld's -64 in every
level, so in the Nether and the End (min Y 0) players and mobs only took
void damage, and arrows, thrown potions and other projectiles were only
removed, below -128 instead of -64; a guest's death there was not named
"fell out of the world". World::below_world_y gives the level's own floor
minus 64, and an entity exactly on it stays, as vanilla's strict < keeps it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
MaceItem.hurtEnemy resets the attacker's fall and sets its motion, and the
server sends that motion to the attacker in a ClientboundSetEntityMotionPacket
right after the hit. A LAN guest's PvP mace smash now sends the motion the
hit gave it, as it already did for mob hits. The server_knockback harness no
longer lists pvp-mace as a known difference.
Fixes#8246
`tools/port_map/game-012.tsv` records `Sheep.readAdditionalSaveData`; its
`Color` read now goes through `mobs/farm.rs::saved_color`, which gives white
for an id the legacy codec does not know.
`mob_from_nbt` is what `Game::load_mob` goes through. A sheep with
`Color: 99b` loads with `extra.color == 0` (white), not the wrapped 3.
Without the codec bound the sheep loads light blue: `left: 3`, `right: 0`.
`Sheep.readAdditionalSaveData` reads `Color` with
`DyeColor.LEGACY_ID_CODEC` and `orElse(DEFAULT_COLOR)`. The codec's id map is
`ByIdMap.continuous(..., OutOfBoundsStrategy.ZERO)`, so an id outside 0..=15
reads as the first colour, white.
Horizon masked the id with `& 15`: a saved `Color: 99b` became light blue (3)
instead of white, and a negative id became a wrapped colour the sheep never
had.
The read now accepts only 0..=15 and gives white for anything else, in
`mobs/farm.rs::saved_color` beside the sheep's other colour code.
Entity.checkBelowWorld discards at getY() < Level.getMinY() - 64, so the Nether and the End (minY 0) lose an item at y = -64, not the Overworld's -128. Use the level's minimum y in ItemEntity.tick and the item tick, and keep an item at exactly the threshold, as vanilla's < does.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
JigsawBlockEditScreen sets a tooltip on its selection and placement priority
boxes, explaining what each number does. Horizon's jigsaw editor drew both
boxes without one. The block editor now takes an optional tooltip per box and
draws it over the screen, and the jigsaw gives its two priority boxes theirs.
PatchedDataComponentMap keeps only values that differ from the prototype:
a diamond pickaxe already has DAMAGE 0, so the zero components_tag now keeps
for a stack-declared max_damage is dropped again by the prototype-aware save.
A stick, whose prototype has no DAMAGE, keeps it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every read of settings.extras.pvp outside the PvP module and its HUD
editor (glint, attack indicator, legacy look and rules, sound and
particle filters, shield and totem size, fire height, clear fluids,
chat search and copy) now calls one accessor in game/extras/pvp.rs.
Same values, read at the same places: each accessor becomes one engine
knob or hook when PvP & HUD leaves the engine.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M6. horizon-server-files gets chat_guard (the ChatGuard trait, Place, Action, Mute, and an
install/load registry). horizon-chat-filter implements the trait and its loader; entry installs it
at startup. Behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.getSecondsToDisableBlocking reads
Weapon.disableBlockingForSeconds. Horizon read the prototype's value, so a
patched weapon disabled a shield for 0 seconds instead of its own.
Test: a diamond sword with disable_blocking_for_seconds 1.5 reports 1.5.
frame_format, frame_configuration, overlay_format and horizon_tone_curve
move unchanged beside their tests (frame_policy_tests.rs), so gfx.rs does
not grow against main with linear blending in.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both add an equippable reader beside `equippable` in item_sync.rs: with
#13748 merged in, either lands first without a conflict. The new test's
doc comment now describes it, and the glider test keeps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new test went between the glider test's doc comment and its fn.
Also check that a removed component plays no equip sound, as the
description says.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three loom PRs change banner.rs side by side: with the others merged
in, any of them can land first without a conflict. All their tests kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both PRs change the loom's component reads in banner.rs: merging #13744
here lets either land first without a conflict (both tests kept).
provides_banner_patterns names `#minecraft:pattern_item/<name>`, a tag in
a subfolder, and an id may omit its namespace: load every tag under
tags/banner_pattern and namespace bare ids (Identifier.parse), with
test cases for both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game workspace pinned winit to an unreleased backport in a personal
fork (crmne/winit) for native Wayland file drops. Every player build would
then depend on a repository outside winit's own, which the project doesn't
take. The drop import itself works wherever winit already delivers
DroppedFile (Windows, macOS, X11, XWayland); native Wayland follows once a
winit 0.30 release carries rust-windowing/winit#4729.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The same fix (compare with the main the pull request lands on) is open as #13882. A change to
.github/ also keeps a fork's runs from being approved automatically, so this branch got no parity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ItemStack.isDamageableItem` asks for the MAX_DAMAGE, DAMAGE and
absence of UNBREAKABLE components, not for the item prototype's own
durability. A stick given `minecraft:max_damage` and `minecraft:damage`
is therefore damageable and wears while mining, but Horizon only looked
at the prototype `max_damage` in `mining_damage_capacity` and in the
attack wear path, so such stacks never took durability damage and an
explicit `damage: 0` was dropped on save.
- `durability::stack_max_damage` mirrors `getMaxDamage` for any stack:
MAX_DAMAGE present, DAMAGE present (prototype, stack value, or an
explicit zero), no UNBREAKABLE.
- `mining_damage_capacity` and `damage_held_in_slot` both use it.
- `components_tag` keeps an explicit `damage: 0` when the stack carries
its own `max_damage`; durable prototypes still drop the zero, matching
vanilla's `fromPatch` sanitizing.
The capacity logic lives in its own module so `items.rs` (already past
the 3000-line limit) does not grow; the thin method there delegates.
Completes the deferred part of #11316: custom durability on non-durable
item types. Covered by unit tests, the `mining_component_tests` game
regression, and 74 stack-mining parity cases against vanilla 26.3.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
A pure move under game/extras/, where each Horizon extra's game glue
gathers: paths and visibility only (pub(in crate::game)). game.rs
loses its mod line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move: the connection's start, join and end, the countdown and the
retry leave game/extras.rs for a child module of their own, so the extra
leaves the engine in one move once the seam lands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityRandom (Entity.random) and LevelRandom (Level.random) wrap a
LegacyRandom in horizon_core::random, each with one named constructor
(EntityRandom::own, LevelRandom::for_level) and its own positional type.
World.random is a LevelRandom and Mob.random an EntityRandom, with
entity_random, living_random, Mob::with_random and
mob_from_nbt_with_random. Both deref to LegacyRandom while the rest
migrates (#13813). No draw changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::seen moves to mobs/seen.rs and the FollowMob test to mobs/invisible_follow_tests.rs, so neither file over
3000 lines grows: 6858 -> 6777 and 3740 -> 3738.
Entity.checkBelowWorld runs in AbstractBoat.tick (through Entity.baseTick,
after the fire) and in AbstractMinecart.tick (after its hurt and damage wear
off), before either moves: under the level's floor minus 64, onBelowWorld
discards it. Horizon had no such check for vehicles, so a boat or minecart
that fell out of the world kept falling for ever (#8024 measured one at
y -243 where vanilla had removed it by tick 33).
The discard ejects the mob riders where they are and spills a chest boat's or
container minecart's slots (AbstractChestBoat.remove,
AbstractMinecartContainer.remove), with no ENTITY_DIE and no vehicle item.
The logic lives in game/boats/below_world.rs; minecarts.rs keeps its size.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A projectile added keeps an earlier UUID for its id (a client copy's from the
spawn packet) through or_insert again, so the client copy needs no reset.
Part of #12561
Co-Authored-By: Claude Opus 5.5 <[email protected]>
entity_nbt.rs may not grow (over 3000 lines), so the sulfur cube's own load
re-reads its saved Health after sizing it instead of the living load
skipping the Health Boost clamp: the result is the same, the clamp to the
unsized maximum no longer sticks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CTM picks a texture, not a render layer: OptiFine draws a tile in the
block's layer. Horizon took the pass from the tile's own sprite, so one
of Conquest Reforged's six random grass blade tiles (7 partly clear
texels of 1024) put its overhang plane in the translucent pass. That
pass has no alpha test, so the plane's clear rectangle wrote depth
(depthtex0 had it, depthtex1 not), and Bliss fogged it: faint
rectangles against the sky. A tile now takes the face's pass; its own
texels still decide whether it skips the alpha test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Identical still sprites share one layer. Chaining looked the picked tile's
rules up by that layer, so a tile chained to every rule naming a tile with
the same pixels. Conquest's grass top repeats 16 blank placeholders that
random rules beside them replace (matchTiles=./N.png); dozens of other
folders' placeholders are blank too, and their rules, sorting first, drew
their own tiles on the grass. A rule's tile now chains to the rules naming
that tile, and a tile file (not a model's sprite) no longer matches faces by
layer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The living load refreshed a Health Boost holder's maximum and clamped its
Health before sulfur_cube::from_nbt sized it, so a size-3 cube saved with
Health 12 under Health Boost dropped to the constructor size's 8. Vanilla
sizes the cube (AbstractCubeMob.readAdditionalSaveData) before
LivingEntity's setHealth; its own load now sets the maximum.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The loads take the own random's UUID through one helper, so persist.rs and
creatures.rs keep their size. A client copy drops its own random's UUID: the
spawn packet's, kept before it is added, wins.
Part of #12561
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SulfurCube.setSize sets the max_health (4 per size) and movement_speed
(0.2 + 0.1 * size) attribute bases, so every load, spawn, split and bucket
release leaves both instances to save. Horizon's set_size wrote the cached
values only: the save held no max_health, and from the first tick a
movement_speed of the type's 0.7, which a reloaded cube then moved at.
set_size now records the bases (the spawn path did that alone before), the
load applies saved bases after the size as LivingEntity does, and Health
Boost changes refresh a sulfur cube's maximum like any model-owned one.
Fixes#13801
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ai.rs is over the 3000-line limit (docs/layers.md#file-size), so the
Hoglin.canFallInLove check and its explanation move to
piglin::refuses_love; takes_food keeps one line for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.doHurtEquipment skips a piece whose Equippable.damageOnHurt is
false. Horizon read the prototype's flag for both the player's armor and a
mob's, so a stack patched to false still wore.
Test: a golden helmet patched with damage_on_hurt:0b does not wear, and a
removed equippable component does not wear.
Player and mount equipping read the item prototype's equip_sound. A stack's
own minecraft:equippable replaces the component, so its equip_sound (or the
generic default when omitted) must win.
Test: a golden helmet with a slot-only equippable component uses the generic
sound, not the gold one; a patch naming a sound uses that sound.
LoomMenu.isDyeItem needs the LOOM_DYES tag and a present DYE component, and
the result uses that component's colour. Horizon read the prototype's dye,
so a patched or removed dye kept its item default.
Test: white dye patched red colours red; white dye with !minecraft:dye is not
a loom dye.
LoomMenu.getSelectablePatterns reads the stack's PROVIDES_BANNER_PATTERNS
HolderSet. Horizon read the item prototype's list, so a patched pattern item
offered the wrong patterns.
Test: a globe banner pattern patched to creeper offers creeper, and a removed
component offers none.
LoomMenu.isPatternItem needs the LOOM_PATTERNS item tag and a present
PROVIDES_BANNER_PATTERNS component. Horizon accepted any tagged item whose
prototype provided patterns, even after !minecraft:provides_banner_patterns.
Test: a globe banner pattern with the component removed is not a pattern
item.
Boat steering, a boat rider's dismount vector and a minecart rider's push
used the platform's sine and cosine, where vanilla uses `Mth`'s 65,536
entry table, so a boat paddled at most yaws drifted from vanilla's path
(about 1e-4 of the push per tick). They now go through
`horizon_core::geometry::trig`, as `AbstractBoat.controlBoat`,
`Entity.getCollisionHorizontalEscapeVector` (reusing the mounts port)
and `Entity.getInputVector` do. The dismount also uses vanilla's float
products: the boat's width times `Mth.SQRT_OF_TWO`, the 1.0E-5F margin and
the player's 0.6F width.
`getWaterLevelAbove` scans from the hull's top up to
`ceil(maxY - lastYd)`, where `lastYd` is the boat's vertical velocity at
its last `checkFallDamage` (before collisions zero it); Horizon scanned
only the hull top's own row, so a boat falling fast into deep water could
pick another surface height.
Fixes#11381
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's output owner (#13233) decides each renderer's frame in gfx/output.rs.
Whether the frame holds linear light is now part of that decision:
`FrameRequest::linear_blend` carries the picked 0.2 pack's `[frame] blend`, and
`resolve_frame` sets `FrameConfiguration::linear_light` where the frame is the
float one. The renderer reads it from its output instead of a field of its own,
so a pack that changes its blend asks for another frame and the game makes the
renderer again through main's frame check, without a separate outcome key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under a 0.2 pack with `[frame] blend = "linear"`, two draws still mixed or wrote
vanilla's gamma values into the linear frame:
- A section fading in mixed the pack's material result, linear light, toward
the gamma fog colour, so new chunks faded in too bright and unlike "gamma".
The fade now mixes in gamma, as the fog does.
- A resource pack's OptiFine custom sky layers, drawn when the pack has no sky
stage, wrote gamma, so that sky looked washed out. They now store their
colour decoded. Multiply and overlay layers convert their factor so the
product stays what it is under "gamma". Add and subtract add light, as
"linear" means to; dodge, burn and screen come out close to their gamma
results.
Under "gamma", and without a 0.2 pack, nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read these structure pieces from saved starts instead of regenerating their
layouts and losing placed chest or spawner state. Follow StructureStart.loadStaticStart,
PiecesContainer.load and the MineshaftPieces, NetherFortressPieces and
StrongholdPieces tag constructors in vanilla 26.3.
Keep whole-start regeneration when a required piece cannot decode. Preserve
stronghold Source flags and vanilla numeric and optional entrance defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
A hoglin within reach of a #hoglin_repellents block keeps its PACIFIED
memory for 200 ticks, and Hoglin.canFallInLove is false while it is set.
Animal.mobInteract then refuses the crimson fungus: nothing is eaten, no
love mode, no baby. Its baby branch asks canAgeUp instead, so a pacified
baby still grows up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The examples that make a renderer of their own were still passing a bool where
`Renderer::new` now takes the picked pack's `[frame] blend`; they pass `None`,
no pack, so they draw as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guard before creating fixtures because Cargo runs unit tests from their package directory. Validate the integration test by invoking its built executable directly from an isolated game directory.
Co-Authored-By: OpenAI Codex <[email protected]>
Fixes#11698. Route resource-pack drops through the vanilla confirmation, copy ZIPs and folders without overwriting existing packs, and refresh the available list while preserving pending selections. Keep Create World's data-pack route and cover the UI, copy safety, and both event callbacks.
Co-Authored-By: OpenAI Codex <[email protected]>
A 0.2 pack with `[frame] blend = "linear"` was refused: the game showed the
"isn't served yet" message and drew without it. Now it draws. Its float frame
holds linear light, so its glass, water and smoke blend as light does, while
solid surfaces look as under "gamma".
The pack's material, sky and weather results are stored as they are. Horizon's
own draws into the frame (entities, particles, clouds, weather, the sky, stars,
sun and moon, the world border, outlines and the debug lines, world warp and
the visibility buffer) decode their colour at the end of their shaders. The
clear colour is decoded too. Block breaking keeps darkening as vanilla's, and
the material's fog still mixes in gamma. What a stage reads (scene_color,
opaque_color, the water mirror) is copied instead of decoded, so [post],
[tonemap] and everything after the tonemap (the hand, camera overlays, HUD,
screenshots, HDR output) see the same values under either blend.
Where the device can't draw a float frame, the pack draws on the 8-bit frame
in gamma instead of being refused. Resource packs' core sky, stars and line
shaders write gamma, so under "linear" Horizon's own draw in their place.
Changing a pack's blend now makes the frame again, like changing its API line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Entity makes its UUID from the first two draws of its own random
(Mth.createInsecureUUID), and Projectile.shoot spreads the shot with that same
random, from which an arrow keeps drawing (AbstractArrow.setBaseDamageFromMob,
its later rolls). Horizon shot dispensed, crossbow and mob projectiles with a
fresh random that never drew the UUID, player throws with the level's random,
gave an arrow's later draws a random of their own, and gave projectiles a
clock-seeded UUID when first asked.
An arrow now makes its own random in Projectile::new and draws its UUID first;
another kind makes it at the shot (shoot_own), the same stream since nothing
draws between. Every shot goes through shoot_own, the arrow keeps the random,
and the skeleton's base damage comes from it. add_projectile keeps the UUID; a
player's trident takes its shot's. Loaded and summoned projectiles take their
own random's UUID unless the tag has one.
The seed sync proof reads a dispensed arrow's and snowball's UUID and Motion
and a summoned arrow's UUID: 25/30 on main, 30/30 with this.
Part of #12561
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the winit 0.30.13 backport from rust-windowing/winit#4729, pinned to its reviewed revision. Document the Apache-2.0 dependency and removal condition.
Co-Authored-By: OpenAI Codex <[email protected]>
Player.getDestroySpeed, Player.hasCorrectToolForDrops and
BlockBehaviour.getDestroyProgress move from horizon-client's gameplay into
horizon_items::mining, taking plain inputs, so frontends that cannot link
horizon-client mine by the same rules. destroy_progress in game/hand.rs
gathers the player's modifiers and calls it, with the native_break_speed
hook still where getDestroySpeed ends; break_block_as_player uses the same
harvest check. Same f32 operations in the same order: no behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -22,13 +22,6 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
is the GitHub account the PR is opened from (`gh api user --jq .login`). Issues say "Filed by", reviews
"Review by". Every comment starts with `🤖 Agent:`. Write only as the person you work for: the lines on
this page that name Ludovic are for his own agents (account `ludovic111`), never for anyone else's.
- **Review only when asked, and decide.** Review a pull request only when its author or a maintainer asks.
Asked by a maintainer to review open pull requests in general, take them one at a time, oldest first, skipping
drafts, your person's own and those they already reviewed. Check that the change does what it says (bugs, edge
cases), what it could break, whether tests cover it, and whether its scope and description hold up. Report only
what you can point to with a `file:line`, each point marked Blocker (fix before merge), Important, Nit,
Suggestion, Question or Praise. Submit a decision, never a plain comment: `gh pr review --approve` when nothing
blocks, `--request-changes` when something does.
- **Attribution.** Commits end with a `Co-Authored-By:` line for the agent that wrote them, and PR bodies
with that agent's own "Generated with" line. Example, Ludovic's agents (Claude Code on `ludovic111`):
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
@@ -135,8 +128,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
## Build and test fast
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests; paste its block in the PR
python3 tools/pr_ready.py --harnesses # also the affected harnesses (by default only when you change a harness, oracle, probe or the runner, as in CI)
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
description="The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
[dependencies]
# The moderation durations, shared with `/tempban`.
# The hook the hub asks (`chat_guard`), and the moderation durations.
"F3+R starts or stops a repro recording and F3+E writes its bundle. Off by default: a recording copies the world."
}
Item::CosmeticsMine=>{
"Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe."
}
Item::CosmeticsOthers=>{
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
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."
}
Item::HorizonMarks=>{
"Show the Horizon logo next to Horizon players: a small logo before the names of other players who use Horizon, above their heads and in the player list. Only Horizon players see it: servers receive nothing."
}
Item::HorizonMe=>{
"Show me as a Horizon player: other Horizon players see the logo by your name. On, the service answers anyone who asks about you that you played Horizon in the last 3 hours; off, it never reports you."
}
Item::Pvp(o)=>o.description(),
Item::WheelCyclesSettings=>{
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
Loaded 100 of 626 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.