Keeps both new tests at the end of hub_tests.rs (main's teleport accept test and the equipment cache test).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs may not grow after main moved the mob loop out; end_mob clears the mob's context, Projectile::tick sets its own, and the fangs start with none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
gfx.rs is over the 3000-line limit and may not grow, and the 4x terrain MSAA work adds to it. The impl block from resident_sections to cpu_census moves unchanged.
Co-Authored-By: Claude Opus 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]>
after_handler is only called on the signal path (Linux, Android, Apple),
and the tests' busy thread only serves the test that reads another
thread's stack there: on Windows clippy -D warnings stopped in
horizon-threads, and every build warned about after_handler. The function
takes the same cfg_attr(.., allow(dead_code)) as Watched::thread, and the
test helpers and their imports the test's cfg.
Fixes#15476.
Co-Authored-By: Claude Opus 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]>
Its gzip size depends on the flate2 backend that feature unification picks
(3.7 MB alone, 5.2 MB in a workspace test run); what matters is that the
announced-size cap lets it through, so the inflated cap is what stops it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On main `horizon-server report` printed the report's text summary; with the
capture it printed only the file's path, and the text form moved to
horizon-perf, which no release ships, so a host on a VPS could not read the
result there. horizon-perf-format, already shipped through the platform,
gets server_brief: the kind and length, TPS and MSPT, CPU, RSS, players,
chunks and entities, and the five heaviest tick phases. The report prints
it before the path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Any accept() error other than WouldBlock ended the server that offers a
capture to the viewer, before the viewer had fetched it: an aborted
connection (ECONNABORTED, common on BSDs and macOS, WSAECONNRESET on
Windows) or a full file table (EMFILE) left Open broken for that click.
Errors now wait 10 ms and try again until the offer's deadline or a cancel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thread's ring was sized at its first session and kept, but the size a
session needs depends on its Hz, --wall (twice the entries) and the unwind
mode (about 16 KiB per stack copy), so a later capture with --wall or a
higher --hz lost most of its samples to `dropped` (95 of 99 in the new
test). A session now gets a new, larger ring when the thread's is smaller
than it needs. A replaced ring stays allocated for the thread's life, since
a late signal's handler may still hold it; rings only ever grow, so this is
at most a few per thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The requester was keyed by connection id and never cleared on disconnect,
so after the admin's client dropped during an open-ended `profile server
start` everyone got AlreadyRunning until the 600 s window ended, and the
same admin, reconnected under a new id, got NotRunning when stopping. Each
tick's requests now include a stop (reason DISCONNECTED) once the
requester's connection is gone; the file stays in the server's reports/ and
its path is logged, and the slot frees as soon as it is saved, so anyone
(the same admin included) can start again. server_profile_e2e covers the
disconnect and the start after reconnecting.
Co-Authored-By: Claude Opus 5.5 <[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.
share_saved always reused an existing .hzperf.share.json and only re-pinned
it, so once the upload expired (a temporary one after an hour, a shared one
after 30 days) or was deleted, Share failed for that capture until the
receipt was removed by hand. A 404 or 410 on the pin of a reused receipt now
uploads the capture again, replaces the receipt (still 0600, written beside
and renamed over) and pins the new upload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
with_profiler keeps a server's or mod's own `profiler` node, so the chat
completed the server's command while handle_chat_input still answered
/profiler locally, and the server's command could not be reached. Horizon's
nodes now carry their own command id; a typed /profiler runs locally only
when the chat tree's `profiler` is Horizon's (or there is no server), and
otherwise goes to the server or mod that owns the name. The client's own
buttons keep running locally.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
click_command answered any clicked /profiler line itself, so a server's
chat component, book or dialog with run_command "/profiler share" uploaded
the player's last capture after one click. The client's own [Open],
[Share], [Start], [Stop] and retry buttons now carry a client-only click
action that no server text decodes to (as [Folder]'s open_file already
was); a clicked run_command is a plain command again, sent to the server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merger was taken out before finish(), so when finish() failed the
abandoned part was closed with a fresh merger that had yielded nothing,
and no Stop{DISCONNECTED} was written for a part that began. finish() now
borrows the merger, which keeps what it yielded for cut_short.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client took whatever size a server announced and inflated every chunk
without limit, and appended fields of any side into the player's capture.
The announced size is now capped at 64 MiB (the share endpoint's own cap),
the inflated part at 512 MiB (the timeline's 256 MiB cap plus the rest of
a 600 s capture; real captures inflate about 3x from 130-230 KB a minute),
and one field at 64 MiB, which also bounds what waits half-arrived. A field
whose message is not side = SERVER, or has no side (an attachment), refuses
the part; what arrived stays with the incomplete warning, as for any part
that fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 kacpherek's request on PR #14365: remove the post-hit tie scan while keeping slice-based search and the short-list upper bound. Extend the existing regression test with 16-entry duplicate and signed-zero lists; expected indices were observed on the JVM and the pinned vanilla method was checked with javap.
Validation: cargo check -p horizon-assets --locked; cargo test -p horizon-assets --locked (53 passed); cargo fmt -p horizon-assets -- --check; cargo clippy -p horizon-assets --all-targets --locked -- -D warnings.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
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]>
Address the reviewer request by retaining main's invulnerable_time field rename alongside the PR's RNG changes.
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]>
The attack-checks test only asserted that attacking a pig does not
disconnect the guest. It now also asserts that the pig lost health,
so a normal attack on a mob is shown to still land.
Co-Authored-By: Claude Opus 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
The device-code interval, its expiry and the Minecraft token lifetime came from the services
unchecked; Instant + a huge interval panicked the sign-in thread and left the window on the
code. The interval is clamped to 1-60 s (slow-down too), the device code's expiry to an hour
and the token lifetime to a week, all added with saturating_add.
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]>
A compressed frame declares its uncompressed size up front, and the inflater reserved that
much at once: a few bytes declaring 8 MiB made the reader allocate 8 MiB per frame. The
buffer now grows as output arrives (capped by DEFLATE's ~1032:1 ratio), so a lying frame
is refused after a small allocation and a real large body still inflates whole.
Split out of #15249 at review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
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]>
ServerGamePacketListenerImpl.handleAttack disconnects a player whose
attack names an ItemEntity, an ExperienceOrb, the player itself or an
AbstractArrow that is not isAttackable, with
multiplayer.disconnect.invalid_entity_attacked, after the world border,
attack range and PIERCING_WEAPON checks. A hosted or dedicated Horizon
server let the attack end without a word.
handleAttack's checks before Player.attack move to
game/lan_actions/attack_checks.rs: the border and range checks
guest_attack already did, then that test. An arrow's isAttackable reads
#minecraft:redirectable_projectile from the tags the players were sent,
so a data pack that adds arrows to it keeps them attackable.
Co-Authored-By: Claude Opus 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 pass timing switches and readings and the world pass helpers move
from gfx.rs to gfx/pass_timing_api.rs, the streamer's queue census and
job_stats to streaming/, Horizon's mark sprite to mark_sprite.rs, and
`horizon-ctl lan op` to game/lan_op.rs. A guest's plugin channel
routing moves beside horizon_channel, the client takes the profile
channel's replies before handle_packet, and tick_profile starts and
ends a mob's tick for both the sampler and the mob costs. The server
bench test reads the saved line the server prints now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Removes the fork's review and claim tooling and its docs, and the scene
corpus, which the design keeps separate (an input fixture, not a
measurement). The design doc describes the viewer and upload API in
horizon-rust/horizon-site and drops the planning sections; tests and
doc comments point at playhorizon.dev and horizon-site. The debug keys'
comment is upstream's again.
Co-Authored-By: Claude Opus 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 reader skips a version broken on the player's Horizon commit for an
older one that builds; only downloads, stars and updated are for show and
sort. None of the four is checked against a downloaded file.
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]>
`projects[].updated` is the date of a project's newest version, set by the
registry's publish job (horizon-rust/horizon-mods#683). While the index is
preview, a reader may rely on every field but the publish job's four
(`downloads`, `stars`, `updated`, `broken_on`), which may be absent at any
time. Point (3) of #10562.
Co-Authored-By: Claude Opus 5.5 <[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
Preserve the contributor's renderer extraction unchanged while including main's fix for the Git maintenance race in test_git_slim. The diff against main remains the same four moved methods and port-map citation.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
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.
EditBox.handleCursor requests CursorTypes.IBEAM while hovered and editable, and
NOT_ALLOWED when it takes no text. Horizon's cursor set had no I-beam, and the
pack screen's search boxes and the settings shell's search never asked for one.
widgets::text_cursor adds it, the two screens call it where their fields are
drawn, and the client maps Cursor::Ibeam to CursorIcon::Text.
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]>
Preserve the contributor's TNT power change and the minecart extraction already on main. Git resolved both touched files without manual conflict edits. The net change against main is three files (+82/-2).
Validation: rustfmt, public-tree, layer/file-size checks and three command-fixture Python tests pass. The focused Rust test attempt failed in sccache before compiling Horizon; vanilla resources and the port ledger are unavailable, so gameplay proof remains outstanding.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
`a_full_hopper_cannot_take_a_book_from_a_chiseled_shelf` asks the rule
with a hopper full of stone (false) and an empty one (true). Without the
shelf rule both answer true.
`ChiseledBookShelfBlockEntity.canTakeItem` is
`target.hasAnyMatching(other -> other.isEmpty() || (same item and
components && other.count + stack.count <= target.getMaxStackSize(other)))`:
a hopper may take a book only when it has a free slot or a matching stack
with room.
Horizon's `Containers::can_take` had no shelf rule, so a full hopper
still pulled a book out (and put it back). The hopper's path now asks
`can_take_into`, which applies the shelf rule before the existing checks.
`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).
Vanilla's predicate is `state.getFluidState().is(FluidTags.WATER)`, which
the inline 9x2x9 search already matches; the row's `canBeHydrated` gap
was wrong.
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]>
ThrownEgg.onHit sets the chicken's variant from the item's CHICKEN_VARIANT component (egg temperate, blue_egg cold, brown_egg warm); a thrown or dispensed egg now carries it.
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]>
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]>
PrimedTnt.getOwner names the mob a saved owner UUID points at, so its
blast's indirect source is no player and tryAwardExperience awards
nothing. Keep whether the owner is a player beside blast_owner (a chain
reaction hands a mob owner on as a mob). Block.popExperience draws the
amount, then pops nothing with block_drops off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Fox.aiStep` finishes the item in the mouth with `ItemStack.finishUsingItem`
(`Consumable.onConsume`), which ran only the chorus fruit's teleport here: run
the whole consume in vanilla's order — `emitParticlesAndSounds` (the
consumable's sound, and the four draws a crumb of `spawnItemParticles` costs,
though `Level.addParticle` is empty on a server), `FoodProperties.onConsume`'s
own sound, the `on_consume_effects` through `LivingEntity.addEffect` (so a
golden apple's regeneration and absorption land) and `GameEvent.EAT` at the
fox's feet with the fox as its source.
`Panda.pickUpItem` calls `onItemPickup` before it takes the item, so
`CriteriaTriggers.THROWN_ITEM_PICKED_UP_BY_ENTITY` fires for whoever threw it,
as it already did for the fox (`LivingEntity.onItemPickup`).
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
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]>
CreakingHeartBlock.onExplosionHit awards the heart's experience
(tryAwardExperience) when the explosion's indirect source is a player, a
primed TNT's owner, unless that player is in creative or spectator
(preventsBlockDrops). Horizon always broke the heart as if no player
caused it.
Part of #11330
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]>
The e2e capture's live heap was all on horizon-profile: the CPU
sampler's drain thread copies the binary's unwind sections (~9 MB,
framehop's Module::new from StackUnwinder::refresh) at the start of the
capture and holds them to the end, so the heap sampler profiled the
profiler. Samples were already tagged with the allocating thread.
heap_sample::ignore_this_thread() stops sampling a thread's allocations
(its frees still leave the live set). The sampler's drain thread, the
capture writer (WriterThread::spawn_with), the finishing thread and
the gauges thread call it.
server_perf_e2e checks live bytes over every thread, none on a profiler
thread, and that the top live function is not the profiler's; it
samples every 64 KiB so its 4 s capture holds some. It now passes: 1.0
MiB live on tick, workers and chunk threads, 19,453 samples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chat keeps the lines with a next step (started, saved, shared, failures); saving,
uploading, opened and marked go to the action bar; the saved line carries the
capture's length from when stop was asked; [Retry] names its profile.
Text::styled dropped the style's inline object (Style::inherit never passes one
on), so the profiler tag drew the font's [OBJ] box instead of horizon:gui/mark.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Avoid shadowing the ownership map with the layer-key list so the latest main branch compiles after the CEM armour-layer merge.
Co-Authored-By: Codex <[email protected]>
By rdbtCVS's AI agent (OpenCode, GPT 6.1 Sol). Preserve the contributor and maintainer history while including main's merged #15027 repair for the inherited studio_replay and rocket RNG compilation failures. No PR-specific code changes.
Co-Authored-By: GPT 6.1 Sol <[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]>
Apply the burst damage-calculator multiplier to the Ender Dragon impulse before native knockback hooks, and cover the behavior with a regression test.
Co-Authored-By: Codex <[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]>
Use the production timeout builder rather than an unrelated test agent, and
cover IPv4-first multi-address and IPv6-only resolver ordering.
Fixup by rdbtCVS's AI agent (OpenCode, GPT 6.1 Sol), not by rdbtCVS personally.
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]>
`horizon-ctl memory` and a capture's memory snapshots are built from `#[derive(Serialize)]` structs instead
of `json!`; existing keys keep their shape. A dedicated server (no window) names its columns `columns`;
single player keeps `client_columns` and `server_columns`.
New, all estimated from capacities:
- `entities`: count and bytes by entity type (top 30), across every level
- `players`: count and, per player by name, the send queue (packets and bytes), chunks waiting, chunks sent
and tracked entities
- `chunk_send`: the encoded section and heightmap caches and the shared compressed frames
- `light_queue`, `generation_queue`: lengths and the bytes of their tables
- `allocator`: `live_heap_mb` (mimalloc's statistics), `commit_mb`, `peak_commit_mb`, `overhead_mb`
- `named_mb` and `unattributed_mb` (resident minus the named parts) on every side
The server e2e asserts a stop snapshot has `players`, `entities` and `unattributed_mb`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`hot` is the top 30 functions (the `hot` op's row) of the main thread's samples in the time blocks
overlapping the tick, only the slow samples when the tick is slow, and `samples_from`/`samples_to` say the
window those blocks cover. With 1 s blocks that is the second around the tick, not the tick's own samples.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`memory_labels` (CLI `query memory-labels`, MCP and wasm through the shared op list) gives one row per
snapshot, `{label, t, side, rows}`, in time order. `counters` gives one row per counter definition,
`{id, name, unit, kind, labels, side, samples}`. Each has a golden reply. `memory` also ranks a negative
megabyte row by its size, so `unattributed_mb` can show.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`block_s` defaults to 1.0 and `MAX_TIME_BLOCKS` is 1800 (30 minutes, the block index a `u16`); a longer
session keeps the rest in the last block, as before. The reader selects every block a `--from`/`--to`
range overlaps, a range inside one block picks that block, and the reply's `selection.from`/`to` name the
blocks the weights cover, so a 2 s range no longer leaves Functions and the flame graph empty.
Capture size before and after on the 200-bot benchmark and a client capture is not measured yet: pending a
quiet machine. If growth is over 2x, blocks older than 5 minutes get merged at write time.
Co-Authored-By: Claude Sonnet 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]>
horizon-threads' heap_sample wraps mimalloc and is the game's allocator
on Linux (alloc-count and alloc-census still replace it). While a
capture runs it samples about one allocation per 512 KiB
(HORIZON_HEAP_SAMPLE_KIB; 0 turns it off) with an exponential countdown
per thread, records the stack, thread and span context, and weighs
each sample interval / (1 - exp(-size / interval)) bytes. Frees look a
pointer up in a 1 MiB table of per-bucket counts and only take the lock
on a hit. One session per process: the first capture to start owns it,
so the integrated server's joined side does not start another.
Captures write two more kinds, alloc and live (bytes, period 1), into
the same tables as the CPU samples, without the allocator's own frames
at the leaf end.
Measured (fast profile, Linux x86_64, 24 threads, machine at load ~50
from other builds):
- Off: alloc_count_bench 64 B alloc+free pairs, +0.4-0.6 ns a pair
(+9-11% of mimalloc's 5 ns on 1 thread; +1-14% on 8, noisy): one
relaxed load and branch on each side, as designed.
- On, 200 walking bots with a capture: the sampler is 1.2% of the
tick thread's CPU samples (1.24 and 0.77 ms a tick), about 70% of it
.eh_frame unwinding, at ~40-70 us a sample; 63k and 100k samples in
~2 min. MSPT avg on 89/148/224/97 vs off 108/69/217/65 ms: the
overloaded server (TPS 5-14) and the machine load swamp the A/B.
- Capture size: 209 and 231 KB on vs 171 and 171 KB off (+22-35%).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SampleSource gains LIVE (sampled heap bytes still allocated when the
capture stopped, at their allocation's time block), and Overhead gains
heap_samples and heap_sample_ns.
horizon-perf reads them: --select live works in query hot/tree/callers
and in the folded and pprof exports, in bytes (a total, not a rate per
tick); the summary prints a heap line (live and allocated MiB, the
functions holding the most) and the heap sampler's cost.
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]>
Interleave the address families of resolved sign-in hosts and bound the
connect time, so a broken IPv6 route no longer fails sign-in with
'Could not reach Microsoft'. Fixes#15013.
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]>
Fox.SeekShelterGoal.canUse no longer looks for shade when the fox stands in a
village (ServerLevel.isVillage: within one section of a village POI), so a
village fox keeps strolling in the sun as vanilla's does.
Fox.DefendTrustedTargetGoal is now registered at target priority 3: the fox
takes whoever last hurt a trusted entity as its target, plays FOX_AGGRO at its
feet, sets DEFENDING (the growl and the avoid/stroll guards were dead code) and
drops the flag through Fox.setTarget(null) when the goal stops.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[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]>
`/profiler stop` prints the saved file and never uploads. Where a captures part is
registered (the official build) the line carries [Open] and [Share] buttons that run
`/profiler open <id>` and `/profiler share <id>` (no id: the last capture saved);
a build without the part answers both "not available in this build". horizon-ctl
gains `profile open FILE` and `profile share FILE`. The profiler harnesses read the
new saved line, and `profile_client_e2e.py --official` checks the buttons and open_at.
Docs: saving, Open and Share are independent of telemetry; Share is the only upload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The benchmark report format reads its hardware through `horizon-system`, not
`horizon-telemetry`, so `horizon-bench` is an engine crate and the two `-> horizon-bench`
exceptions go. `horizon-perf-format` is engine, `horizon-perf` a tool, `horizon-perf-share`
Horizon. The launcher and `horizon-server` reach the report format and the capture files
through `horizon_platform::{bench, perf_format}`. `horizon-threads` joins the portable
crates, which `horizon-world` already used.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Saving a capture is local. A registered `Hooks::captures` offers the viewer page,
Open (serve the file once on 127.0.0.1 and open the viewer on it) and Share (the
upload). `horizon-perf-share`, a Horizon crate in `horizon-official`'s parts, holds
both, with the addresses in `horizon-addresses` and their env overrides; the upload
code moves there from `horizon-perf`, which gains `horizon-perf open`.
The saved line and the `profile stop` reply carry no address: `open_at` is present
only when a build has the hook. `horizon-client` keeps `horizon-perf` for tests only.
Co-Authored-By: Claude Sonnet 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.
Shulker reads AttachFace with Direction.LEGACY_ID_CODEC: a byte through
from3DDataValue (abs(data % 6)). The loader took the remainder of the
whole number, so an int 258 was face 0 where vanilla's byte 2 is face 2.
The shulker bullet's Dir already narrowed first.
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]>
Compares the cached equipment with a direct conversion of every equipped
slot over static ticks, after a count change and after a slot is cleared.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Held, offhand and armor stacks were converted to network stacks for every
guest on every tick. Each guest now keeps the converted stack of each
equipment slot together with the ItemStack it came from, and converts
again only when that stack differs by full equality. The /reload path
clears the cache, since conversions depend on the registries.
Co-Authored-By: Claude Haiku 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.
Wolf and Cat read CollarColor with DyeColor.LEGACY_ID_CODEC: a byte
through DyeColor.byId, whose out-of-range answer is the first colour
(OutOfBoundsStrategy.ZERO). The loader masked the id with & 15, so 17b
was orange and -1b black where vanilla's collar is white.
-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.
AreaEffectCloud extends Entity and never moves, so Entity.saveWithoutId
writes the fall_distance Entity.load read. entity_base_tag, which the
cloud's save uses, took OnGround from the loaded compound but wrote
fall_distance 0.0; it now takes that from the compound too.
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]>
Entity.saveWithoutId writes fall_distance from fallDistance and
Entity.load reads it back. AbstractArrow.tick never calls Entity.move,
so a trident keeps the loaded value until water resets it. The trident's
save wrote 0.0 and its load ignored the key; it now reads, keeps and
saves it as the arrow does, and water resets it in the ticks that reach
Entity.baseTick. Lava's halving is not modelled for a trident.
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]>
A server owner has to know which connection floods or lags, so net rows carry each player's name, client
brand and view distance. server_perf_e2e still checks that the world, seed, MOTD, mob and sign names and the
IP stay out, and now checks that net names both bots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window now hands its server connection's byte counters to the capture each frame, so a client capture's
network tab has data. A server-thread capture asked for while the previous one is still being written waits
for that file instead of refusing, which the window had already reported as started.
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).
A capture made with --gpu now writes each timed frame (gpu_frames = 24: its
start on the capture clock, first-to-last timestamp total, every pass and part)
in blocks about once a second, with the CPU ms of every drawn frame. GpuPasses
ms becomes the median over the whole capture instead of the last 15 frames.
Where the device stamps inside passes, every timed frame splits the world
passes into their draw groups (sky, solid terrain, occlusion boxes, entities,
translucent terrain, clouds, weather, hand...) and compute passes into their
dispatches, so GPU culling's parts show without detail mode; Metal keeps pass
boundaries, recorded as GpuDevice.pass_split.
query gpu gains per-pass and per-part samples, p50/p95/p99/max and share of the
GPU frame p50, plus a summary row with GPU and CPU frame percentiles and a
gpu/cpu/mixed bound verdict; query gpu_frames gives the series with slow frames
flagged. Older captures read with empty statistics.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Above 65535, below 1024, not a number and negative all read the invalid-port
reason; 1024, 25565 and 65535 keep the Port Number label, and an empty box
takes the suggestion.
WorldOptionsScreen.setPortError swaps the port box's tooltip from
PORT_INFO_TEXT to INVALID_PORT when tryParsePort rejects the value. Horizon
always showed Port Number, so a red box never said what was wrong.
SulfurCube.canFreeze answers `!hasBodyItem() && super`, so a cube carrying a
block in its body takes no frozen ticks from powder snow and no freeze damage,
while one without a body freezes like the cube mobs do. can_freeze answered
the tag and the skeleton's refusal and nothing else, so a loaded cube shook and
took the 1 a fully frozen mob takes.
The test puts a sulfur cube with a stone body at 0 and at 140 frozen ticks: it
stays at 0 with no damage and melts to 138, where the cube without a body gains
a tick (1) and takes the 1 at 140. Without the arm the loaded cube reads (1,
0.0) like the empty one.
Upload completed chat-started captures on a background worker and show a clickable capture URL. Preserve the local file and an owner-only receipt before pinning, so /profiler share can retry without uploading twice. Use an application user agent accepted by the deployed API. Keep control-socket and environment captures local.
Validated timed and manual in-game completion against the live API with byte-identical download, plus retry/ownership regression, command completion and Clippy. Test uploads removed after validation.
Fixessunrise-org/horizon#267
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Keep bounded raw unit timings during recording and expose exact slow-tail means through meta. Tick rates express work capacity capped at 20 TPS; old or incomplete captures omit lows. Related: sunrise-org/horizon#264.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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.
A folder where the official launcher wrote versions/26.3/26.3.json but not
26.3.jar (a version picked but never played, or a download that stopped)
said "no Minecraft 26.3 client jar", the same as a folder that never had
26.3. It now says the version file is there and to play 26.3 once in that
launcher. A wrong jar and a folder without the version file keep their
messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Benchmark A/B runs and --benchmark-set/--profile, the memory section, span
laps through streaming and the world update, the server report and the
/profiler command tree, ported from the original patches onto the files
upstream split out of game.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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
Conflicts kept both sides: the platform's pass timing feature moves into the
device search's new open_adapter, its Options fields and work-attribution
calls follow the code upstream moved out of game.rs, and the perf channel
joins upstream's list of Horizon channels.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
horizon-client-lib's unit tests spent 71% of their time in the 708 mobs::
tests and 7% in lan::prediction, and nearly all of that was each test
reading the whole prepared root again in a debug build: Assets (block
registry, tags), BlockFacts, MotionResources, MobData, MobTypes, Biomes,
Items and the collision registry, about 7 s a call under load. A test
calling the fixture 16 times paid 16 times (157 s).
The tests only read those tables and build their own World over them, so
mobs/world.rs keeps the vanilla root's bundle (TestResources) in a OnceLock
and run_test_world builds the per-test World on it as before. A mod root
in a test's scratch folder is still loaded fresh each call, since the same
path can hold different content between tests. test_fixtures::vanilla()
does the same for Assets, RenderAssets, collisions and MotionResources;
lan::prediction's steps_in uses it, and motion.rs and entities.rs can
follow.
Same tests, same cases, same assertions; nothing new is #[ignore]d.
cargo test -p horizon-client-lib --lib on an 8-core laptop at 8 threads:
388 s -> 99 s wall (three interleaved runs each), 3061 s -> 770 s summed.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
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]>
Mob::pushed_by_fluid, the predicate both push sites of
LivingEntity.updateFluidInteraction read: a drowned with its swimming flag set
answers false, the same drowned with the flag cleared answers true, the water
mobs that never take a push (a cod, a squid, a turtle, a nautilus) answer
false either way, and a zombie answers true. Without the drowned arm the
swimming one answers true too.
Drowned.isPushedByFluid answers `!isSwimming()`: a drowned swimming after its
target holds its line in a stream, while one walking the bottom is carried
along like any other mob. Horizon answered the plain LivingEntity rule for
every drowned, so a drowned chasing a player through flowing water was swept
off its path, and the current was applied even while its own move control was
driving it.
pushed_by_fluid now answers the drowned with its swimming flag, which
Drowned.updateSwimming keeps on the mob (`extra.flag`) and the clients read
from the shared flags, and the rest of the table stays as it was.
LivingEntity.aiStep's freezing, through freeze_tick, in a world with a powder
snow block at the mob's feet: a skeleton, a stray, a bogged, a parched and a
wither skeleton gain no frozen tick (0) where a zombie gains one (1), and one
of them put at 140 frozen ticks melts to 138 with no damage where the zombie
takes its 1. Without the skeleton arm the skeleton freezes like the zombie.
Skeleton.canFreeze answers false, so powder snow builds no frozen ticks on a
skeleton and never deals it the 1 a fully frozen mob takes, and a skeleton
already frozen melts out of it at two ticks a tick. The stray, the bogged, the
parched and the wither skeleton inherit the same false. Horizon only asked the
freeze_immune_entity_types tag — which lists the stray, the polar bear, the
snow golem and the wither — and the worn freeze_immune_wearables, so a plain
skeleton standing in powder snow froze like a zombie.
can_freeze now answers the five skeletons before the tag, which is where
vanilla puts the class override.
The summon and remote-spawn sites build their orb with it, so the new
fall_distance field doesn't grow commands.rs and remote.rs.
Co-Authored-By: Claude Opus 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]>
Entity.isInLava is false while firstTick holds, so Entity.baseTick halves
the fall only from the second tick. Review finding on #14440; the
`orb fall distance` group gains an orb 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]>
Part of #14440: an orb's Entity.fallDistance builds up as it falls and is
saved and loaded (and taken by /data merge). Entity.baseTick's fluid step
resets it in water and halves it in lava; Entity.move's long-move ray
resets it past a fall-resetting block or water, and checkFallDamage adds
the drop and, landing after a fall, posts HIT_GROUND. Adds the
`orb fall distance` command-exec group.
Co-Authored-By: Claude Opus 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]>
`LecternBlock.getStateForPlacement` sets HAS_BOOK from the item's
`block_entity_data.Book` when the placer may use game-master blocks, so a
lectern placed from a book-carrying item draws its book.
`LecternBlock.getAnalogOutputSignal` reads the state's HAS_BOOK before asking
`LecternBlockEntity.getRedstoneSignal`, so `/setblock ... lectern[has_book=true]`
with no book now reads 14 instead of 0, and `has_book=false` reads 0.
`LecternBlock.resetBookState` sets POWERED and HAS_BOOK in one write, then posts
`minecraft:block_change` (the player who put the book on, none when
`LecternBlockEntity.onBookItemRemove` takes it off) and updates below, so a
sculk sensor hears both.
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
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
SignBlock.useWithoutItem answers PASS when the player may not build and
the sign is not waxed and has no click action to run (SignBlock.java:139-146),
so ServerPlayerGameMode.useItemOn goes on to the item phase
(ServerPlayerGameMode.java:411-427) and ItemStack.useOn's canPlaceOn test
lets a `can_place_on` block place against the sign (ItemStack.java:358-362).
Horizon's sim's sign arm answered SUCCESS, which `use_hand` consumed before
the item was used. `sign_consumes_click` now asks the sign's block entity
the question vanilla asks it, and use_hand only ends the click when it says
a waxed sign, a click-command side or a player who may build takes 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
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]>
Keep Rust reader artifacts in the game repository, move the viewer source into sunrise-org/horizon-site, and add CLI uploads and remote capture reads. Update fork automation for the organization transfer.
Co-Authored-By: Codex gpt-6.1-sol <[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]>
S's gamma rounds up in half float, to light just above S that the curve holds at S. Whether that comes back to the same half float depends on the GPU's rounding: on an Apple M4 Pro (Metal) it lands one step lower, so the exact check at S failed there. Every stored value below S still comes back bit-identical.
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]>
The two redirect tests built a fresh wind charge, which this PR now (as
WindCharge.deflect) refuses for its first five ticks; their fixture clears
no_deflect_ticks, and a new test checks a fresh one keeps its course and owner.
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
The eight component branches (#14131-#14138) each edited the same spots and
three of them left an unclosed block in finish_consuming. Merged here:
- item_sync::stack_consumable builds the component a use reads once: the
stack's own when it has one, with Consumable.CODEC's defaults for the
fields it leaves out (1.6 s, eat, entity.generic.eat, particles), else the
item's. finish_consuming, the per-tick particles and sounds and the
instant-consume check use it.
- Consumable.canConsume's food check asks the stack's food component.
- A direct sound reads SoundEvent.DIRECT_CODEC's `sound_id`, and every
ItemUseAnimation name maps (trident, crossbow and spyglass were eat).
- The glider test's doc comment is back on its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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
main's 3b34446f84 added the same helper with the fuller ItemStack.isDamageableItem rule (an
explicit DAMAGE component on a non-damageable prototype); this branch keeps that one and its own
ItemDamageFunction.apply.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's health/equipment.rs (3cdf31772f) runs a hurt in stages and copies the block results
between them; it now copies blocking_item_used too, so a staged hit reports the shield use as
the one-shot hurt does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
main moved more of game.rs out (#13877, #13878, #13880...): the conflict is only the
neighbours of the moved functions; game.rs takes main and drops the four functions
this branch moves to game/render_settings.rs, unchanged.
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]>
WindCharge.deflect refuses while noDeflectTicks (5, one less after each tick,
not saved) lasts: a projectile hitting a young wind charge doesn't redirect it
(Projectile.onRedirectProjectile), and a player's blow goes on as an attack
that hurts nothing. A breeze's wind charge has no such wait.
Harness: tools/test_wind_charge_no_deflect_ticks_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs is over 3000 lines (docs/layers.md#file-size). The arrow's hit on the player goes
through shield.rs's arrow_hits_player, which sets combat.piercing_hit; hurt_player_from takes it
as it takes rocket_hit and hands it to player_blocking. Same behaviour as hurt_player_with_pierce.
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 verify job compared big files against this branch's old base, so main's own growth of options_ui.rs since then failed the size check.
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]>
- A Silent:1b warden played its emerge, dig, roar, sniff, tendril, attack and sonic boom sounds:
both warden sound helpers now check `silent`, as Entity.playSound does (#8835).
- ForceUnmount was stubbed out: a warden put on a vehicle with /ride or a summon's Passengers
never got off, so it could never dig. It now runs vanilla's unRide (passengers ejected, the warden
set down where its vehicle lets it off) and stops in the tick it starts (#8833).
- WitherBoss.customServerAiStep counts the side heads' idle updates only on Normal and Hard: on Easy
a wither fired its random blue skulls all the same (#8840).
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]>
scrub.rs moves unchanged from horizon-telemetry to horizon-system (engine);
horizon-telemetry re-exports it at horizon_telemetry::scrub for the launcher.
The repro export (agent control) reads it there, so gameplay no longer uses
horizon-telemetry for it.
Co-Authored-By: Claude Opus 5.5 <[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]>
rss_bytes, cpu_seconds and load_average move to horizon-system (engine);
horizon-bench re-exports them for the launcher. horizon-system's cpu() and
ram_bytes() become public and replace horizon-bench's identical copies.
The dedicated server's performance report and cem_expr_bench read them
there, so gameplay no longer uses horizon-bench for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #11857. new_renderer, adopt_renderer, forget_atlas_slots and all_sections_changed join game/render_settings.rs. Methods that were private in game.rs become
pub(super), the same reach.
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]>
ClientPacketListener.handleSetCamera takes any entity the level holds, but
remote_set_camera only looked among the mobs, so a camera on an armor stand
(the usual target of a server's cutscene or menu scene) was dropped: the
view stayed on the player, who kept moving and turning. The stand now holds
the camera like a mob does: the view looks out of its eyes along its yaw,
the player sends no moves, the crosshair skips the stand and first person
does not draw it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.tickThunder picks its strike column via Level.getBlockRandomPos(minX, 0, minZ, 15), which advances Level.randValue (Ticks::block_random_pos) and extracts bits 2..=5 for X and 10..=13 for Z instead of drawing from Level.random.
SkeletonTrapGoal.tick draws each additional trap horse's horizontal push (two triangle(0.0, 1.1485) draws) from the original trap horse's own random after creating and mounting its skeleton rider, rather than drawing from Level.random before rider creation.
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]>
Roll pending loot before automation accesses chest-boat slots or destroying
removals spill them. Keep occupied slots, synchronize open-menu copies, and
award container loot only for the player opening a pending table.
Co-Authored-By: Codex (GPT-6) <[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]>
`entity_nbt.rs` is over the 3000-line limit and may not grow (docs/layers.md
#file-size), so the goat's save pair moves into `mobs/goat.rs`: the screaming
and horn fields `Goat.addAdditionalSaveData` writes plus the long jump
cooldown memory, and their read side. `entity_nbt` keeps a one-line call.
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]>
`Goat.addAdditionalSaveData`'s row in `tools/port_map/game-012.tsv` names the
three own fields; it now also names the `GoatAi` `LONG_JUMP_COOLDOWN_TICKS`
brain memory that the save covers.
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A goat with 57 ticks of long-jump cooldown saves
`Brain.memories."minecraft:long_jump_cooling_down"` as `{value: 57}` and the
reloaded goat's `frog::jump_cooldown` is 57 again. A goat with no cooldown
writes no memory, even over a compound that still names one.
Without the write the save has no memory (left `None`, right `Some(Compound({"value": Int(57)}))`), and the
reloaded goat has no cooldown (left `None`, right `Some(57)`).
`GoatAi` sets `LONG_JUMP_COOLDOWN_TICKS` when a goat long jumps, and
`CountDownCooldownTicks` counts it down; `Brain.Packed` saves every brain
memory with `LivingEntity.addAdditionalSaveData` and reads it back.
Horizon kept the cooldown in the goat's `extra.memories` (the shared
`frog::jump_cooldown` the long jump goal asks) and wrote no `Brain`, so a goat
that had just long jumped unloaded with its chunk and came back ready to jump
again. The save now writes `minecraft:long_jump_cooling_down` as the integer
memory it is and the load puts it back in the goat's memories.
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]>
The design note and graphics.md said a display at 1000 nits or more shows
the scene as written. That holds at a paper white of 203; in general the
curve is the identity where Peak Brightness is at least 1000/203 times
Paper White, so say that.
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]>
Paper White now runs from 10 to 480 nits and Peak up to 10 000 (#12881): the tone curve
test checks that the world shows exactly as written wherever the display's peak reaches
the scene's, at 480/10 000 and 10/200 nits too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the float frame the world's light no longer depends on the display. Light sources
multiply their brightest pixels by fixed gains in multiples of paper white (1.6, 2.4,
2.8 and 4 from faint glowers to the sun), and HDR lighting's strongest light by 3. The
tone curve alone fits that scene to the display: light above the scene's peak (1000/203
of paper white, a 1000-nit display at BT.2408's 203-nit white) is held at it, keeping
its hue, then BT.2390's EETF maps the scene's peak onto the display's.
For players: on a 1000-nit display the world shows exactly as written; a 1500-nit or
brighter one shows the same picture, the sun included, instead of a brighter sun and
lava; a dimmer one keeps its midtones and rolls the highlights off below its peak.
Before, the gains scaled with the display's headroom and the curve then compressed
that light again from a peak it derived from the same headroom, so the look changed
with the Peak setting.
Light Horizon doesn't lift (HDR Lighting off, the title, a 0.2 pack's own) has no
known peak, so there the curve keeps the source that follows the display, as the 0.2
contract describes. HDR off, HDR Lighting off, the HUD, the black lift, the SDR
screenshot's shoulder and the 16-bit or 8-bit compatibility frame (HDR's default until
step 5, which changes the look once) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Summary tiles read the wasm's own series stats (new optional `stats` on series rows, asked for with filters.stats); the old browser-side statistics disagreed with the CLI summary (MSPT 8.9 against 10.7, CPU 8.47 against 6.6).
- Queries no longer carry the real file name, so the Query tab never shows it.
- Slow ticks: every kept tick opens into its full span tree; the meaningless min/median/p95 of the slowest 16, the histogram and the sparkline are gone. The summary no longer repeats the worst ticks and lists only the top steps of the tick.
- Metadata: tabs that only repeated Memory, Network and World are gone, rows without data are left out, empty tabs are hidden, enum values read as words, and the mod crate column reads the `crate` field the wasm sends.
- Units on every number, frame figures say frames, warnings are worded from their codes, compare rows have readable names and drop an all-"not enough data" verdict column, network packet columns hide when nothing was counted, TPS windows longer than the capture are not shown.
- Keyboard focus is no longer hidden under the sticky tab bar, and rows that open show a chevron.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The diff gains opt-in function and call-path rows (--calls, calls in hp_diff), matched by stable id. One comparison state feeds Summary, Spans, Functions and the Call tree; rows only the second capture has are listed apart.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Spans and functions the profiler meets most (tick_blocks, tick_lan, streaming,
tick_mobs, lan_chunks, random_ticks, allocator and memory calls...) get a short
explanation keyed by stable id in the translation file, shown as an accessible
hover and focus tooltip. Every row of the Summary, Spans, Functions, Call tree
and Slow ticks views copies its stable id from a control that appears on row
hover or keyboard focus, so rows stay clean at rest.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A List / Flame graph toggle in the Call tree tab draws the current side, thread,
selection, grouping, merge mode and focus as a canvas flame graph: widths are time,
colours are the tree's crate groups, hover shows name, time and share, a click zooms
in with a trail back, and arrow keys, Enter and Escape do the same from the keyboard.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The tree rows carry their crate, and a request can ask for each row's own time as a percent.
The tree tab is split into focused components; chains of single-child functions share a line.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The no-capture screen is now a landing page: what the viewer reads and how to get a capture. A tab appears only when its query answers with rows for the side on show, so GPU is gone from a server and empty tabs are gone everywhere; Metadata's sub-tabs follow the same rule.
Health, and Summary's network and dimension cards, repeated other views and are gone. So are the file name and size, the report box, New profile, the sort direction and row limit, the placeholder flame graph, and id chips.
Fixes: slow ticks say they cover only the slowest kept, in time order; query errors show their real message; Compare is a table of real rows for the side on show; table headers line up with their columns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Removes sharing entirely: the Share button, share links, the upload API, its S3 storage and the CLI's `share` and `upload`. A capture stays on the machine.
Fixes:
- ureq and the snapshot files are gone, so the wasm builds and the tree is 7,600 lines lighter
- merge mode hides wrappers and merges a function across its callers
- `group --by mod` backs the mods view, charging a sample to its innermost mod frame
- a failed compare shows its error, and the compared capture is closed when replaced
- Hot shows milliseconds rather than calling them percent; Tree bars share one scale
- tick details line up; wasm replies are typed structs; launcher lock is current
Polish follows docs/design-system.md: Geist, list rows with aligned number columns, one accent, one sticky tab row, a real opening screen, focus states and reduced motion. Panels share one query hook, and dead icons, strings and exports are removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fan out implementation of the remaining perf-platform web UI pieces and upload API:
health/metadata, call tree/group-by/select/detail/search, flat/mods/merge/info,
flame graph and ticks tab, compare/GPU/export/copy/share/open links, and
upload route handlers, S3 storage, validation, share pin, sweep, rate limit.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Upstream moved Game::shut_down and idle to game/window_handler.rs and loading_frame and start_game to game/loading_window.rs; finish_profile and the benchmark set and profile fields move with them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The slow ring no longer shows up as span lists in the log, but whose packets or events took a tick
stay in the server owner's log as a separate line. pacing_bench keeps only frames over the threshold
recorded during the capture.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The slow ring and the captures keep the steps of slow ticks and frames; the notes channel that only fed the log line (event timings, packet notes, spawn counters) goes, and the spawn counters move to the counter registry.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server sends its finished file in 32 KiB chunks over horizon:perf, at most 256 KiB/s and not while the guest has a backlog. The client merges it field by field into its own stream (PartMerger, no decode), or saves it unchanged for profile server alone; a cut transfer ends the file with SERVER_PART_INCOMPLETE.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A client asks a Horizon server (LAN host, dedicated server) to capture its side: operator level 4, one capture at a time, the server logs who profiled and keeps its copy in reports/. profile server start|stop work on a joined server; a bare profile also asks and a refusal is a warning in the client file.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A server-only mark replied capture-relative and a joined one on the game clock; both now use the game clock.
The e2e checks the server mark against the client mark's game-clock t.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The integrated server thread writes its side (kind INTEGRATED) into the window's capture file, on the same clock, and a bare profile means both. profile server writes the server thread's own file.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds BenchResult (field 21) and Attachment (field 22) to the capture contract. horizon-perf reads bare benchmarks/*.json as one-sided captures, answers query bench and prints bench lines in the summary, and attach appends an attachment on the wire bytes so unknown fields survive. The game's --benchmark --profile records a capture and embeds the report through profile stop --bench; server_bench.py --profile records Horizon's measure window with horizon-report and attaches the run record.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Chrome-trace writer in spans.rs is replaced by per-thread buffers drained once a second into
TimelineChunk (field 15), capped at 60 s or 256 MiB with timeline_stopped_t and a warning.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds the gpu field (GpuPasses per timer) to the contract, written from the
renderer's PassTiming readings at stop; profile start --gpu[=detail], the
HORIZON_SHADER_PACK_TIMING and HORIZON_GPU_PASS_DETAIL aliases, horizon-perf
query gpu with fragments_per_pixel, a summary line and a meta count.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
One-line docstring and debug-key comment, F3+O feedback decided by who pressed stop, hardware described off the render thread, and the gauge probe spawned on the first frame with this second's counts.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The window's frames feed an always-on client health history and slow ring.
profile start|stop|status|mark (and F3+O, HORIZON_PROFILE=DIR) write a .hzperf
into reports/ in the game home, with the render thread's samples, retention and
the translated saved message. The capture session is shared with the dedicated
server's through a Role; the gauge probes run off the frame thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A Recording now turns spans and job stats off when it is dropped, so a capture that failed
to start or a shutdown no longer leaves them on. A leaver's unread bytes are lost (noted on
TrafficRecorder). The e2e script's docstring is one line and its build command is in
docs/perf-platform.md, which also says the privacy checks' split and the dropped between-tick spans.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The dedicated server's capture now carries span_blocks (5 s blocks with parents), the spans
summary (totals, histogram, 16 worst ticks with their trees), workers, per-player net bytes by
join order and the world at start and stop with sampled mob costs. perf-*.json and .txt are no
longer written. capture.proto gains the game_rules and horizon_options allowlists and the mob
timing overhead fields. Spans recorded between ticks no longer count in a tick.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Reads the samples section: functions merged by normalized name, ids over the
normalized name and call path, filters (thread, select, slow, where, window),
several captures for frames, meta counts samples, summary gains hot functions
and the entity/block/chunk splits.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The samples field (SampleProfile) is built from horizon_threads::Profile in
horizon-client-render: a deduplicated function table, a call-node chain per
stack, a context table and weights per thread, state, slow flag and time block.
The sampler's aggregate gains time blocks. horizon-report samples the tick
thread on Linux unless --no-samples, and finishes the file off the tick thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The always-on server health history and slow ring feed a capture file
(Start, history, slow_history, health, counters, slow, Stop) in reports/,
with retention of 30 files or 1 GiB that never touches renamed files.
horizon-report accepts --no-samples and --profile; horizon-server report
prints the saved path and the web UI address.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Replies are serde structs (one Envelope<R>, a row struct per op, typed meta, watch events, diff and bench diff) and
closed vocabularies are enums (Op, Select, ContextKey, GroupBy, Unit, Verdict, HealthField, Flag, the proto name enums).
The CLI parses strings into them at the edge; no serde_json::Value, json! or string-keyed match is left in the library.
The JSON is the same as before as parsed JSON (tests/golden holds 49 cases captured from the old code). Intentional
differences, each visible in the golden diff:
- --where dimension=X is now the key dim, so dim and dimension merge in selection.where
- follow --json spells a stop reason window_cap (it said "window cap"), like query meta
- an unknown --by is a usage error at the edge instead of a world-only query error
- JSON object keys now come out in declaration order, not alphabetical
horizon-threads gets a feature alloc-count (alloc-census implies it) with
CountingAllocator: each thread owns a cache-line slot of four counters, so a
block costs two plain load/store bumps. GameAllocator picks the census, the
counting mimalloc or plain mimalloc, and main.rs and the scene_replay example
declare it; without the feature the allocator is unchanged. The census counts
the same way.
In a counting build stats, bench and fly reply with alloc (window-thread
allocations and KiB per frame, all-thread rates, net growth), and stats raw and
fly raw add alloc_count and alloc_bytes beside frame_ms. Other builds reply
null. docs/memory.md gets the section with the measured cost.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Describes only what exists: horizon-ctl memory and what each number means, the
alloc-census build (how tags are chosen, what it costs: +1.8% resident, frame
CPU time within the run-to-run spread, measured with --benchmark standard),
spans/trace/stats, resident memory in benchmark reports, scene_replay --memory
and the system-allocator feature for heaptrack on small probes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
symbols.rs resolves addresses against this process's own binaries (DWARF through addr2line, the ELF symbol table for full paths), keeping base file names only. tree.rs names the aggregate's trie, merges it into a function tree and ranks functions by own and total samples. The pprof, folded and HTML writers stay out; the capture's samples field replaces them. Also fixes the spans test's work:: path and a clippy as_chunks lint.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`profile::start` runs a writer thread that drains the sampler's rings every 100 ms, unwinds the stack copies there and folds each record
into a call-stack trie (131k nodes at most) plus counters per stack and context (span, entity type, chunk, block, dimension, slow tick).
A tick marker decides afterwards whether the samples of that tick count as slow, so the report can keep only the ticks over a threshold.
Tick durations go to a fixed log-bucket histogram per thread with the ten worst ticks. `Session::status` reads progress while it runs.
`Config::wall` arms a second timer per thread on the monotonic clock, signalling SIGRTMIN so it queues instead of merging with the
CPU-time SIGPROF; each handler blocks the other, so they never nest on one ring. A wall sample is marked running when the thread's CPU
clock advanced by more than half the period since its previous wall sample (the handlers' own microseconds don't count).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The innermost span (spans::guard), the entity type before each mob, item,
cart, projectile and block entity tick, the chunk and block of random and
scheduled block ticks, the chunk in natural spawning, the level's dimension,
and a tick marker with its duration at the end of each server tick and each
client frame. Plain stores into the sampler's thread-local; the new
horizon_threads::profile module makes them no-ops where the sampler does not
run and interns the names behind the ids.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Frame pointers cost 29-34% world load in release builds, so the release build keeps them off and the SIGPROF handler now copies
the interrupted registers and the top 16 KiB of the thread's stack (clamped to its bounds) into a byte ring, memcpy only.
`Session::drain`, on the caller's thread, unwinds each copy with the loaded objects' `.eh_frame` tables. The frame-pointer walk
stays in the handler as the fast path when this build keeps frame pointers (`Unwind::Auto`, decided at start from a probe).
Ring entries are variable length; a ring holds two seconds of samples by default, and a full one drops and counts as before.
New dependencies (Linux only): `framehop` 0.16 (MIT/Apache-2.0, samply's unwinder; default features off, so no Mach-O/PE
parsers) unwinds from `.eh_frame`, and `object` 0.39 locates the sections in each loaded ELF. There is no in-tree unwinder, and
writing a CFI interpreter is the alternative. Both share one `gimli` 0.33, which was not in the lock before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`unwind` walks frame pointers within a thread's stack bounds (shared with the allocation profiler). `sampler` arms a per-thread
CPU-time POSIX timer that signals SIGPROF to its own thread; the async-signal-safe handler copies the thread's context and
stack into a preallocated ring, and `tick_end` writes tick-duration markers so a report can keep only slow ticks.
Nothing is installed or armed until `sampler::start`. Frame pointers stay off in `.cargo/config.toml`: the benchmark
shows world load +29% (7 of 8 pairs) with `-C force-frame-pointers=yes`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
step moves above tables' doc comment. The docs now say GPU culling uploads is a pass entry of its
own, only in detail frames on devices with encoder timestamps, not a part of GPU culling. A new
GPU test times both phases and the uploads and checks detail leaves the counts and draws as an
untimed frame's; the test device now asks for SHADER_EARLY_DEPTH_TEST so phase 2 can run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`horizon-ctl gpu-passes detail` turns on dispatch times and, on Vulkan, per-pass counters;
stats and bench add `gpu_pass_detail`, `gpu_pass_parts` and `gpu_counters`, F3 lists the parts
and the six passes with the most fragments, and `scene_replay --gpu-detail on` reports both per
view after the timed frames. RADV counts fragments per 2x2 quad, so a calibration draw at setup
finds each device's `fragment_unit` and `fragments_per_pixel` is computed from it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`gpu-passes detail` frames now alternate between a parts frame (times) and a counters frame
(vertex, clipper and fragment shader invocations per world pass, split into segments such as
before translucent, occlusion boxes and translucent terrain). A counters frame throws its times
away, and CountedPass ends its query on drop, so none stays open at the end of a pass. Vulkan
only; a device without pipeline statistics keeps parts frames.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`gpu-passes detail` (the renderer's `set_pass_detail`, or HORIZON_GPU_PASS_DETAIL) times each
dispatch of GPU culling (apply, cull, scan, scatter; count2, scan2, scatter2) and its uploads
plus clear_buffer, so a slow pass can be split. A timestamp pair brackets each part inside one
compute pass where the device has TIMESTAMP_QUERY_INSIDE_PASSES, else each part gets a pass of
its own. Off by default; `on` records the same queries and passes as before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
land_frame told the GPU timer and a shader pack's pass timer that their frame
was submitted, but not Horizon's own pass timer: with the submit thread on, its
first timed frame was read and every later one waited forever, so gpu-passes,
F3 and scene_replay showed the first frame's pass times only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client --benchmark travel flies 512 blocks at 16 blocks/s with the time
and weather frozen, then holds at the end for four blocks: the renderer after
travel, where GPU culling's slowdown shows. Scenario::parse replaces scenes();
--benchmark-set SWITCH=VALUE sets one of the SWITCHES (defaults as gfx has them)
before the world opens, and a refused switch ends the run with the game's
reason. The bench control word takes raw like stats and fly, and the report gets
gpu.<phase>.p50_ms and cpu.<phase>.p50_ms for the trip and hold phases.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
horizon_bench::compare pairs the ab.a/ab.b blocks (B A A B order) and gives the
Hodges-Lehmann effect, the exact Wilcoxon signed-rank interval, a verdict, the
blocks' noise and the first-block flag. tools/bench_stats.py computes the same
numbers in Python and checks a report's stored result against its frames.
Report gains an optional ab field; the format stays version 1.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The new crate holds piece F1 of docs/perf-platform.md: capture.proto (Capture's
top level with Start, Stop, strings, marks, warnings, health, history, the slow
ring and counters), compiled by protox + prost-build in build.rs, and the gzip
writer (flate2, rust_backend) that appends whole top-level fields with a sync
flush after each, on a horizon-perf-writer thread fed by a 4,096-field bounded
channel that drops and counts instead of waiting.
Tests: the frozen tests/fixtures/v1.hzperf decodes to its listed values; after
every flush the file on disk inflates to exactly the fields written; a file cut
at random points yields only whole fields, in order; unknown field numbers are
skipped; the descriptor lint passes capture.proto and rejects a missing
_UNSPECIFIED, a JSON-name collision and a group; a full channel drops, counts
and never waits, and Stop records the drops.
Fixes#2
Co-Authored-By: Claude Opus 5.5 <[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]>
MinecartTNT reads explosion_power and explosion_speed_factor with
getFloatOr (4 and 1), clamped to 0..128, writes each only off its
default, and explode() blows with explosionPowerBase +
explosionSpeedFactor * nextDouble() * 1.5 * min(speed, 5). Horizon blew
every cart at 4 + 1.5 * speed * r and kept whatever the loaded compound
had for the fields, unclamped.
Command harness group "tnt minecart explosion power": 13/18 -> 18/18.
Fixes#11167
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:28:56 +03:00
996 changed files with 100335 additions and 9287 deletions
| `horizon-anticheat` | The dedicated server's anticheat, off by default: `[anticheat]` settings, violation levels, the checks' rules; the `hub` feature runs them on the engine's hook (`lan/watch.rs`) ([docs/anticheat.md](docs/anticheat.md)) |
| `horizon-anticheat` | The dedicated server's anticheat, off by default: `[anticheat]` settings, violation levels, the checks' rules; the hub runs them (`lan/anticheat.rs`) ([docs/anticheat.md](docs/anticheat.md)) |
| `horizon-cosmetics` | Cosmetic items Horizon clients draw on players (off by default): the item format, bones and animations ([docs/cosmetics.md](docs/cosmetics.md)) |
| `horizon-addresses` | Every built-in address of an outside service vanilla doesn't contact (Horizon layer; [docs/layers.md](docs/layers.md#built-in-addresses)) |
| `horizon-friends`, `horizon-relay` | Play with Friends: invite codes, encrypted tunnel, the relay ([docs/play-with-friends.md](docs/play-with-friends.md)) |
@@ -109,6 +102,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
| `horizon-perf-format` | The `.hzperf` capture contract ([docs/perf-platform.md](docs/perf-platform.md) §3): `proto/horizon/perf/v1/capture.proto`, its generated types, the gzip writer and its thread. No Horizon dependencies; nothing in the game reads a capture |
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
| `horizon-system` | The machine the game runs on (OS, CPU, memory, drive, power, GPU maker and memory), for the crash report, the renderer's GPU info and telemetry |
| `horizon-platform` | The platform's public API: what the Horizon layer may use of the engine ([docs/layers.md](docs/layers.md)); re-exports `horizon-system` today |
@@ -135,8 +129,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 anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
[features]
# The checks on the game's server hub (`hub`): the engine's hook (`lan::watch`) filled. Without it,
# only the settings and the rules, which horizon-server reads.
hub=[
"dep:horizon-client-lib",
"dep:horizon-core",
"dep:horizon-protocol",
"dep:horizon-world",
"dep:chrono",
"dep:serde_json",
]
[dependencies]
serde={version="1",features=["derive"]}
toml="1.1"
# The `hub` feature. The engine's own features stay the game's (`default-features = false`).
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 996 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.