A host with a fixed startup command (a game panel running `./horizon-server run`) can't set
HORIZON_PARALLEL_TICK. `[engine] parallel_tick = true` makes horizon-server start the game with the
variable set to 1, the way `low_memory` sets MIMALLOC_ALLOW_THP. Off leaves the variable alone, so one
already in the environment still applies.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A fox that finishes chorus fruit teleports within half the item's
diameter on each axis (item data) and then falls to the first ground
below, so no margin bounds its destination. Islands::plan now returns
None, in production and verify, while any mob holds such an item in
either hand or is using one; it checks the item, not the species.
Adds a destination-herd regression and a plan() case, and documents the
fallback and the audit of other self-moves in docs/mob-islands.md.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Goat.finalizeSpawn, GoatAi.initMemories and Fox.populateDefaultEquipmentSlots
said that Ctx::finalize and Ctx::populate call goat_spawn and
fox::populate_default_equipment, and that goat_spawn calls goat_init_memories.
They do not: Ctx::finalize and Ctx::populate have their own copies of the same
draws, and goat_spawn draws the two cooldowns itself.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- th_th.json (game and lang_review copy): drop the HDR peak hint en_us
no longer has, as b36e25ab16 did for Ukrainian;
bundled_files_are_served_and_have_no_stale_or_repeated_keys failed on it.
- booleans_are_read_as_bytes: the sign side in the has_glowing_text case had
no messages, so it now loads empty (4787069c5b); give it four lines.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Update stale test call sites and the player RNG field to the current APIs without changing seeds, draws or assertions.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: timing spans around every untimed call of a level's pass (about 200 ms of the SMP's 250-330 ms overworld pass had none, #15349); spans read nothing while off, and no behaviour changes.
🤖 Agent: the watchdog now reads the stuck server thread's stack (a SIGURG handler installed only when a tick hangs, 5 s wait, else the report says why), so the next SMP freeze (#15349) names the function it was in; nothing changes until a tick hangs.
tests/test_command_exec_group_cleanup.py fails on main since #13823: the new 'sulfur cube size attributes' group kills its cubes after its last tick without shrinking them, so a size 2 cube would split inside the next group. Same shrink as the other sulfur cube groups; both commands are setup commands, so no record changes.
en_us dropped "Your display's reported peak is often too high: calibrate to set it up." in 1c93067327; #14706 added its Ukrainian text back today, so lang::tests::bundled_files_are_served_and_have_no_stale_or_repeated_keys fails on main and on every pull request tested against it.
The whole-word match turned GPLv3, LGPLv3, AGPLv3 and MPLv2 into custom: their token is
gplv3, not gpl. A token now matches a family when it is the family's name followed by
nothing, or by an optional v and digits. Tests for those spellings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Address the maintainer request on PR #14373 by relocating the loaded and spawnable block-position helpers into a sibling module. Keep both implementations and their game-module visibility unchanged.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Polygon, ModelNormals and Part move byte for byte; only visibility changes
(the private members the sibling modules reach become pub(super)), and mobs.rs
declares the module and re-exports Part and Polygon. A port map row follows the
move. mobs.rs goes from 4931 to 4720 lines without test modules, so a change
to Seen has room.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
naga's GLSL frontend did not know the GL_KHR_shader_subgroup_quad swaps, so any program using
them (FidelityFX SPD in several packs) failed with Unknown function. They map to naga's QuadSwap gather.
naga rejected gl_SubgroupID and gl_SubgroupInvocationID when the workgroup had more than one
dimension, a WGSL rule GLSL does not have, so pack compute passes with a 2-D workgroup were skipped.
A pack's core shader could read only vanilla's blocks and samplers. A
pipeline that gives it more (the improved-transparency stages' depth bounds
and coefficients for core/text, Polytone's blocks) translates it with
translate_core_with and the names it provides; CoreProgram::other lists
them in binding order, at other_binding(i). A uniform block declared on one
line is bound like one over several lines, where it came out unbound and
the shader was refused. An unprovided one is named as the pack declared it.
Part of #11486
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moving the data folder was refused when any folder above a data root was a symbolic link (a
~/.local/share on another disk, Fedora Atomic's /home). Only the roots are copied, so only the
root itself may not be a link.
Split out of #15247 at review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Vanilla 26.3 draws text displays, name tags and their backgrounds with
core/text (TEXT_POLYGON_OFFSET, and TEXT_SEE_THROUGH with
IS_SEE_THROUGH), so a pack's text shader reaches them, not only the
GUI's. The Globals and fog blocks the pack shaders read are made once a
frame for core/rendertype_lines, core/sky and core/text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pastes an #include every time, as shaderc does (vanilla's includes carry
guards), and a #moj_import once, as 26.2's GlslPreprocessor did; a
shader may paste at most 1024 files and grow to 1 MiB, so includes that
multiply are refused instead of exhausting memory. Keeps a 26.3
shader's own varying locations on both stages, even where another
branch gives the same name another one, so branch-local varyings no
longer push a program past OpenGL's 16. Takes NAME VALUE defines, as
RenderPipeline.Builder.withShaderDefine(name, int).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The flat world lies below y 40 and seed 12345 has slime chunks within
the view distance, so either server may spawn a slime
(Slime.checkSlimeSpawnRules) during the minute the steps take. On
Horizon one did: it pushed the boat off the bobber's line before the
cast and killed Alice, so the boat was never hooked. Fishing hooks do
not depend on the difficulty; peaceful keeps every monster out.
Fixes#15323
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The table said 47 exceptions at d5504cd328 and still listed edges that
are gone (the horizon-client-input row; -theme, -bench, -telemetry and
-mod-api-gen for the render crate). Rebuild it from the 30 entries in
'allowed' at a89f338 and point at tools/check_layers.py for the live
count.
Closes#15408
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official SMP's slow-tick lines spend ~200 ms of a 300 ms overworld
pass outside every named span. Spans now wrap each untimed call of the
level loop (enter_level, the mods' level hooks, streaming, update_world),
of tick/tick_world and of update_world, and level_tick wraps the world
tick, so the line names where that time goes.
Refs #15349
Co-Authored-By: Claude Opus 5.5 <[email protected]>
new_rocket now takes the rocket's own random (upstream); the rocket's splash and move helpers stay before it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerWatchdog's report only had the last 50 log lines, so a tick stuck
for 60 s on the official server (#15349) never said where. The watchdog now
interrupts the server thread with a signal (Linux, Android, Apple) whose
handler captures that thread's backtrace, and the report carries it as its
stack, as vanilla's report carries the server thread's stack. A thread that
does not answer in 5 s, or a system without the signal, is named in the
report's details instead.
Refs #15349
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep both the trapdoor fall_location_of and its test module and main's
mob-hit impl block in game/combat.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
JukeboxBlock.setPlacedBy sets has_record with level.setBlock alone;
only JukeboxBlockEntity.notifyItemChangedInJukebox posts the
block_change game event. Since #14900 set_has_record posted it for the
placement too, and use_item_on failed 79 cases on main.
Fixes#15320
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]>
Move list state, foreground scheduling and the single background updater
into a private list_updates module. Retain the public social paths and
move the original state-mapping test without changing its assertions.
This is R1 after merged #15182, reapplied independently to main at
b66b2f8372. Preserve shared state, lock scopes,
request order and existing cache, failure and timing behavior.
Refs #15183
Co-Authored-By: ZCode <[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.
AGENTS.md gets a short "Don't lose time" section from measured agent sessions and CI: check for duplicates first, find code with the map, cargo check while working, no polling loops, one test per changed behaviour, don't hunt unrelated red checks, finish when a rule is unclear. CONTRIBUTING asks for a harness only where vanilla has something to compare.
docs/cleanroom, docs/native-hooks-neoforge and docs/how-horizon-is-built linked to the removed section and repeated its rules. The history of code removed for its license stays in how-horizon-is-built.
FullTextSearchTree.searchPlainText searches the plain-text tree (the
results' tooltip lines) alone; the id path is only searched for a
namespace:path query. #14910 also matched the item id path, so
recipe_book_view failed on main ("light_blue_candle" found recipes).
Fixes#15321
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CONTRIBUTING now says Horizon works in two stages: first it plays exactly like vanilla, then it improves on it. Performance work keeps compatibility with vanilla clients, servers and saves and the gameplay players rely on, but need not copy vanilla's internal order, random calls or slow paths. Pull requests say what they no longer reproduce exactly and how it was checked.
Allow the supported pinned Ubuntu 24.04 fallback in the runner-policy check while preserving the shared pool-routing and privilege guards.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Refs #15336. Pin the GitHub-hosted verify fallback for a controlled CI run; preserve the existing self-hosted routing and checks.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Report missing inline-feature fields against the complete flat feature map, matching the vanilla decoder.
Validation: 84/84 strict vanilla records, the feature-codec unit test, and all 5 Pong compatibility tests passed locally; required verify passed on f55a82aa12.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Keep the gate change unchanged and include main #15250, which carries the validated Guest.record test correction.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Match the already validated fix in #14219, #14088, #15250 and #15291: Guest.record holds the prediction record directly after main #14005.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
The requested bulk merge includes #15250 and #15291. Keep interaction_data and inline_feature_parse selected so those fixes retain automatic coverage.
Co-Authored-By: Codex GPT-6.1 Sol <[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]>
Resolve the port-ledger conflict by retaining the explicit social_screen.rs::Entry description. The other path corrections are already on main.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
use_item_on (#15320), recipe_book_view (#15321), block_entity_render
(#15322) and fishing_hook_entities (#15323) passed at main's last green
parity-main (fdc87aecd, 2026-11-07) and fail now whatever a pull request
changes. Each has an issue naming the merge, or the range, that broke it.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
An issue was opened only for a harness that passed in the previous audit,
so one merged while its pull request's parity job failed or was skipped
never got one: nine such harnesses failed on main unreported (#15158).
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Seven runs between 2026-11-09 and 11-11 searched up to 1,500 merges until
the job's 240 minutes ended, named no merge, and the next run started again
from nothing. The search now has --budget minutes (90), then says between
which merges each harness broke. What it learnt is kept in
.local/parity-runner/bisect.json while the base is the same, so the next
run narrows it further, and a merge already named gets no second comment.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
They merged while their pull request's parity job failed or was skipped and
fail on main whatever a change touches (#15158), so every pull request that
selects them fails and parity-main cannot pass. They still run when named and
in the nightly audit, and come off the list with their fix.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[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]>
Canonicalize root entity attribute-list order while preserving all values, styles and other record fields.
Validation: 9 focused comparison tests and 105/105 strict vanilla parity records pass; current-head required verify is green.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Mob::seen wrote a villager's reputation of the player into every Seen it
built, and no code reads the field (git grep finds only its definition and
that write; the trading code reads the gossip itself). Remove the field and
the lookup. The `player` parameter of Mob::seen is now unused and is named
_player: it has many callers, and a later change can drop it from them.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
CombatTracker.getMessageForAssistedFall and DamageSource.getLocalizedDeathMessage
are ported with their .item forms; the rows now say what the code does.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Piglin.holdInOffHand, PiglinAi.holdInOffhand and Drowned.rangedAttackUncertainty
are ported as vanilla's bytecode reads; 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
In 26.3 Feature.DIRECT_CODEC dispatches on `type` straight to the type's
own map codec: there is no `config` wrapper, so `config` is just another
unread key and a missing field's DFU error names the whole map. The
command codec still fell back to a `config` sub-map, so
`{type:"minecraft:simple_block",config:{}}` reported `MapLike[{}]`. The
inline_feature_parse parity harness (#15158) now matches 84/84 records.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Forward coordinate-hit Reset World tiles to actions 70-75 so Generate New
World reaches the existing CreateWorldRequest flow. Keep active tasks from
accepting either coordinate clicks or direct tile actions.
Follow Minecraft 26.3 RealmsResetWorldScreen.init/FrameButton and
RealmsCreateWorldFlow.createWorld. Extend the existing regression with
actual mouse coordinates, bounds, creation metadata and running-task gates.
Fixes#15273
Co-Authored-By: ZCode (new-provider/gpt-6.1-sol) <[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
On Linux a socket read timeout is reported as WouldBlock, so a read cut short by the last part of
the deadline came back as that error instead of TimedOut (seen in CI's
a_router_trickling_its_answer_runs_out_of_time). Both kinds now end the request as TimedOut.
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]>
LocalPlayer.isSuppressingSlidingDownLadder is !abilities.flying &&
super.isSuppressingSlidingDownLadder(), with LivingEntity's being
isShiftKeyDown() and ServerPlayer not overriding it. Horizon answered the
sneak for every body. A client body (client_side) that is flying no longer
suppresses the slide; a server body still looks at the sneak alone.
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]>
FallLocation.blockToFallLocation returns LADDER for Blocks.LADDER or a
block in #minecraft:trapdoors; Horizon mapped only the ladder by name, so a
fall off a climbed trapdoor said other_climbable. Move the name match into
fall_location_of and give it whether the block is in the trapdoors tag.
Co-Authored-By: Claude Sonnet 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]>
Replace only the two unseeded equality probes with deterministic built-in profession-domain checks. Keep saved VillagerData and all finalized reads exact, with regression tests for the corpus and strict feedback/callback comparison.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Keep quoted text, nested custom lists, modifier order and every styled
separator exact. Cover values, duplicate entries, modifier IDs and actual
state differences with regression tests; preserve the contributor commit.
Co-Authored-By: GPT 6.1 Sol <[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]>
Identifier.isValidNamespace returns false for the namespace "..", and
otherwise accepts a-z, 0-9, '_', '-' and '.'. from_parts only checked the
characters, so Identifier::parse("..:x") succeeded.
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]>
ListOperation.Append, Insert and ReplaceSection give the old list back when
the result would pass the limit (256 for lore) or the offset is past the end
of the list. The command cut every result to 256 entries and moved an offset
past the end to the end instead.
Co-Authored-By: Claude Sonnet 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]>
MagmaCube.setSize (MagmaCube.java:60-66) writes size * 3 into
Attributes.ARMOR; LivingEntity.getArmorValue (LivingEntity.java:1883-1885)
reads that base back. Mob armor now reads the set attribute base instead of
only the type's armor default, so a summoned size-4 cube keeps its 12 armor.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RconClient.sendCmdResponse cuts the Java string with substring at 4096
UTF-16 units, so a character outside the BMP can straddle a cut, and
String.getBytes(UTF_8) writes each lone half as '?'. Horizon counted
characters instead. Split the output on UTF-16 units and write a lone
surrogate half as '?'.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`width` and `interaction` share a HashMap bucket at every table size, and
keys sharing a bucket iterate in insertion order. java_hash_order ranked
neither, so they fell back to key order and `interaction` came first,
while Interaction.addAdditionalSaveData writes width, height, attack,
interaction and response in that order. The interaction_data parity
harness (#15158) now matches 120/120 records.
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
The official server's players.online (any u64 from a plain status ping) went into the history
as i64: above i64::MAX it wrapped negative, and near it SUM() overflowed, failing
/v1/live/history?source=official until the rows were pruned. A count above a million is now
treated as no answer: that minute stays empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- Host: the inbound handshake slot is an RAII guard, so a failed spawn can't leak one of
MAX_HANDSHAKES until the direct port takes nothing.
- Sessions authenticate at most 16 direct-race streams, each under one 6 s deadline instead of a
per-read timeout a peer could drip past.
- Invite decode takes at most MAX_ENDPOINTS (8), as encode writes: a crafted code can't make the
guest dial thousands of addresses.
- A STUN attempt has one deadline, so datagrams arriving faster than the read timeout can't keep
the query, and the host's setup, going forever.
- A UPnP description's controlURL, URLBase or serviceType with control characters or spaces is
ignored instead of reaching a request line or header.
- The guest's direct-race listener handles each inbound stream on its own thread with a deadline.
- Relay: the connection count is given back by a guard, and attach tokens compare in constant time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- UserList add/remove only log a failed save, so with a read-only or full disk the CLI and the
stopped server's API said "Banned X" or "Made X a server operator" though the file still said
otherwise. Each change is now saved and checked (saved()).
- ServerProperties::store wrote server.properties in place, default mode: a crash at start left a
short file read as defaults (a new management secret, settings dropped), readable by other
accounts. It now writes a new file (0600 when new, an existing file's mode kept) renamed over.
- system_message/kick translatable parameters are plain text, as vanilla's Message takes them,
not components tellraw would resolve with the console's rights.
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]>
- One address could hold all 16 RCON places idle and keep the operator's own client out; each
address now gets at most 4.
- Management notifications went to every client from one thread with blocking writes: a client
that stopped reading stalled them for everyone. Management sockets get a 10 s write timeout
and a client whose write fails is closed.
- The event stream (SSE) gets a write timeout too, so a client that stops reading doesn't pin
its thread forever.
- same_token compares SHA-256 digests, so its time doesn't show the token's length.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
The council's polls are applied: rdbtCVS is the second organisation owner,
the council team holds CODEOWNERS and the security-manager role. The pages
that said Ludovic alone held these powers now say who does.
Co-Authored-By: Claude Opus 5.5 <[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
With Improved Transparency the cracks draw vanilla's destroy_oit_stage_* sprites, but the
crumbling shaders took their size from destroy_stage_0. A resource pack that replaces only the
plain stages at a higher resolution made the crack window larger than its sprite, so it showed
neighbouring atlas sprites (#11164). crumbling_sprite now reads the set the cracks draw.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
serde_json's index panics on a Map missing the key and on a non-object Value: a density
function "spline" without its "spline" field, a template pool whose elements aren't objects, or
a structure whose horizon:piece_limited isn't an object panicked the port. In the launcher that
killed pack_job's thread before PackDone, so the Modpacks page stayed busy until a restart.
Those reads now use get/get_mut and skip what isn't there, and pack_job turns a panic in a step
into an error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Keep main's minecart index/display extraction and expose cart_at to the below-world discard helper. Vehicle discard behavior is unchanged.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
- dechunk sliced the device's chunk size into a from_utf8_lossy string: a chunk ending inside a
character panicked the sharing thread, under the reach lock on renewal, and the poisoned lock
then crashed the game at Host::stop. It now stops at such a chunk, and stop() and the renewal
loop take the lock even when poisoned.
- A router's HTTP answer has one 10 s deadline (each read was limited, not the answer), SSDP
discovery tries at most 8 gateways, and the mapping is renewed outside the reach lock, so
stopping sharing no longer waits on a slow device.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
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]>
`sudo horizon-server service install --system`, as the help says to run it, wrote `User=root`:
sudo resets USER to root, so the network-facing server (game port, RCON, management API, panel)
ran as root and left root-owned files in the operator's folder. The unit now runs as SUDO_USER,
else USER, and refuses to be written for root (from a root shell).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- 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
- The relay-wide unknown-code budget now refuses only joins whose code turns out unknown:
a handful of /24s or /48s could keep it empty and every guest with a real code got
RateLimited. Per-address and per-site budgets are still checked before the lookup.
- Behind the PROXY protocol, a connection from outside RELAY_PROXY_FROM is closed before
anything is read, as the docs say, instead of holding a slot for first_line.
- Registrations get a per-site budget (IPv4 /24, IPv6 /48; 30 a minute, burst 10) on top of
the per-/64 one, and ended nameplates expire oldest first from a queue instead of a scan of
the whole map under the global lock on every registration.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
AbstractScrollArea.extractScrollbar requests POINTING_HAND over a scrollable
bar and RESIZE_NS while it is held. The pack screen drew its scroller without
asking, so the pointer stayed an arrow over both lists. The draw now requests
the hand or the resize cursor from the held state it already tracks.
`SerializableChunkData.parse` reads the section list with
`getListOrEmpty("sections")`, so a chunk that does not carry one is a chunk with
no sections rather than an unreadable one. `ChunkData::new` refused it, so an
empty or hand-written chunk could not be opened; it now keeps an empty list in
the tag, which is what the writer would put there and what the accessors below
look for.
`Zombie.isUnderWaterConverting` and the `ConversionTracker` it drives had no
row, nor did the two husk overrides that send the conversion to a zombie; the
behaviour is `undead.rs::custom_server_ai_step`, and the row cites it for
each method the jar declares there.
Names read from `javap -p` on 26.3: `Zombie.isUnderWaterConverting`,
`Zombie.convertsInWater`, `Zombie.convertsToWhenDrowning`,
`Zombie.getConversionSound`, `ConversionTracker.tick`, `Husk.convertsInWater`,
`Husk.convertsToWhenDrowning`, `Husk.getConversionSound` and
`ZombieVillager.convertsInWater` (false in the jar, so it is not part of the
arm).
`map-check`: 165 rows, no problems.
`SerializableChunkData.parse` builds the position with `getIntOr("xPos", 0)`
and `getIntOr("zPos", 0)`, so a chunk that does not carry either tag is the
chunk at 0, 0 and `Tag.asInt` accepts the narrower integer tags as well as
`Int`. `ChunkData::new` refused the whole chunk, so an edited or hand-written
chunk without them could not be opened at all.
AbstractWidget.WithInactiveMessage.getMessage returns
ComponentUtils.mergeStyles(message, Style.EMPTY.withColor(-6250336)) while the
widget is inactive, so a disabled button's message is drawn grey (0xFFA0A0A0).
screen_draw::button drew the same white label in both states; it now colours
the label when inactive.
The documented `bufferObject.<n> = <size> <file>` form was refused:
the loader read the value as a size and nothing else, so a pack with
`bufferObject.2 = 16 data/seed.bin` lost its buffer with the note
"not a buffer object".
The loader now reads that form: the file's bytes come from the pack,
relative to `shaders`, and the buffer starts with them. A file that is
not there, that leaves the pack, or that is bigger than the buffer it
fills is refused with a note, as the other pack properties are.
Co-Authored-By: OpenCode <[email protected]>
Font.PreparedTextBuilder.markBackground reaches one pixel left of the first
glyph and one above it, the glyph advance to the right and nine pixels down.
Horizon drew two pixels on every side (right and bottom included), so every
HUD title, action bar, boss bar and selected-item backdrop was a couple of
pixels too large. The fill now matches the glyph box.
Without the change the four length cases fail with `Err(InvalidData, "invalid
chunk payload length")` and the cut-off case with `Err(UnexpectedEof, "failed
to fill whole buffer")`.
`RegionFile.getChunkDataInputStream` answers `null` whenever the file cannot
hand over the chunk: a header cut off by the end of the file ("Chunk header is
truncated"), a declared length of 0 ("allocated, but stream is missing"), a
length past the chunk's sectors ("stream is truncated") or a payload shorter
than its own header. Horizon returned an `Err` for all four, so a reader that
walks a region gave up on the whole file instead of skipping the chunk.
A zombie at 3.0 absorption converts through `single` into a drowned: the new
mob reads 3.0. Without the copy the new mob has 0.0, the effect having been
copied but not the field the effect filled.
`ConversionType.convertCommon` copies the effects, then
`to.setAbsorptionAmount(from.getAbsorptionAmount())` - the golden hearts an
absorption effect left on the mob are a plain field, and the effect copy does
not rebuild them.
`conversion::single` copied the effects but never the field, so a zombie with
three absorption hearts that turned into a drowned came out with none.
Move the existing DTOs and response parsing into a private model module,
retaining public social paths and the existing parsing tests. Keep wire
and presence helpers visible only inside the social module tree.
Document the small pure-move preparation sequence, including a mandatory
manager module and a vanilla audit gate after all structural steps.
No cache, scheduling, action or UI behavior changes are included.
Fixes#15179
Co-Authored-By: ZCode <[email protected]>
tools/ci/legacy_releases.py writes the pinned SHA-256s of the releases
published before signing began, and tools/ci/resolve_reviewer_ids.py
fills in the numeric GitHub IDs the comment commands decide by. Twelve
of their thirteen siblings under tools/ci/ have a test; these two did
not, although both are pure functions over what GitHub returns.
Co-Authored-By: OpenCode <[email protected]>
Milking a cow, goat or mooshroom plays at pitch 1.0 with no level-random draw, as AbstractCow.mobInteract, Goat.mobInteract and MushroomCow.mobInteract do.
Shearing a sheep or mooshroom by hand now posts GameEvent.SHEAR with the player, as Sheep.mobInteract and MushroomCow.mobInteract do.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its two version lookups fell back to a literal "26.3" when the lock
file was missing or had no version, so a version bump that touched
only research-lock.json left the ledger looking in the wrong folder.
tools/lab.py already reads the pinned version from there; the
fallback now uses it.
Co-Authored-By: OpenCode <[email protected]>
Its help marked "build --check" and "check --offline" as (CI), but no
workflow runs them: CI runs tests/test_roadmap.py, which checks the same
two things (the generated files match the toml, and the file keeps its
rules). The help pointed contributors at a command CI never runs.
Co-Authored-By: OpenCode <[email protected]>
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]>
🤖 Agent: CODEOWNERS moves from @ludovic111 to @horizon-rust/council, as the council decided (P35); the team exists with the six members and write access; reviewer tests pass. Paths guarded by /nightly are unchanged.
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.
The council decided (P35, 2026-11-10) that code-owner reviews move from the
founder to a GitHub team of the six council members. The team
horizon-rust/council exists and has write access, which code owners need.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
container_screen: the oracle makes its screens with Unsafe.allocateInstance,
which skips AbstractContainerScreen's constructor and so its
`skipNextRelease = true`. #13723 made Horizon's Clicks start that way,
as the real screen does, and the oracle then disagreed on every case
whose first event is a release (373 cases). The oracle now sets the
field the constructor would.
stand_lodestone needs --work-dir, a frozen Java run (--reference-dir)
and a built game (--binary), which an automatic run never passes, so it
failed in 0 s on every runner. It joins the other capture-preserving
harnesses in parity_hosts.json's named_only: it runs when named, and
in the nightly audit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractSliderButton.handleCursor asks for RESIZE_EW while dragging,
POINTING_HAND when hovered and NOT_ALLOWED when inactive. Horizon's
widget_cursor only knew the hand and the forbidden sign, so the shell's
sliders (their held state already known in RowView) never asked for the resize
cursor. widgets::slider_cursor adds it, the shell's slider rows call it, and
the client maps Cursor::ResizeEw to EwResize.
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]>
The #[path] mod wheel block sat right after pub use Opt, the anchor the
widgets PR uses. It now sits after mod shell_pages, and its test moved into
the module's own #[cfg(test)] mod tests instead of appending to
options_ui_graphics_tests.rs, so the three siblings touch disjoint lines. No
behaviour changes.
The #[path] mod preset block sat before use crate::{, the same anchor the
widgets and wheel PRs use. It and its pub(crate) use now sit just above the
test modules at the end of the file, and the preset test moved into the
module's own #[cfg(test)] mod tests, so it no longer appends to
options_ui_graphics_tests.rs either. No behaviour changes.
`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.
Options.mouseWheelSensitivity is an IntRange(-200, 100) through
logMouse/unlogMouse: the value is 10^(i/100), so the slider's handle sits at
(i + 200) / 300 and a drag picks the step by floor(map(v, 0, 1, -200, 101)).
Horizon used a straight line from 0.01 to 10.0, which put the default 1.0 at
0.099 of the track and made every drag pick a different sensitivity.
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.
The packet queued with `served.send` and the one the probe sends from
`Session::tick` are framed by different paths: `Conn::serve` takes the first
from the outbox, `Conn::sweep` frames the second straight into `out`. Whichever
the worker thread reaches first is written first, and the sweep is on a timer,
so a CI run took the probe's packet (id 33) before the queued one and the test's
two `assert_eq!`s failed. The order is common, but not something the pool
promises.
The test now takes both packets and tells them apart, then checks what it is
about: the kick goes after the queued packet, `kicked` is reported, the
connection closes, and nothing follows the kick. The kick is still sent only
after both packets arrived, so that part stays ordered.
Fixes#14660🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
`a_slot_toggled_in_the_menu_lands_on_the_crafter` opens a crafter with
slot 3 disabled, clicks it with an empty hand and checks the block
entity: the slot is enabled and the chunk is marked. Without the mark the
click leaves `world.modified` empty.
`CrafterBlockEntity.setSlotState` ends with `setChanged`, which is what
saves a toggled slot. Horizon applied the menu's slot states to the block
entity on every click (`menu_click`'s write-back) but never marked the
chunk, so a disabled slot could be lost on save.
The write-back is now `Game::apply_crafter_slot_states`: it copies the
menu's flags, and when one changed calls `container_changed` (the
container-marking path, which also re-reads comparators).
A 1.20.2-1.21.4 server's smithing-trim display (or trim recipe) is expanded into one
SmithingTrim per pattern, each with its own copy of the base and material displays. Those
can themselves be trim displays, so a few hundred bytes asked for billions of nodes.
Refuse a preview whose patterns x (base + material weight) exceeds MAX_TRIM_PREVIEW (4096),
counted with the new SlotDisplay::weight, both in SlotDisplay::read (1.21.2-1.21.4) and in
the 1.21.1 trim recipe translation; the 1.21.1 path also builds each display once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Every Commands packet becomes a dispatcher, and every resource-style
argument the vanilla tree doesn't match goes through intern(), which
Box::leaks its registry name for the life of the game. The name is the
server's, up to 32767 bytes and unchecked, so a server sending trees of
new names packet after packet leaked megabytes each time until the
client ran out of memory.
intern() now keeps only resource-location names of at most 256 bytes,
and at most 1024 of them; anything else is kept as "" (no suggestions
for that argument).
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]>
SystemEntropy is splitmix64 seeded from 8 bytes. On a first start with
no secret and no level-seed (both defaults), the 40 characters of
management-server-secret were outputs 1-40 of that stream and the world
seed was output 41; splitmix64's mixer is a bijection, so the world
seed (which ops read with /seed, and seed crackers recover from
terrain) gave back the state and every secret character. The secret is
kept when the owner later enables the management server. On Windows the
seed fell back to the clock, so the secret depended only on the start
time.
Secrets (the management secret, the TLS keystore password) now come
from OsEntropy, every draw straight from getrandom with rejection
sampling, as vanilla's SecureRandom. SystemEntropy itself is seeded
from getrandom too, the clock only if that fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
PATCH /api/v1/config checked the raw JSON key with changeable_remotely,
which doesn't trim it, then applied "{key}={value}" through
parse_change, which does. A key " engine.path" (or "\tapi.allow_public",
" discord.webhook", " properties.enable-rcon") passed the deny-list and
was written to server.toml: at the next restart the API or panel opened
to the internet, chat went to another webhook, RCON came on, or the
supervisor ran another program.
changeable_remotely now refuses any key with whitespace or control
characters in it, so what it checks is what gets applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
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
A backup turned autosave off, then flushed. If the flush failed (the
control connection's 30 s reply timeout on a big world or a slow disk),
the ? returned before `console save-on` was sent, and the game kept
running with autosave off until the next backup that succeeded;
scheduled backups take this path, so a crash in that window lost
everything played since.
Once save-off has been asked, saving is now always turned back on: over
the same connection after a good backup, and over a fresh one when the
flush failed (a reply that timed out leaves the old one out of step).
The flush gets its own 300 s reply timeout.
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]>
A pack whose one terrain program (gbuffers_terrain) draws both solid and
cutout terrain ran it twice over the solid list, each pass moving the other
part's quads off-screen: every solid-terrain vertex was shaded twice (0.6 ms a frame measured with a path-traced pack on an RTX 2080).
Now the game also gives the loader terrain's inputs for one pass
(GameInputs::terrain_one_pass, GbuffersInputs::parts): the vertex stage
works out its quad's part from the sprite layer, as before, and hands it to
the fragment stage as a flat varying, which applies that part's alpha test.
alphaTestRef becomes a per-part value in both stages (0 for solid, 0.5 for
cutout, as Iris 1.11.7 did in #9307's probes), and alphaTest.gbuffers_terrain
still replaces both parts' tests. When both tests are the same, nothing
changes in the shader. Packs with gbuffers_terrain_solid or _cutout keep
their two passes.
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]>
HORIZON_SHADER_PACK_TIMING pushed every pass's time into the window of its name, so two passes of
one name in a frame (a program drawn over several render passes, or deferred2.csh beside
deferred2.fsh) shared a window and its median was the time of neither. A frame's passes of one
name now add up to one sample, and a compute pass is timed as "<name> (compute)".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Zombie.doHurtTarget` (Zombie.java:315-325) rolls `effectiveDifficulty * 0.3`
for a burn of `2 * (int)effectiveDifficulty` seconds only when
`super.doHurtTarget` landed, the main hand is empty and the zombie is on fire,
and every species that extends `Zombie` inherits it: a burning husk, drowned,
zombie villager and zombified piglin now burn too, while a zombie holding a
weapon and a blow a shield took whole no longer do. The chance is drawn in
`do_hurt_target` where vanilla draws it, in the mob's own tick, and the game
lights the burn only for the blow it rides.
The turtle egg's break sound also draws its pitch from the level random
(Zombie.java:553-555): `0.9 + level.getRandom().nextFloat() * 0.2`.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A window cleared its GPU timings when its time was up, then wanted eight: at 10 FPS (five frames in
500 ms) or with timestamps arriving in gaps, every window started over and the scale never came
down in the scenes that need it most (review on #14815).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Match TagValueInput.getByteOr for wider numeric and fractional tags, and cover signed frame rotation through NBT round trips for both frame kinds.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
GoatAi.initRamActivity wants TEMPTING_PLAYER and BREED_TARGET absent, and
GoatAi.updateActivity picks RAM before IDLE, so a goat being fed or bred only
follows; a player who starts feeding it during the prepare also ends the
activity (PrepareRamNearestTarget.stop starts the cooldown).
RamTarget.tick runs applyItemBlocking a second time for its mobAttack push and
scales it by blockingFactor (0.5 when the victim's raised item blocked part of
the hit). The push now names the damage the hit dealt so the host can do that,
and blockingFactor * speedFactor keeps vanilla's float-then-double order.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[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]>
OptionInstance.SliderableEnum.fromSliderValue maps a slider position by
floor(v * values.size()), with 1.0 just below, and clamps to the last value:
the four GraphicsPreset values change at 0.25, 0.5 and 0.75. Horizon rounded
v * 3, so dragging to 0.2 picked Fancy instead of Fast and 0.8 Fabulous
instead of Custom.
The client kept only the dialog tags of ClientboundUpdateTagsPacket and
read block and item tags from the game's own files, so a data pack's or
plugin's tags (climbable, mineable, ...) never reached a player on a
server. Dynamic now keeps every registry's tags; a server session builds
the game's Items with the server's block and item tags in place of its
own (by name, through the built-in registries), and Game::items() reads
it. The play-state packet after /reload replaces them the same way
(ClientPacketListener.handleUpdateTags).
The server tables (the synced enchantments from #9301 and now the items)
move to game/remote_tables.rs, so remote.rs shrinks.
Closes#7843
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt.getOwner names the mob a saved owner UUID points at, so
dropExperience (the indirect source is a player) is false. Keep
whether the owner is a player beside blast_owner (a chain reaction
hands a mob owner on as a mob), and compare only whether orbs came
out, since they may merge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockInput.place loads the tag into the comparator's block entity, and
ComparatorBlockEntity.loadAdditional reads getIntOr(OutputSignal, 0).
Horizon keeps a comparator's output beside the block entities and only
/data wrote it; /setblock and /fill now load it through the same
load_comparator_output.
Fixes#13870
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block.onExplosionHit passes dropExperience = the explosion's indirect
source is a player (a primed TNT's owner) to spawnAfterBreak, so ores,
sculk and spawners blasted by a player's TNT sample and pop their
experience while blocks drop. Horizon popped none.
Part of #11330
Co-Authored-By: Claude Opus 5.5 <[email protected]>
container_edge_sound sent every block named *chest* to block.chest.open
and close. Vanilla's ChestBlockEntity opener counter plays the block's
getOpenChestSound/getCloseChestSound, which a copper chest takes from
CopperChestBlock.getHingeSound by its weather state. The copper golem's
transport path already had that mapping; it moves to
transport_items::chest_hinge_sound and both paths use it.
Closes#15039
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]>
Include the root model scale in the appendage margin so animated elder guardian vertices remain inside the precheck bounds. The existing prepared-model coverage test remains unchanged.
Co-Authored-By: Codex (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]>
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]>
#14603 gave new_rocket the rocket's own random as a fourth argument; the
tests added by 188817342 still called it with three, so the client's tests
stopped compiling on main.
Refs #15025
Co-Authored-By: Claude Fable 5.2 <[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)
#14335 read Game::studio_replay, which the layer split had moved to
Game::replay before it merged, so main stopped compiling.
Fixes#15025
Co-Authored-By: Claude Fable 5.2 <[email protected]>
On a server whose data pack adds an enchantment, the anvil showed the red
cross for a book of it: the client's result prediction read the world's
own enchantment table, which lacks the entry. AnvilMenu.createResult on
the client reads the synced registry, so the station previews and the menu
engine now read Game::enchantments(), the server's table in a server
session and the world's otherwise.
Closes#8688
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain every scoped worker handle, release the first-lane guard, join all workers, then resume the first original worker payload. Extend the production-path injected spawn/setup/mid-turn regression to assert exact payloads as well as timeout-bounded release.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Inject later-spawn and worker-setup failure through the production island runner; keep first and pending guards inside the scope callback. Compare independent cloned Gaussian/long outcomes and final group snapshots. Keep raid-member ticks serial in both production and verify so the serial diagnostic copy cannot mutate shared hurt ordering. Cover one-CPU fallback and preserve the original ticked check.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
`SocialInteractionsScreen` declares no `mouseClicked` at all: its clicks come
from `ContainerEventHandler.mouseClicked`, the inherited child dispatch, which
reaches the search box's `EditBox.onClick` before the tabs, the rows' buttons
and Done. The body and the port map had attributed that to an override on the
screen.
`social_screen::click_kind` now answers which child the click belongs to -
`Click::Search` for the box, `Click::Action` for the list - and `Game::social_click`
performs it. The test drives `click_kind`: both corners of the box are the
box's, the pixels left of and just below it are nobody's, and Done's centre is
the list's `Action::Done`. Without the search branch the two inside corners
read `None`.
The caret move itself still needs the GUI atlas (`EditBox::click`), so it sits
in `Game::social_click` and is not asserted: every `Game` test in this crate
needs prepared resources and is `#[ignore]`d.
Compare deferred and in-place metadata for 2,085 varied mobs, including pack-selected effect particles and switch-off vector reuse. Exercise live climbing, snapshot order and nearest-player invalidation for 293 mobs. Build 132 random-tick section masks ahead and retain exact event, state and random comparisons through edits and reloads.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Includes 0b21fcbf60, the unchanged cherry-pick of 9298d7961c. Preserve the original islands implementation and both contributors' history.
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Treat LegacyRandom::set_seed as an ordered mutation: acquire the shared stream before replacing its LCG state and clearing the Gaussian cache, without discarding the gate. This also publishes a reseed-only item's state through end_item and keeps finish_lane and LaneDone working.
Add timeout-bounded multi-lane regressions for reseeding before/after draws, reseed-only items, cross-lane Gaussian state and panic cleanup, plus serial cache-reset coverage. The regression rejects the original gate loss, omitted synchronization and retained Gaussian cache.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
(cherry picked from commit 9298d7961c)
Cap waiting lanes at available hardware, release all unstarted lanes on spawn failure, reject invalid planner inputs, and verify full outputs including pending pushes and Gaussian state. Order new mob RNG allocations with level draws; record/replay diagnostic seeds without resetting the global counter or pinning other threads. Add resource-backed planning, visibility, moved-rider and actual verify coverage.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
InsideBlockEffectType.FREEZE raises Entity.ticksFrozen for every entity
box touching powder snow and Entity.saveWithoutId writes TicksFrozen
while positive. Boats and minecarts never tracked it.
A snapshot could copy an old cached height and then a stale bit another reader had just cleared, retaining the old height as clean. Acquire the stale-bit snapshot first: marked columns rescan, and cleared bits publish the refreshed height. A concurrent Chunk snapshot test covers both heightmaps and fails with the original ordering.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: OpenCode GPT 6.1 Sol <[email protected]>
Count performance cores on Linux systems with asymmetric core frequencies
(such as Intel Alder Lake, Raptor Lake, Meteor Lake and ARM big.LITTLE)
via sysfs cpufreq, bringing parity with the Apple silicon performance core
heuristics from #11294.
Co-Authored-By: Antigravity <[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]>
`BannerPatternLayers.CODEC` is `Layer.CODEC.listOf()` and
`BannerBlockEntity.loadAdditional` resolves it with `ValueInput.read` plus
`Optional.orElse(BannerPatternLayers.EMPTY)`, so a single layer the codec
rejects leaves no layers at all. The branch kept the valid ones with a
`filter_map`, which is the partial behaviour the body says it fixed.
The list now reads all or nothing, and the test asserts the empty answer for a
mixed list, a missing field, and that a list of valid layers still reads back
(and round-trips), so an always-empty result cannot pass it.
The row pointed at passenger_attachment, which is the seat for riders on the
spider, and claimed the spider always uses its fixed seat. The rule is
vehicle_attachment: 0.3125 of the scale only when the vehicle's box is no wider
than the spider's, and a wider vehicle falls to the type's vehicle attachment,
EntityAttachment.VEHICLE's AT_FEET fallback, which is the feet.
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]>
ai::step_sound over a stone block: a sulfur cube with a stone body answers
None, and an empty one answers block.stone.step at volume 0.15 and pitch 1.0.
Without the arm the loaded cube reads Some(("block.stone.step", 0.15, 1.0)).
SulfurCube.playStepSound returns before its super while the cube has a block
in its body, so a loaded cube makes no footstep at all; an empty one takes the
block's own step at 15%. step_sound had no sulfur cube arm, so a loaded cube
walked with block.stone.step like an empty one.
The arm sits in step_sound, which moves out of ai.rs into mobs/ai/step.rs with
the block sound ids it interns: ai.rs loses 100 lines of code to the move.
`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]>
Mob::check_fall_damage with six blocks of fall and the mob standing inside a
lava pool: the strider takes 0.0 and keeps a fall distance of 0.0 with no
LandingDust, a pig in the same lava takes 3.0, and a strider landing on stone
takes 3.0 too. Without the arm the strider in lava reads 3.0.
Strider.checkFallDamage resets the fall distance and returns while the strider
is in lava, so the base's landing never runs: no fall damage and no dust, and
a strider that drops into a lava sea walks out of it owing nothing. Horizon
ran the base for every species, so a strider that fell six blocks into lava
took the three a pig would.
`a_container_writing_a_shelf_posts_block_activate` puts a shelf and its
block entity and runs the container-change path on its view: the posted
events hold `("minecraft:block_activate", [2.5, 4.5, 3.5], None)`.
Without the post the same test fails with `Some([])`.
`ShelfBlockEntity.setChanged()` is overridden without arguments and
passes `GameEvent.BLOCK_ACTIVATE` to `setChanged(Holder)`, so any
container write to a shelf (a hopper's or dropper's `setItem`) posts a
block activation at the shelf and syncs it to clients.
Horizon's `Game::view_changed` marked the shelf's chunk and re-read the
comparator, but posted no game event.
`ZombieVillager.defineSynchedData` seeds `DATA_VILLAGER_DATA` from
`initializeZombieVillagerData(this.random)`: one `nextInt(15)` on the entity's
own random, made inside `Entity`'s constructor. Horizon built a synthetic
`LegacyRandom` out of the mob's random state and id instead, so the profession
never matched vanilla's and the mob's own random never advanced.
The draw also has to sit in the right place in the stream. `Entity.<init>`
makes the UUID's two draws and then calls `defineSynchedData` last, while the
yaw `living_random` used to take belongs to `LivingEntity`'s constructor, after
it. So `living_random` now returns the random straight after the UUID, and the
two draws it left behind move to `zombie_villager::constructor_draws`, which
`Mob::with_random` calls in vanilla's order. Nothing moves for other kinds:
those draws are constants, so their streams are unchanged.
Two draws vanilla never makes are gone as well. `ZombieVillager.finalizeSpawn`
only picks the villager type and never rolls a profession, and neither does the
spawn path. `readAdditionalSaveData` redraws only for a finalized save with no
`VillagerData`, which `read_nbt` now matches.
Verified with the issue's repro (`parity_fuzz.py --replay`, seed 998): vanilla
`butcher` against Horizon `nitwit` before, both `butcher` after. The replay's
other difference, `Motion[1]` -0.0784 against 0.0, is pre-existing on
`origin/main` and untouched here. `seed_sync`, `zombie_cure_sound` and
`world_spawn#1`-`#4`/`world_spawn_sync` pass.
`mobs.rs` is past the 3000-line limit, so it gains no lines: the helper lives in
`zombie_villager.rs` and `living_random` loses the yaw line it used to take.
Co-Authored-By: DeepSeek V4.1 Flash (OpenCode) <[email protected]>
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[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.
The type check (beb30045fd) keeps a built-in only as the type it gives, sized by components(), which
took Horizon's own vec4s for floats: a pack declaring horizonDynamicLight<n> (#12069) or
horizonWaterPatch<n> as vec4 had them switched off and read 0. Their counts stay ints.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
BookViewScreen.extractRenderState visits its pages with
HoveredTextEffects.TOOLTIP_AND_CURSOR, which shows the hover tooltip and
requests the pointing hand over a clickable word. Horizon showed the tooltip
but never asked for the cursor, so links looked like plain text.
CommandSuggestions.SuggestionsList.extractRenderState ends with
rect.contains(mouseX, mouseY) -> requestCursor(CursorTypes.POINTING_HAND).
Horizon's draw_list highlighted the hovered row but requested no cursor, so
the pointer stayed an arrow over the suggestion list.
Villager.readAdditionalSaveData reads it with getByteOr, which narrows
any numeric tag (IntTag.byteValue is (byte)(value & 255)): FoodLevel:300
loads as 44. The loader kept the raw number.
The cube's AI step grew every adult of size 1 to size 2 on its second
tick, and shrank every baby above size 1 back to size 1. Vanilla's
SulfurCube.ageBoundaryReached runs only where AgeableMob.setAge crosses
the baby boundary, which finalize::set_age already does. The tick check
is gone: a summoned or loaded adult of size 1 and a split baby of size 2
keep their size.
command_exec gets a "sulfur cube size ticks" group with the cubes' AI on.
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.
Match vanilla's cancellation boundary once a world archive starts extracting, and cover cancellation on both sides of that boundary.
Co-Authored-By: ZCode <[email protected]>
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]>
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]>
Adds a fourth NoAI pig (self) summoned with last_hurt_by_mob set to its
own UUID, read at the same points as echo, so vanilla 26.3 shows that
EntityReference.getEntity resolves the holder too and baseTick ends the
record after 100 ticks.
Part of #12830
Co-Authored-By: Claude Sonnet 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]>
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]>
Players are entities of the level from placeNewPlayer/respawn on, so a
guest whose client is still loading is equipped too, and guests and the
local player sort with the mobs and stands: entity sections first
(EntitySectionStorage.forEachAccessibleNonEmptySection, SectionPos.asLong
order), then join order by entity id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EquipmentDispenseItemBehavior picks the first living entity in front that
canEquipWithDispenser, players included; ArmorStand.canUseSlot rejects locked
slots and hands without arms. Fixes#12937
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entity.load reads Fire with getShortOr for every entity. The player's
loader read it, Air and TicksFrozen through one unnarrowed read; Fire
is now narrowed, the other two stay ints as vanilla reads them.
Checks the borrowed lookup against the clone-and-flatten expression it
replaces, over an empty slot, filled slots, slots past the end and an
empty inventory.
Refs #14939
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Replace the open-ended UI scope with eight reviewable work packages and
separate backend, toast, face and host-adapter steps. Record vanilla
entry/lifecycle requirements, old-port defects, evidence limits and the
maintainer decisions required before implementation.
Keep the account overlay distinct from the approved host/share migration;
leave the old implementation PR and isolated merge branch untouched.
Co-Authored-By: ZCode <[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.
`a_crafters_grid_counts_for_the_recipe_book` opens a crafter menu, puts
four oak planks in its first slot and asks the stacked contents whether
an oak-plank ingredient can be crafted. Without the crafter arm the same
call fails.
`CrafterBlockEntity.fillStackedContents` hands the crafter's nine slots
to the recipe book's stacked contents (`accountSimpleStack` each), so an
open crafter's grid counts towards the craftable check.
Horizon's `Menu::stacked_contents` read the player's inventory, the mod
sources and the `GRID` container, but a crafter's slots are its `MAIN`
container (`CrafterMenu.addSlots`): with a crafter open the book saw an
empty grid and marked nothing craftable.
Random viewers and sections (20,000 cases, with an empty section checked
too), and every single block around 60 random viewers including the edge
of the reach, assert that reaches() keeps any viewer for which near()
holds. The sections-with-hits count and the blocks-near count guard
against a test that proves nothing.
Refs #14923
Co-Authored-By: Claude Opus 5.5 <[email protected]>
broadcast_block_changes tested every changed block of a section against
every recording viewer, although only a block inside the viewer's `near`
reach is ever recorded. bounds() takes the section's box once, and
reaches() rejects a viewer whose reach misses it, using the same
expressions as near() so the reject is conservative. note_changes() keeps
the per-block loop and its order, and moves the loop out of hub.rs.
Refs #14923
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horse.getVariant is Variant.byId(typeVariant & 255) and getMarkings
Markings.byId((typeVariant & 0xFF00) >> 8); both maps are
OutOfBoundsStrategy.WRAP. The loader clamped them, so Variant:9 was
dark brown where vanilla's horse is chestnut. The saved Variant still
comes from the two ids, not the loaded number.
bodies() encodes one body per set of columns. The test builds the packet
each receiver got before (cloned columns, encoded alone) for three
receivers sharing five columns, a receiver with a subset in another
order, and one with none, and compares the bytes.
Refs #14925
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
resend_biomes cloned each receiver's ChunkBiomeData and encoded the
ChunksBiomes packet again for every guest. Receivers that have the same
columns now share one encoded body, built by the new lan::biome_resend
module; hub.rs gets smaller. Each guest still gets the same bytes in
the same column order, and the fake-player filter is unchanged.
Refs #14925
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Differential test over 2000 seeded random inputs: entity sets of up to 40
states (all four kinds, own and other ids), guest positions and slacks,
with entities placed on or next to every edge of the close box, checked
against a verbatim copy of the old scan. Plus an inclusive-edge test and
the own/item/orb exclusions. The inputs are multiples of 1/8 so the box
edges are exact in f64. No world or game resources needed; cargo not run.
Refs #14920
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
excused() ran a string compare on each entity's kind before the box test,
for every guest every tick. Move the box test first (it is a pure float
check) and move the entity scan into entity_near, so the excuse can be
checked against the old scan. The boolean result is unchanged: the
predicates are pure and any() is order-independent.
Refs #14920
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.
game.rs is over the 3000-line limit, so the gate and its test move into
native_criteria.rs, which already builds the player's advancement views,
instead of a new module declared in game.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`a_disc_change_posts_block_change` places a jukebox, records the posted
game events and calls `set_has_record`: the box posts
`minecraft:block_change` at its centre with no source. Without the post
the recorded list is empty.
A 0.2 pack can't show a colour outside sRGB today: the contract says every
colour is in BT.709 primaries, forever. This adds the key that will let a
pack ask for more, before 0.2 is on by default and packs are written against
that contract.
- `[frame] primaries = "bt709"` (the default) or `"bt2020"`. The check takes
`"bt2020"` only with `blend = "linear"`, since a change of primaries is only
right in linear light. The game refuses a `"bt2020"` pack for now, as it
refuses `blend = "linear"`.
- `Frame` gains `primaries` and `luminance` (that space's luminance weights)
after `black_level`. The renderer writes 0 and BT.709's weights. The
uploaded buffer grows by 16 bytes; 0.1 declares the same prefix as before.
- The reference's "Room to grow" says a later release adds a name to a
stage's prelude only under a manifest key the pack sets or in a new stage.
`tests/names.rs` holds each 0.2 prelude to its list of names
(`tests/names-0.2/`), and shows why: a pack's own `fn depth_at` in a post
stage is refused as a redefinition, its own `fn to_bt709` passes.
No pack's picture changes: a pack without the key, a `"bt709"` pack and 0.1
packs draw as before.
Part of #14584 (step 1) and #12989.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
-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.
`JukeboxBlockEntity.notifyItemChangedInJukebox` sets the `has_record`
block state with update flag 2 and then calls
`level.gameEvent(..., GameEvent.BLOCK_CHANGE, pos)`: sculk sensors hear a
disc enter or leave a jukebox. Horizon's `set_has_record` changed the
state but posted no game event.
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.
SlotSources.CODEC admits a registered holder (`ns:id`), a holder list and a
`#tag` holder set wherever a slot source is accepted, and validates the
`group`, `limit_slots`, `filtered`, `contents` and `slot_range` shapes
(including a `slot_range` source context) against the loaded registry.
DataStore loads `tags/slot_source` in the same pass as the definitions, in
pack order with `replace`. `slot_source_tag` resolves a tag as TagLoader
does: first-seen member order, nested tags, required members and cycles
rejected. The command registry publishes the tags that resolve.
Part 1 of 3 for #13091 (PR #13951 is the last part).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Share only isolated-group/filter metadata; each PR keeps its own corpus and gameplay change. Preserve hopper-pot exact selection and all existing groups.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Share only isolated-group/filter metadata; each PR keeps its own corpus and gameplay change. Preserve hopper-pot exact selection and all existing groups.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The fix and its Rust tests stay here (about 350 lines). The harness,
its transcript and the VanillaProtocolClient mode follow in a stacked
pull request, as asked in review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With LOD Distance on, the Render Distance row is relabelled "Full Detail
Distance", and its value came from Horizon's own "%s chunks" key instead
of vanilla's options.chunks, so "12 Chunks" turned into "12 chunks".
Format the value with options.chunks, as the vanilla row does.
Fixes#14880
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
On Windows the HDR page's Calibrate row said "Your display's reported peak is
often too high: calibrate to set it up." That is wrong for a player who ran
Windows HDR Calibration: its profile overrides the display's own figure in the
`MaxLuminance` DXGI reports, so their good value was called too high. Horizon
can't tell which source the number has, and nothing says reported peaks are
too high in general.
The row now reads "Auto uses the peak Windows reports. Calibrate if highlights
look clipped.", true for either source. A new test keeps the row's four
descriptions within the description strip's two lines at vanilla's 427-wide
GUI, where a longer sentence would be cut off.
Nothing else changes: the setup states, the button labels and calibration are
as before. The comments and docs/graphics.md say what is known of the figure.
The old sentence was in en_us.json only.
Part of #12989.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
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.
RecipeBookComponent.keyPressed closes the book when Escape is pressed and
!isOffsetNextToMainGUI (a window narrower than NARROW, where the open book
covers the menu). Horizon's Escape arm went straight to close_container, so
pressing Escape threw away the inventory or crafting table instead of the
book.
Companions, shout range and the cat bed check must give the same ids in
the same order with and without the mob grid. Two lattices of 300 and
200 mobs cover raiders within and beyond 8 and 16 blocks, and lying and
standing cats around four beds.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
companions and shout_range walked every mob to find raiders in a box
around the patrolling raider; the cat's occupied check walked every mob
for cats lying beside a bed. Use others_near with the box each already
computes, one block wider so every accepted mob's bounds meet it. The
filters are unchanged, so results and their order stay the same.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Covers view_needed: before the advancements load the view is built as
before; after that it is built only for a trigger with a listener in
tree.by_trigger, the same lookup trigger() makes. Needs no resources.
Co-Authored-By: Claude <[email protected]>
tick_advancements built the player's EntityView every tick (and every
20 ticks for minecraft:location) even when no loaded criterion listens
to that trigger, and then dropped it. The view is now built only when
the trigger's own lookup (tree.by_trigger) finds listeners, or before
the advancements are loaded, when the tree is not read yet, so the
view is built exactly as before. load_advancements and the early outs
of trigger keep their order.
Co-Authored-By: Claude <[email protected]>
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]>
Adds a unit test in level/ticks.rs that loads, replaces and removes
moving pistons, then edits the Vec from outside the index (a direct
push and a reversed Vec), and checks that every lookup equals the old
linear scan, that tickers stay in ascending id order, and that
`moving_piston_has` accepts only the current id.
Co-Authored-By: Claude <[email protected]>
Ticking moving pistons ran an `any` scan over every moving piston for
each ticker, and the lookups, progress updates and set/remove went
through linear `find`/`retain`/`position`, so a tick with N moving
pistons cost about N^2 comparisons.
`Ticks` now keeps a position-to-id hint next to `moving_pistons`. The
hint is only trusted when the slot it leads to really holds the position
(the Vec is sorted by ascending id, so the slot is a binary search), and
it falls back to the old scan otherwise, so a Vec edited from outside
still gives the same answer. The Vec, its order, and every observable
result are unchanged.
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.
Both wrapped the command_exec driver without --work-dir, so they shared
.local/command-exec-parity with each other and with command_exec, and two
running at once read each other's scripts and worlds.
Fixes#14855
Co-Authored-By: Claude Fable 5.2 <[email protected]>
ActiveTextCollector.ClickableStyleFinder tests each glyph's own area
(BakedSheetGlyph.GlyphInstance: its top and bottom, the shadow, bold
thickness and italic shear), so a glyph a font's ascent moves off its
line is clicked where it is drawn; only empty glyphs cover the line's 9
pixels. The last area with a click event wins, not the first character
under the cursor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The End's own dragon flies above the cleanup's floor at times, and killing it started vanilla's
death sequence. bird flap sensors ran on item honey bounce's seed sync, 36 groups earlier, and
failed in more than half of the runs once the cleanup ran between them (0 of 9 without it): each
bird now gets its own sync, with entity ids clear of the world's loaded entities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assets::load leaked a new BlockRegistry (about 11 MB for vanilla) on every
call. The game calls it once, but tests call it once per fixture, and none
of it is freed. Keep the &'static contract and intern by content: equal
registries (same states in order, same defaults) share one leaked copy.
Closes#14503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Breaking a too-wide hint at its spaces could leave the button's print
alone on a line ("B", then the label under it). The print now stays with
the hint's first word, and that line is drawn smaller if it is still
wider than the panel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Summons pigs with several AbsorptionAmount values on vanilla's dedicated
server and Horizon's, saves and reloads the world, and compares
/data get entity ... AbsorptionAmount before and after the reload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
unpack() rejected a profile's textures all-or-nothing: any texture URL
not on textures.minecraft.net dropped every texture, so a head's skin
was lost whenever another texture (cape, elytra) named a disallowed URL.
Vanilla's SkinManager judges each texture per URL: disallowed ones are
omitted and the rest are kept. unpack() now skips an invalid texture
instead of rejecting the profile, so custom player heads keep their skin
on servers whose profiles carry extra textures with foreign URLs (e.g.
hub.fadecloud.com).
The head-profile cache test expected the all-or-nothing behaviour; its
cape assertion now matches vanilla's per-URL semantics, and a regression
test covers a mixed valid-skin + disallowed-cape profile.
Refs #5376
AbstractArrow reads life as the saved short, negative too, and shakeTime
as the saved byte & 255. The arrow already did; the trident's own copy
kept both unsigned, so a loaded life:-1s or shake:-1b saved back as 0.
Both are signed on the trident now, read as the arrow reads them, with
vanilla's 'if (shakeTime > 0) --shakeTime' and pickup at shakeTime <= 0.
EntityReference.getEntity looks the UUID up in the level and returns any
matching LivingEntity, the holder included, so LivingEntity.baseTick ends
such a record after 100 ticks. resolve_last_hurt_by_refs dropped a
reference equal to the mob's own UUID, leaving it unresolved and saved for
ever. Remove that exclusion and cover it with a test.
Part of #12830
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
CEM names a humanoid's armour with two models, `<mob>_outer_armor` and
`<mob>_inner_armor`, but 26.3 draws it on four layers of the model: the helmet,
the chestplate, the boots and the thinner leggings. A CEM name gave one layer
key, so no armour file reached any of them, and the 24 armour layers of the
zombie family's baby models were unreachable.
An armour model now reshapes every layer it covers: the outer one the helmet,
the chestplate and the boots, the inner one the leggings. Layers a model does
not have are dropped by the existing check against vanilla's layers, so no list
of mobs has to track the game, and a baby keeps its own file through the suffix
rule.
The rabbit's `left_foot`/`right_foot` mapped to `left_hind_foot`/
`right_hind_foot`, which 26.3 no longer has: the haunch carries the box under an
empty hind leg, so the foot is the haunch and the thigh the leg.
`mobs::cem::tests::names_reach_horizons_models` (ignored: needs the prepared
resources) passes with no gaps.
Fixes#8426🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
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]>
This reverts merge 5248e83f28 (#13953).
Since it merged (2026-11-09 15:51Z), entities in the command_exec
fixture world stop ticking: they no longer fall, die, despawn or pick
items up, and command_exec fails 159/184 (it passes at the merge's
parent, and fails at the merge, both run here). Every PR whose change
selects command_exec has failed parity since.
Vanilla's gate is right, but Horizon does not keep the ring around a
forceloaded or ticketed chunk resident the way vanilla's ticket levels
do (a FORCED ticket gives its neighbours FULL), so the residency check
holds back chunks vanilla ticks. #13852 needs that ring modelled first.
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]>
test_item_model_property and test_port_ledger imported their helpers as
tests.test_world_input_capture and tests.test_class_index, while every
other test imports siblings by module name (unittest discover puts tests/
on sys.path). The tests. form resolves to any installed top-level tests
package first, so both modules failed to import wherever one exists.
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).
Eight offers put the thumb at the track's top; a hand over it, the vertical
resize while dragged, nothing left of the thumb or past its bottom edge, and
the disabled scroller of seven offers asks for nothing.
MerchantScreen.extractScroller requests CursorTypes.POINTING_HAND over the
scroller's thumb, or RESIZE_NS while it is dragged. Horizon drew the thumb
but asked for no cursor, so the pointer stayed an arrow over it.
The Windows-only test still built `horizon_prepare::java::Jdk`, but since
92886cd964 the launcher reaches that crate only through horizon-platform's
`prepare` feature, so `cargo clippy --all-targets` in launcher/ failed on
Windows (Linux CI never compiles this file). Use
`horizon_platform::prepare::java::Jdk`, as backend.rs does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
libc declares proc_pidinfo/proc_taskinfo for macOS only, so horizon-threads
didn't compile for aarch64-apple-ios. iOS reads MACH_TASK_BASIC_INFO's
resident size instead.
Co-Authored-By: Claude Opus 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.
The mushroom cow's row said its mycelium rule lived only in the spawn check;
walk_target_value carries it now, and the enderman, the pillager and the
warden have their flat 0 beside the other walk-target rows.
walk_target_value at three positions that differ only in the block below: a
cow scores grass 10.0 and stone and mycelium by the light, a mushroom cow
mycelium 10.0 and grass by the light (never 10), a zombie the negated light,
and an enderman, a pillager and a warden 0.0 on all three. Without the two
rules the mushroom cow reads 0.5 for its mycelium, the light term, and the
three flat classes read the zombie's score.
PathfinderMob.getWalkTargetValue is what a random stroll ranks its ten
candidate positions by, and three of its class overrides had no port:
Enderman.getWalkTargetValue, Pillager's and Warden's answer 0 wherever the
position is, so their strolls pick one of the ten at random, while Horizon
scored them by the negated light and sent them to the dark. A mushroom cow's
override replaces Animal's outright — mycelium 10, everything else by the
light — so grass is worth nothing to it, while Horizon gave it the cow's grass
10 and nothing for its own mycelium.
walk_target_value moves out of ai.rs into mobs/ai/walk_target.rs with the two
rules, and ai.rs loses 75 lines of code to the move.
RangeChoiceSampler::sample_volume kept or replaced each value with a
conditional the compiler turned into one branch per value. Final density
runs it over a chunk's whole 16x384x16 volume (the noodle caves), where the
choice changes often, so the branch was mispredicted: about 4.5% of proto
generation time at one thread.
The select now runs in its own function over plain slices, on the values'
bits, which the compiler vectorizes. Each value is the one vanilla's
conditional store leaves; a test checks it bit for bit against a scalar
reference, with NaN bounds, NaN selectors and signed zeros.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MWgQD4Rbxknozw5LZ
PointedDripstoneBlock.animateTick, getFluidAboveStalactite and canFillCauldron
were unlisted on the client side: the drip they decide is
animate_tick.rs::dripstone_drip, and the 26.3 filter reads the fluid's type.
The half of `trySpawnDripParticles` that decides where a fluid drop lands had
no case: every fluid the zoo put a block over was air or stone, both full
cubes, so the branch that reads the fluid block's own face was never taken.
Two cases put a waterlogged bottom slab over a dry one - a fluid block with a
full down face and a dry half-height block under it, the arrangement the branch
exists for - at seeds 0 and 1057. The corpus goes from 2,188 cases and 462,718
lines to 2,190 and 462,960; the 2,188 cases keep their own seeds and print the
same 462,718 lines as before, byte for byte, on both sides.
Negative run, against the same corpus: the face back to `full_collision` makes
only these two cases differ, 78 lines against 67 at seed 0 and 81 against 76 at
seed 1057, both short of the dripping_water particle the client adds on the
slab's top face.
Two reads of the fluid a drip comes from were taken from the wrong accessor.
`ClientLevel.doAnimateTick` hands `trySpawnDripParticles` the fluid block's own
face, `state.isFaceSturdy(this, pos, Direction.DOWN)`, and its collision shape,
`state.getCollisionShape(this, pos).max(Y)`. Horizon passed `full_collision`,
the "collision shape is a full cube" fact, for the face: a waterlogged bottom
slab has a full down face and is not a full cube, so with the dry slab under it
the client put a dripping_water particle on that slab's top and Horizon put
none (78 lines against 67 at seed 0, 81 against 76 at seed 1057). The facts
already carry `isFaceSturdy(.., SupportType.FULL)` by direction ordinal, so the
down face is bit 0.
`PointedDripstoneBlock.canFillCauldron(Fluid)` is `fluid == Fluids.LAVA ||
fluid == Fluids.WATER`: the fluid above the root by its type. Horizon also
required it to still be a source (`fluid_level >= 8 && !fluid_falling`), which
drops the flowing fluid a stalactite drips in vanilla.
🤖 Agent: founder-imposed governance amendment of 2026-11-10 (Ludovic's go, ~09:55Z): three kinds of action, elections by tier, the permissions table checked against the live Discord, panel, ops list and GitHub, and P41 with a 24 h veto; docs only, verify green on this head. The council can overturn it by poll.
AgeableMob.canAgeUp to ai.rs::can_age_up, and Animal.mobInteract's food
branches to ai.rs::takes_food: an adult at age 0 that canFallInLove, else
canAgeUp.
A frog and a cow, both at Age -24000, through takes_food and feed_animal: the
frog refuses the food and keeps the age, the cow takes it and grows a tenth.
Without the fix the frog takes it: took_food=true ate=true age=-21600, where
the test wants false, false and -24000. The cow reads the same either way
(age -21600).
Animal.mobInteract's growing branch is `canAgeUp()`, which is
`isBaby() && !isAgeLocked()`, not `getAge() < 0`. ai::takes_food still
decided that branch from the raw age, so a frog spawned with Age at
getBabyStartAge (AgeableMob.finalizeSpawn's group baby is not gated on
canBeABaby), a camel husk (AbstractHorse's group spawn is 20 percent), or a
slime, magma cube, parrot or zombie nautilus with a negative age took the
food: it ate the item, played the eating sound and grew a tenth towards 0,
where vanilla refuses the click.
takes_food now asks can_age_up(m), the port of canAgeUp, the same predicate
the rest of the feeding code already used.
- 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
Founder-imposed amendment of 2026-11-10. Free actions need no vote; a role
with power or a body's own decision is a 24-hour body vote with a 24-hour
council veto; everything else stays a council poll (48 h only for this
document, the licence, money and org ownership). Adds the election table,
its hard cases and Kira's reminders (5.3), the permissions table checked
against the live Discord, panel, ops list and GitHub teams (2.2), and P41:
the mods decide moderation matters, council veto 24 h (section 7).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- 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
ChestBlock.isChestBlockedAt is a redstone conductor above the chest or a
cat sitting in the block above it (ChestBlock.java:318-319, the cat test
at :327-338). getAnalogOutputSignal (:346-348) reads getContainer(..,
false), whose combine(.., false) (:279) uses that predicate, so a blocked
chest combines to nothing and the comparator reads 0.
The comparator's container view (machines::Containers::view) only checked
the conductor; the cat test lived on the menu path alone. Hand the level's
mobs to the view and share one isCatSittingOnChest helper with the menu
path (chest_blocked_at). A hopper's getContainer(.., true) still ignores
the block, so hoppers and menus behave as before.
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
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
`falling_model_vertices` works out the model seed from the block's starting
position itself, so its call in the falling-block loop fits on one line and
game.rs ends up shorter than on main (docs/layers.md#file-size).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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]>
A new get_the_game hook (horizon-hooks, re-exported by horizon-platform)
answers with the page to open and the screen's texts as translation keys.
horizon-official-servers answers it with Horizon's launcher text and
horizon_addresses::WEBSITE. With no hook, the engine screen and
missing_resources say how to prepare the files and offer only Quit.
The screen's strings are now translation keys. The no_resources.rs entry
leaves tools/addresses.json's allowed list.
Closes#14479
Co-Authored-By: Claude <[email protected]>
The float frame is smooth, but the last pass rounded it to the HDR10
swapchain's 10 bits as it was. Around 90 nits one PQ code is a 1% step
in light, and a clear noon sky is two or three codes tall, so it showed
as wide flat bands with hard edges (#12989's audit, F3).
The pass into the HDR10 swapchain now adds triangular noise of one code
either way to the PQ signal, each channel's own, as the last thing
before the write: over the world and the HUD alike, after the black
level lift. A gradient shows as grain one code deep and its mean is the
light meant.
- The noise is a fixed function of the pixel (PCG's 3D integer hash):
no frame counter, state or texture, so a still picture stays still
and HORIZON_STILL needs nothing.
- Black stays black: within a code of 0 or of the top the noise shrinks
to what fits, so a signal of 0 is written as 0, one of 1 as 1023,
nothing leaves the range, and near-black isn't lifted.
In that last code the rounded mean leans to the end (a third of a
code from it shows as an eighth): the accepted price of a true 0.
- A pipeline override constant (`pq_dither`) turns it on for the HDR10
swapchain's pipeline alone. scRGB (a float swapchain), SDR, the SDR
screenshot, recordings and the screenshot's HDR10 copy (`-hdr.png`)
are as before, byte for byte; calibration's test pictures keep their
exact codes.
Refs #12989
Co-Authored-By: Claude Fable 5.2 <[email protected]>
AbstractHorse.onClimbable (horses, donkeys, mules, the undead horses, llamas,
trader llamas, camels, camel husks), Ghast.onClimbable, Phantom.onClimbable
and HappyGhast.onClimbable all return false, so LivingEntity never takes its
climbable branch for them: no 0.15 clamp and fall reset in handleOnClimbable,
no 0.2 climb against a wall in handleRelativeFrictionAndCalculateMovement, no
fall reset for a ridden mount in handleMoveVehicle, and isPushable keeps them
pushable in vines. Mob::on_climbable now answers that false, which every
caller (travel, pushEntities, the vehicle fall reset) already reads.
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
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]>
PhantomSpawner.tick asks Level.canSeeSky, which is "sky light 15 here"
(BlockAndLightGetter.canSeeSky), not a scan for a full collision cube:
glass and stained glass keep the sky light and spawn phantoms, while
stone, water and a slab roof do not. The advancement location predicate
read the same hand-rolled scan and now reads the sky light too.
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
Rule 3 told every pass that keeps a history to blend tone-weighted. Measured on Horizon's
own TAA, which accumulates the float frame's gamma, that raised the sun scene's largest
per-pixel standard deviation from 11 to 27 PQ code values. The rule now goes by the encoding
a pass accumulates: tone-weighted for linear light (a 0.2 pack's post buffers, or the frame
under linear storage), as stored for vanilla's gamma.
Rule 2 named the sharpening's limit above 1 as a fault to fix. Measured, sharpening the sun's
edge as the rest raises its flicker under upscaling by 55 to 69% for 1 to 6% more detail, so
the limit stays and the rule says why.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Step a of #14035 (layer plan M6). An external shader pipeline implements
ExternalPipeline; the renderer calls begin_frame and shadow_request on world
frames, resize and reset_history, and draws vanilla's way where it doesn't
answer. Nothing sets one yet: each point is one Option test.
ShadowView moves from pack_post (re-exported there) and ShadowCull,
shadow_box, shadow_runs and shadow_casters from gfx.rs, unchanged: the
engine's side of the shadow frame point.
Co-Authored-By: Claude Opus 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
DefaultDispenseItemBehavior.spawnItem builds the item with the
five-argument ItemEntity constructor, which draws a default motion (two
nextDouble) from the item's own random before spawnItem sets the thrown
one. Horizon passed the thrown motion straight in and skipped those draws,
so every later draw from the item's random, starting with
moveTowardsClosestSpace's escape speed, came two doubles early.
spawn_item_thrown makes the draws; the dispenser, the vault and the trial
spawner use it.
Fixes#14537
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Turtle.TurtleLayEggGoal sets the egg then posts GameEvent.BLOCK_PLACE, and its lay-egg pitch comes from level.getRandom(); TryLaySpawnOnFluidNearLand posts the same event for frogspawn. Turtle.TurtleBreedGoal.breed overrides Animal.breed, so Animal.finalizeSpawnChildFromBreeding's entity event 18 never runs for a bred turtle (Frog.spawnChildFromBreeding still calls 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
They opened HORIZON_HOME/h260 without removing it, so a second run found
the border the first had saved and its first worldborder set changed
nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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
Since the radial menu, Pad::holds_action reads the actions pad_button
records for a pressed button; the helper called pad_action directly, so
four tests read a held action as not held.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayerGameMode.useItemOn skips the block's useItemOn while sneaking
with something in either hand, so CampfireBlock.useItemOn never places the
food; the item's own use (eating) runs instead. The block use it does
consume triggers ITEM_USED_ON_BLOCK, which the campfire placement now fires
too. CampfireBlock.douse and AbstractCandleBlock.extinguish post
BLOCK_CHANGE after the extinguish sound, so water on a lit campfire or a
lit candle is heard by sculk sensors, and CandleCakeBlock.getCloneItemStack
answers a cake for the plain candle cake as well as the dyed ones.
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
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]>
living_death_message is a free function in horizon_client_lib::living_death,
so the test needs a set for that module like hunger, landing and chat.
Co-Authored-By: Claude Haiku 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.
Six rows still described behaviour the code has, and two of them cited
`Game::use_spawn_egg` in `creatures.rs`, where it no longer lives (it is
in `creatures/spawn_eggs.rs`):
- `SpawnEggItem.spawnMob`: the spawn path posts
`minecraft:entity_place` at the spawn position (the dispenser path does
too), so sculk sensors hear the egg; the path is now cited.
- `PotionContents.getAllEffects`: `brewing::contents_effects` is the
potion's effects plus the stack's `custom_effects`, and
`finish_drinking` applies them.
- `PiercingWeapon.makeHitSound`: the jab's sound plays at the target
through `audio.play`, shared with the LAN guests.
- `TooltipDisplay.withHidden`: the loot table's
`minecraft:toggle_tooltips` is applied in `loot.rs::Context::run`, next
to the `/item modify` path.
- `SlotDisplay.Composite.resolve`: `Book::stacks` flattens a composite
into its members' stacks.
Tadpole.aiStep counts the age up, and NoAI only skips serverAiStep, so a
NoAI tadpole ages, shows its age lock's particles and grows up like any
other. The NoAI branch of the mob tick now runs the aquatic aiStep parts
that NoAI doesn't skip from one place in aquatic.rs.
Fixes#14615
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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.
Fixes#14628: the five source_silence_game tests overflowed the 2 MiB
test thread in a Windows debug build (4 MiB is enough) and took the whole
horizon-client lib run down.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.teleportSomewhere calls level.gameEvent(GameEvent.TELEPORT,
oldPos, Context.of(this)) after setPos and before level event 2016. Emit
the matching MobEvent at the old block's centre so sculk sensors and
wardens hear the teleport.
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
apply_late_wind_pushes gave each entity only the first push held for it.
Harness: tools/test_wind_bursts_all_push_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.baseTick's updateFluidInteraction applies the fluids' currents to a
thrown or hurting projectile (Entity.isPushedByFluid); only arrows had it.
Harness: tools/test_projectile_fluid_current_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
moveEntityByPiston's applyEffectsFromBlocks reaches CactusBlock.entityInside,
even for a push that moves the vehicle nowhere. The pricked vehicles are
hurt once the tick's pushes are over, so the targets found first stay valid.
Harness: tools/test_piston_cactus_vehicles_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Primed TNT joins the boats and minecarts in the list of objects every
projectile, the trident included, can strike: a wind charge bursts on it, a
snowball breaks on it, an arrow or a trident turns back.
Harness: tools/test_projectile_hits_tnt_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Game::apply_spawn_egg_entity_data` moved from `creatures.rs` to
`creatures/spawn_eggs.rs`, so `EntityType.appendCustomEntityStackConfig`
and `EntityType.updateCustomEntityTag` cited a function that is not
there. The leash row's gap is gone too: the tick checks the leashed
mob's life and its player holder's life and spectator state, so a dead
or spectating holder drops the lead.
FireworkRocketItem.asProjectile builds the rocket from Entity.random, then
Projectile.spawnProjectileUsingShoot spreads the shot with that same random.
Horizon made a second random for the constructor's draws, so a dispensed or
crossbow rocket's LifeTime (and the shot's spread) were out of step.
new_rocket now takes the rocket's own random and draws the UUID, motion and
lifetime from it in vanilla's order; launch_angled_rocket makes it and hands it
to the shot. seed_sync gets a dispensed-rocket case (UUID, LifeTime, Motion).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`FireworkRocketEntity.explode` posts `GameEvent.EXPLODE` from `getOwner()` at
the rocket before the damage, and the entity event passes `!isSilent()` to
`createFireworks`, which gates only the blast and the twinkle of
`FireworkParticles.Starter`, so a silent rocket keeps its sparks and flash.
A rocket launched from a block face also counts as a use, as
`ItemStack.useOn` does for `FireworkRocketItem.useOn`'s SUCCESS.
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
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]>
The falling-block loop rebuilt every block's model vertices each frame: the
model lookup, two sprite hash lookups per face, shading and uv mapping. They
depend only on the block state and the model variant picked, so they are now
kept in model space per state and variant; each frame copies them, applies the
same translation as before and sets the block's light. The output is the same,
bit for bit.
8,100 falling sand blocks (fast profile, Windows 11, Ryzen 5 7500F, RTX 3070 Ti):
151 -> 247 fps, CPU 6.6 -> 4.0 ms per frame.
Part of #13007
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]>
preedit's state is one static for the process, and three tests change it while
cargo runs them in parallel: one's reset or should_suppress_key could land in
another, which failed at random in CI. They now hold a test-only lock.
Co-Authored-By: Claude Opus 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]>
LivingEntity.readAdditionalSaveData reads last_hurt_by_mob as a lazy
EntityReference (the UUID) and sets lastHurtByMobTimestamp to
tickCount - ticks_since_last_hurt_by_mob. getLastHurtByMob only answers once
the UUID resolves to a living entity in the level, and baseTick ends the
record when that entity is dead or more than 100 ticks old.
- Mob.ai keeps the bare UUID (last_hurt_by_ref) until the game resolves it
into last_hurt_by (a mob, or the player) before the mobs' baseTicks.
- baseTick (mobs/hurt_by.rs) ends a resolved record as vanilla does; a
reference naming nothing stays, keeps aging, and is saved as it is.
- setLastHurtByMob(who/null) clears an unresolved reference.
- The tests' world loads the 2-column neighbourhood entities need to tick.
- The corpus gains a pig naming an entity that arrives later.
Part of #12830
Co-Authored-By: Claude Sonnet 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]>
The game, entity_nbt, stations, creatures, minecarts, particles and remote modules were split this week; twenty rows
still named the file a function left.
Fixes#14568: 62e62c21e6 put the switch test between the socket test's
attribute and the test, so the socket test (std::os::unix) compiled on
Windows and the switch test ran only on unix.
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]>
ThrowableProjectile.tick and LlamaSpit.tick call Projectile.updateRotation,
which lerps pitch and yaw 20% from the tick's old rotation towards the
motion's direction (the spit with its motion before drag and gravity).
Horizon set the target angles at once.
Fixes#14544
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs is over the 3000-line limit (docs/layers.md#file-size): the
coat coin, the wolf pup's voice and the test of both move to
game/creatures/bred_draws.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Wolf.getBreedOffspring picks the pup's voice with
WolfSoundVariants.pickRandomSoundVariant from the breeding parent's random,
after the collar colour mix; Horizon left every bred pup on the classic
voice. The cow, pig, chicken, fox, cat and wolf getBreedOffspring also draw
the coat coin (which parent's variant the baby gets) from the parent's own
random, where Horizon spent a level draw for every species but the
mooshroom.
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]>
Asked by a maintainer to review open pull requests, an agent takes them
one at a time, oldest first, skipping drafts, its person's own and ones
already reviewed. It checks function, regressions, tests and scope,
reports only points with a file:line, each marked by severity, and
submits Approve or Request changes, never a plain comment. Based on the
review prompt rdbtCVS shared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SocialInteractionsScreen.mouseClicked gives super.mouseClicked the widgets
first, so the search EditBox's onClick runs on a click inside it and puts the
caret under the pointer. Horizon's social click only knew the tabs, the rows
and Done; clicking the search box moved nothing.
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]>
An agent asked for some reviews swept about 300 open pull requests, most
reviews saying it found nothing. Agents now review a pull request only
when its author or a maintainer asks for it, post only findings someone
can act on as a real review, and skip reviews that found nothing.
Maintainers' own agents may run wider passes when the maintainer asks.
Closes#14545.
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]>
MobBucketItem.spawn loads the bucket's entity data and sets fromBucket,
never PersistenceRequired. SulfurCube.loadFromBucketTag reads its own
lowercase age keys, and Tadpole.fromBucket is always true, so
requiresCustomPersistence keeps every tadpole.
Fixes#14538
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]>
PotentSulfurBlockEntity reads its column with
CollisionContext.positionContext(the height under it); the shapes were read
with no context, so scaffolding kept its full top and stopped the geyser.
Harness: tools/test_geyser_scaffolding_parity.py.
Co-Authored-By: Claude Fable 5.2 <[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]>
Sniffer.emitDiggingParticles posts GameEvent.ENTITY_ACTION at the head block every ten ticks for the whole dig, outside the dust window. Only sounds that go through Entity.playSound are silenced: the state transitions and the seed drop, not playLocalSound's searching sound or the block's hit sound. A bred pair's egg takes its push from ItemEntity's own random.
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
The generated Hooks trait takes the event only: entity, kind and message are
its fields, so the test impls take one argument.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
A runbook for the Horizon Team and the council: who can do what today, merging by hand, the Nightly
and how to stop or roll it back, what waits when the self-hosted runners are offline and what to do,
the Mojang republish issue, the mods registry (form pull requests, forks, takedowns), Kira, the
official server and security incidents. It points at release-flow.md, ci-runners.md, security.md
and GOVERNANCE.md for the details instead of repeating them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: main merged after #13958: the dial's colours now come from menu_style (page, backdrop_muted, and new lit/lit_edge that Horizon's look fills with the night accent), so the renderer keeps no horizon-theme dependency and the dial looks the same; its curves are cubic-bezier points pinned to horizon_theme by a test. split-controllers waits on this before moving controller files.
Jobs that need the maintainer's machines (bots on horizon-light, parity, parity-main, the Mac's
Nightly and launcher builds) wait up to a day when those machines are off, then fail, and nothing
says why. Every 30 minutes, on GitHub's runners, tools/ci/runner_watch.py lists the jobs of queued and
running runs that wait for a self-hosted runner; when one has waited over 2 hours in a pool where no
job runs at all, it opens one issue naming the pools, what stops and what to do, keeps it current,
and closes it once nothing waits. Only the workflow token (actions: read, issues: write).
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]>
🤖 Agent: ANGRY_AT's UUID and long TTL load back (clamped, not wrapped, past i32), and the test loads a whole saved piglin; the new head only merges the test-fix commit the branch already held (same tree as da800dff).
PotentSulfurBlockEntity.LAUNCH_ENTITY_TICKER pushes every entity in the
column; only mobs, items and primed TNT were.
Harness: tools/test_geyser_launch_parity.py.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Railway's builds of both services fail since horizon-telemetry gained horizon-addresses (and the
platform crates) and horizon-mods gained horizon-release-sign: the Dockerfiles copy a fixed list of
crates, so cargo can't find the new path dependencies. The old deployments keep running, so nothing
looked broken. Copy every path crate of each service's own Cargo.lock, watch them in railway.json,
and test both lists against the lockfiles so the next new dependency fails verify instead.
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]>
Fixes#14493: since #13953 an entity ticks only once every chunk within 2
columns is loaded; with only chunk (0, 0) the pig never fell.
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]>
🤖 Agent: the explosion/ignite/burn branches now attribute their game events to the cause in place and ignition clears TicksFrozen; damage_immunity checks the stand's active slots first. Reworked so no dead copy remains; 15/15 stand tests pass with resources.
🤖 Agent: the Discord switch keeps its discordPresence line in horizon-extras.txt through horizon-discord's settings store, so players keep their choice while the row moves to Extras > Multiplayer; e2e 4/4 and verify green on this head.
🤖 Agent: step 2b-ii of spec #14091 gives PlayerDraw motion, view, looks, particles and first_person so the cosmetics need nothing else from the engine; stacked on #14274.
#13958 (with #14027) took horizon-theme out of horizon-client-render: the
dial's dark page and muted text now come from menu_style's overlay colours,
and OverlayColours gains lit and lit_edge for its sector, which Horizon's
look fills with the night accent and its bright shade, so the dial looks as
before. The easing curves are written as cubic-bezier points like hud_kit's,
and a test pins them to horizon_theme's QUICK and MOVE.
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
Vanilla ticks entities one after the other, so a charge never meets a boat or
minecart that an earlier charge already broke. The tick's hits are all found
first here, so such a charge ticks again without the broken vehicle.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
A piston's push is Entity.move(MoverType.PISTON): the drop adds to the
falling block's fall, and landing runs fallOn and posts HIT_GROUND in
that step. Review finding on #14440.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WallHangingSignBlock.shouldTryToChainAnotherHangingSign lets the hanging sign
in hand chain onto the sign only when the side the player faces cannot execute
its click commands (SignBlockEntity.canExecuteClickCommands), so a waxed sign
with a run_command hands the click to SignBlock.useWithoutItem, which runs it
and returns SUCCESS_SERVER.
The client now answers that for the wall case as it already did for the
ceiling one (chains_hanging_sign: isHittingEditableSide by the sign's FACING
axis), so the click runs the command, plays the waxed-sign failure and places
nothing; the world sim's unconditional Pass for the wall edge is gone.
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
`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
The horse armour layer took its texture from the item id
(golden_horse_armor -> horse_body/golden), but the golden armour's
equippable asset_id is minecraft:gold, whose horse_body texture is
entity/equipment/horse_body/gold. The layer found no texture and drew
nothing. horse_armor_asset maps each horse armour to its equipment
asset; a test checks every vanilla horse armour against items.json and
equipment/*.json.
Fixes#13826
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
NearestLivingEntitySensor.doTick finds the living entities whose boxes meet the
baby's box inflated by its FOLLOW_RANGE (16). BabyFollowAdult took every adult
others_near returned, which only narrows to grid cells and, when the grid falls
back to a scan, returns every mob: the baby checked line of sight to, and could
follow, adults anywhere in the level. Found by kacpherek's verify mode in #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]>
AbstractWindCharge.onHitEntity hurts what it hit by one point before the
burst; a vehicle takes it through VehicleEntity.hurtServer and breaks on the
fifth hit of a tick. wind_charge_damage only looked among the mobs.
Harness: tools/test_wind_charge_vehicle_parity.py.
Co-Authored-By: Claude Fable 5.2 <[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]>
The oracle cache's shim started the real java with the PATH it got, its own folder first. A java
wrapper that looks java up on PATH (a JDK selector, a build wrapper) then found the shim again,
which found the wrapper again: every round left a python3 alive under Popen, past 5,000 on one
machine, until RAM and swap were full.
real() skips candidates that resolve to the shim; the real program gets PARITY_SHIM_REAL_PATH as
its PATH; and past 4 rounds inside itself (PARITY_SHIM_DEPTH) the shim exits with a reason.
Tests use a wrapper java that stops itself after a few rounds, so a looping shim fails them
rather than the machine: without the fix all three fail, with it they pass.
Fixes#14450
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
Part of #14440: PrimedTnt and FallingBlockEntity move with Entity.move and
keep its checkFallDamage, which posts HIT_GROUND after the block's fallOn
when they land after a fall (a piston's push too, for TNT). Extends the
`entity landing sensors` command-exec group.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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
`ItemUtils.onContainerDestroyed` makes each stack a destroyed item held
with `new ItemEntity(level, x, y, z, stack)`, whose motion is 0.2 up and
0.2 less 0.1 sideways from the *item's* own random
(ItemEntity.java:61-66). The spill drew three doubles from the level
random instead, so the stacks barely rose and the level's stream
advanced three times per stack. Route it through `spawn_item_own`,
which already draws the four-argument constructor's motion (#13938).
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
piston_push.rs: main's version (#13655 made the same correction to the order test's expected push).
Co-Authored-By: Claude Fable 5.2 <[email protected]>
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]>
Join a Friend opens from vanilla's Friends icon slot, unchanged in place
and size at every GUI scale; it is now drawn inactive, and takes no click,
key or controller press, when no Hooks::tunnel resolver is registered
(net::joins_friends), as the pause menu's icon is without a sharing page.
horizon-official registers horizon-friends' resolver, so players of the
official game see no change; the plain executable, which refuses every
friend: address, no longer opens a screen that cannot join.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client sent the server's recipes (horizon:recipe_content) numbered its
recipe book displays from its own files, while the server numbers them over
its recipe set, a world's data pack recipes included. Each pack recipe moved
the server's numbers past the client's, and the server's book entries, put
over the book by number, replaced other recipes with server:N entries.
The display numbers now come from the synced recipe set itself
(RecipeIndex::load_from_sources), for the book, its toasts and the
RecipeBookAdd toasts; without synced recipes nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
SulfurCube.getEquipmentSlotForItem answers BODY for every
#sulfur_cube_swallowable item, so the body's SlotAccess takes it. Horizon
checked only the item's own equippable slot and refused every block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main.rs is over 3000 lines and may not grow (docs/layers.md#file-size): the menu's
open flag, scroll and the window height it was opened at become one PickerState in
glass_home.rs, replacing picker_open.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`resolveForItem`, `resolve`, `markResolved`, `resolvePage` and
`isPageTooLarge` cite `WrittenBook::resolve` and `resolve_held_book`.
The spawn egg rows cited `Game::use_spawn_egg` in `creatures.rs`, where
it no longer is: the function lives in `creatures/spawn_eggs.rs`.
The resolve test rewrites a selector page and a score page (a plain page
is left alone), marks the book resolved and then leaves a resolved book
alone; the over-long test keeps the raw pages past 32767 characters and
still marks the book resolved; the codec test round-trips `resolved`.
`WrittenBookContent.resolveForItem` runs in `ServerPlayer.openItemGui`:
`resolve` replaces every page's selectors and scores with the text they
name and marks the book resolved, and a page whose resolved text passes
`resolvePage`'s 32767 characters cancels the resolution (the book is only
marked resolved).
Horizon kept the page components (`WrittenBook.page_tags`) but never
resolved them and had no resolved flag, so a page holding a selector
showed its raw component text and a book could not be told as resolved.
`WrittenBook` gains `resolved` (read and written by the component codec),
`WrittenBook::resolve` ports the resolution, and `Game::resolve_held_book`
resolves the book in the acting hand when `use_book` opens it (a LAN
guest's copy is the mirrored inventory, so its slot is sent back).
`set_written_book_pages` was already applied in `loot.rs`.
Dropping a particle with physics at its first tick still drew it for one
frame, so a falling leaf flickered with the newest wide copy through it.
The check now runs when a pending particle joins the engine list, and
set_polytone moves unchanged to particles/polytone.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_hooks::lang::register(&'static [(code, json)]), re-exported as
horizon_platform::lang. The game's entry installs the registered files
into horizon_client::lang; Language::load reads them in the horizon
namespace right after the engine's bundled file for the same language,
before packs, and the engine's file wins on a key both have. Regional
variants share one alias table with the bundled files (fr_ca -> fr_fr,
de_ch -> de_de without ß). Nothing registered: languages load as before.
tools/lang_coverage.py reports crates/*/assets/lang beside the game's
folder; a crate's review record is tools/lang_review/<crate>/, and
--mark-reviewed puts each key in the folder that holds it. lang.rs's
file checks run on every crate folder, and a crate key the engine also
has fails them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only the rows on screen are widgets; the Multiplayer group is below the fold
of an offscreen window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OldMinecartBehavior's AbstractMinecart.move calls
applyEffectsFromBlocks(position(), position()) straight after the move,
while the cart is still at the rail's top height, and again at the end of
the tick. The port swept once at the settled position, so a cart held by a
fence on a slope never reached the pressure plate above it again.
The row said the four lid-step calls were counted and dropped;
`Behaviours::shulker_neighbor_updates` now runs them, and `tick_shulkers`
calls it once per counted update.
`a_shulker_lid_updates_the_shapes_and_redstone_of_its_neighbours`
observes the level as `shulker_neighbor_updates` runs: each of the six
neighbours gets one `neighborChanged` from the shulker's block, with its
own state as the changed state. Without the call the level sees none.
`ShulkerBoxBlockEntity.tick` calls `doNeighborUpdates` when its lid
starts or finishes opening or closing: the box's collision shape moved,
so the neighbours' shapes are updated (flag 3) and their
`neighborChanged` runs. Horizon's render tick counted those four calls
(`ShulkerTick.neighbor_updates`) and the game dropped them, so redstone
beside a moving shulker box never saw the update.
`Behaviours::shulker_neighbor_updates` ports the two calls, and
`tick_shulkers` runs it once per counted update. The box's collision
shape for the same ticks already reaches the world through
`shulker_collision_state`.
hooks::seat() is on main now: other players' settings neither load nor
save the line, and their Extras page has no row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Version menu capped its height to the room under the button and its
rows scroll (with the launcher's scrollbar and take_wheel); Manage
versions stays pinned below them.
Fixes#14407
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ClientPacketListener.handleRespawn gives the new LocalPlayer
AttributeMap.assignAllValues of the old one when the packet keeps the
attribute modifiers (every level change), assignBaseValues after a death.
The new player here started empty, so the worn armour's modifiers and any
custom one were gone after an End or Nether portal until re-equipped.
Fixes#13541
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The loot data is one slot per process. A test that installed a world pack's
armor_stand table warmed that slot, and every other test's stand break rolled
it too. load_thread_loot_data keeps such a test's loot data on its own thread,
ahead of the slot; a game never sets it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every flag wrote its log line from the tick thread, piece by piece. With 40 load-test bots
(which fail the movement prediction every tick: 76,000 lines in 3.5 minutes) that was
lan_guests 7.7 ms a tick; a lagging player's flood costs the same. The lines now go, in order,
to a writer thread: lan_guests 1.9 ms on the same run.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#14318: a rocket's Entity.fallDistance builds up as it flies down and is
saved and loaded. Entity.baseTick's fluid step resets it in water (and
splashes on entry, and lets currents push the rocket); lava halves it.
Entity.move's long-move ray resets it past a fall-resetting block or water,
checkFallDamage adds the drop and posts HIT_GROUND on landing, and a
cobweb or powder snow makes it stuck. Adds the `rocket fall distance`
command-exec group.
Items now splash with their motion before the water's current pushes it,
as Entity.updateFluidInteraction does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A custom particle with has_physics whose box starts inside a block's
collision is dropped before its first tick, so its emitters never run.
Falling leaves start 0.05 under their leaf block, and a pack's leaf with a
0.08 hitbox reaches into it: in Java such a leaf never falls, while here
every one fell, and a leaf that emits a resting copy of itself each tick
left trails of them under every tree until the frame rate collapsed.
Compared in game against Java 26.3 with Polytone, with particles of its
own: a box overlapping a block by 0.01 goes, 0.01 clear of it lives, and
a particle without physics lives either way; the emitters do run every
tick. A test covers the three cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round the suggestion list scroll offset instead of truncating f64->i32.
Fractional touchpad/PixelDelta wheel deltas were dropped in one
direction, so command hints scrolled down but not up.
Fixes#14338
Co-Authored-By: Claude <[email protected]>
AbstractSelectionList.extractRenderState draws its scrollbar at scrollBarX
(getRowRight + scrollbarWidth + 2) whenever the rows overflow. Horizon clipped
and scrolled the rows but drew no bar, so a long list gave no sign of its
position.
WorldOptionsScreen.updatePortControlsState makes the port box read-only
without LAN (setEditable) but leaves it focusable: clicking it selects and
copies the port. Horizon dropped the focus instead, so with LAN off the port
could not even be selected or copied.
CreativeModeInventoryScreen.checkTabHovering shows a tab's display name when
the cursor is on its icon (3 pixels in from the 26x32 slot). Horizon drew the
tabs without the tooltip, so a tab's name only appeared in a search result's
tooltip.
Each vehicle walked every mob of the level several times a tick:
- Its box queries (`cart_entity_collisions`, `boat_obstacles`,
`boat_contacts`) now go through `mobs_meeting`, which reads the mobs
near the box from a grid of the mobs' boxes. The first query of a
stretch of vehicle ticks builds it, and the boats' grid serves the
carts ticking right after them.
- Mob pushes on carts ask that grid once per cart when the mobs
outnumber the carts, instead of asking the carts' grid once per mob.
- Who rides a vehicle (`boat_mobs`, asked by every boat, and
`cart_is_vehicle`, asked by every cart on rails) comes from one walk
per stretch.
The grid and the riders hold until the mob list's next write (its
`ListStamp`): after one, the walk answers until it has cost about as
much as a new grid. With native mods loaded the walks stay. The mobs
found and their order are those of the walk. Refs #14321.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
remove_entity moved to game/remote_removal.rs, Minecart::to_nbt/from_nbt to
game/minecarts/nbt.rs. A class row takes home, not port.
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]>
Use the existing nightly and reusable parity runners instead of a second
manual inventory. Keep the JFR checks and describe their JDK fixtures.
All 84 removed harness commands remain in parity discovery.
Co-Authored-By: Codex (GPT-6) <[email protected]>
ForcedCover and the Ranges methods that read the forced chunks (is_forced,
forced_chunks, and in_main_range past the player's circle) go to
streaming/forced_cover.rs, so streaming.rs doesn't grow. A test checks that the
distance map answers every margin as the scan of every forced chunk does, as the
forced chunks change, and that past MOST no map is made.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
in_main_range went through every forced chunk for each column it was asked about, several times
a tick. While update runs (nothing can change the forced chunks then, as for the extra areas'
cover), a map of each column's distance to the nearest forced chunk answers instead; it is made
again when they change. The same answers. Streaming on the bench: 6.2 ms a tick to 4.7.
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
AdvancementDisplay, Advancement, UpdateAdvancements and
SeenAdvancements, with their codecs, move byte for byte into
packets/play/advancements.rs. play.rs re-exports the four public items,
so every `packets::play::…` path still resolves.
The port map follows: the two rows that name UpdateAdvancements::read
and SeenAdvancements::write point at the new file.
Part of #13063.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Downloader` kept an entry in `states` for every texture URL asked for and
started a thread (with its own HTTP agent) for each, and an entry in
`resources` for every `texture` id of a profile, none ever dropped. A tab list
of 200 players started about 400 threads at once.
At most 16 downloads now run at once: past that `state` answers `Pending`
without recording the key, and the caller, which asks every frame, starts it
when one has finished. `states` and `resources` keep at most 4096 entries each,
dropping the oldest settled one first (a running download is never dropped); a
dropped texture loads again from the disk cache or the pack. A download thread
that panics before answering now answers "failed" through a drop guard, so its
place is given back and the texture is retried after the wait. The fetch
function and the bounds are fields so a test can set them.
Part of #9370.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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
The crate keeps its switch: a settings store for its discordPresence line of
horizon-extras.txt (players keep their choice) and a row in the Extras page's
Multiplayer group, which horizon-official registers. The engine loses
Extras::discord_presence, Item::DiscordPresence and the Horizon Settings
button; the hooks lose Activity::Hidden.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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
The radial series' holds_action follows what a press went to, not the binding,
so it takes no settings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It reused the crown's 18-pixel attach offset, which an aura may not have
(at most 6 pixels above the feet), so the item failed to parse.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's input methods (#14001) replace the pad calls: the radial menu closes with
a new player in input_player_replaced (through pad_player_replaced), and
horizon-ctl's key press still closes it beside input_keyboard_used.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Vehicles: the mobs' BoxReach is worked out by the first box query of a
stretch of vehicle ticks, not before it, so a level without boats walks
no mobs for it. Mob pushes on carts work out the box only of the mobs the
carts' grid puts near a cart.
- Entity portals: the sweep walk stops at once when no loaded section has
a portal state in its palette, and the pass before it no longer works
out every mob's size to learn that mobs use portals.
Behaviour is unchanged: each skip turns away only what the full test
would have turned away. Refs #14221.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pack screen is a pointer screen: a pad moves a crosshair, which
glides onto the nearest control and hops between controls with the
D-pad. A card's options button registered no target, so the crosshair
never settled on it and the D-pad couldn't reach it; only aiming the
stick by hand opened Pack Options. It registers one now, clipped to the
list's view like its rows.
pad_menu_e2e: pack_card_options, with a fixture pack that has Polytone
config entries, hops onto the button with the D-pad, opens Pack Options
with A and comes back with B.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server started by a harness's build joined that harness's job (sccache
doesn't ask to break away) and ended with it, failing other harnesses'
in-flight compiles (os error 10054). The runner now starts it before any
build, in a job of its own, with no idle timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemFrame.readAdditionalSaveData compares the Item stack as read with
ItemStack.matches, count included, and removes the framed map on a
mismatch; only setItem then keeps one item. apply_entity_nbt compared
against item_frame_from_nbt's stack, already cut to one, so a count-only
change kept the old map's marker. It now compares the stack as read.
Fixes#14178
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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
A pure move (docs/layers.md#file-size): section_instance, chunk_visibility, fade_millis, section_visibility,
FadeInputs and CHUNK_VISIBILITY_THRESHOLD, unchanged but for pub(super). gfx.rs re-imports them, so callers keep
their paths. It makes room for #13780 step 2's few lines of ray tracing glue.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain calls from each stage main and global declarations, including
referenced overloads and prototypes. Blank unreachable function spans
without moving diagnostic lines. Cover the public translation APIs and
post loader with independent positive and negative GLSL regressions.
Co-Authored-By: Codex (GPT-6) <[email protected]>
r.noise read a single simplex noise of seed 0, which is octave 0 of
Polytone's noise: the mod builds 1.18-1.21's PerlinSimplexNoise (gone
from 26.3) with octaves {1}, whose noise comes from a random seeded by
the first noise's value at its own offsets, read at twice the input with
the noise offsets. So grass and leaves a pack tints with the noise took
other shades than on Java.
Measured on the real mod with a probe pack whose grass colormap encodes
the noise (column the coarse value, row the fine part, calibrated with
ramps): r.noise(x/8, z/8) at 1527 blocks matches this construction to
the decoding's 0.016, and so do polytone/noises definitions with seed 0
or 1 and octaves {0}, {1} and {0, 1} (their weights a third and two
thirds). SimplexNoise gains get2_f64/get3_f64: the mod sums and seeds in
double.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ParticleEngine::create moved to particles/spawn.rs, and the anvil's
50-character unbordered rename box is built by stations/anvil.rs's
rename_box; both rows named fns map-check couldn't find.
Part of #14327
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#12820, the part kept open there: the player's own model. An
offline-mode server names the player by nameUUIDFromBytes of their name (a
version 3 UUID), which the cosmetics service doesn't know, so their own
cosmetics, name tag style and Horizon's mark were missing. own_uuid now asks
by the signed-in account's UUID for such a profile, as In single player does
for the window's own server; an online profile is kept, and signed out the
profile is kept as before.
No new setting: an offline server behaves as an online one, under My
cosmetics. Other players are still asked by the UUID the server gives them,
since on an offline server anyone can take any name. The service receives
the account's UUID only, no name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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
Thirteen rows named a fn at the file it was split out of (poll_remote,
tick_world_icon, potion_cloud_to_nbt, the spawn egg fns, the hanging
and trident loaders), so map-check reported "no fn". They now name the
file that holds each fn.
Part of #14327
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game::mob_geometry moved out of game/creatures.rs; 16 rows in
client-003/004/005/007 and client2-002 still named the old file, so
map-check reported "no fn". They now name the new file.
Part of #14327
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
The registry's own workspace still resolved rusqlite 0.37 ->
libsqlite3-sys 0.35, which bundles a SQLite older than 3.54
(RUSTSEC-2026-0459: uninitialised page-cache memory from a crafted
database opened with a "-wal" file of another page size). The registry
only opens its own database, so this isn't reachable as deployed, but
it is the same upgrade the telemetry server and the root workspace
already made: rusqlite 0.41 resolves libsqlite3-sys 0.39.1, and
fallible_uint keeps the u64/usize conversions working as before (an
error past i64::MAX).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
`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
The duplicate check in enqueue stops a double click from filing twice
only once the first issue is recorded. While the first request still
waited on GitHub (up to 20 s) the row's issue was empty, so a second
identical request found the row, got no issue back and filed a second
issue for it; the 10-minute upkeep (file_pending) could likewise file a
report a request was filing at that moment.
Store::claim moves a report from 'new' to 'filing' for exactly one
caller, and Store::release gives it back when GitHub refuses. report()
and file_pending() both claim first; a request that loses the claim is
answered 202 with no issue yet, as when GitHub fails. A report left
'filing' by a server that stopped mid-filing goes back to 'new' when it
starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
bind_icons gave every icon drawn in a frame a slot in drawing order and pushed
its 128 by 128 RGBA pixels (64 KiB) every frame, into an atlas that keeps what
it is given. The screen now keeps which slot holds which icon from one frame to
the next: an icon is sent when it takes a slot, the slot of the icon drawn
longest ago is taken when all are in use, changed pixels under the same key are
sent into the same slot, and a new atlas (another revision) empties the table.
Icons past the 64 slots in a single frame still keep unknown_pack, as before.
Part of #9370.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Fixes#3823, as decided there. Fullbright was already off by default; on a
connection that plays no save of the window's own (Remote::save_folder is
None: a multiplayer server, or another player's LAN world) the renderer now
draws the normal lightmap and the graphics screen hides the row. Single
player, a LAN host, a splitscreen seat in P1's world and Replay Studio keep
it, and the saved choice is never changed.
The flag is applied with the settings and again each tick, since joining a
server sets the connection after the settings were applied. The row reads
Settings::fullbright_held, which each game sets for itself (not saved), so a
splitscreen seat does not share it through a process-wide flag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text::push (MutableComponent.append) cloned the whole JSON component
built so far on every append, then added one sibling, so N siblings
cost N^2. to_text_with pushes once per "extra" entry, and a server's
chat, title or server-list MOTD can carry hundreds of thousands: 20 000
siblings (80 KB) took 76 s on the packet-handling thread.
push now takes the tree out of its Arc and appends in place
(Arc::make_mut, so a Text cloned earlier keeps its own tree). A styled
text is still wrapped once first, including one built from a single
styled span with no saved tree, so its style doesn't reach what is
appended. The JSON produced is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
LivingEntity.updateFallFlying starts with checkFallDistanceAccumulation:
a glide whose vertical speed is above -0.5 keeps at most 1 of its fall.
Horizon's update_fall_flying left it out, so a gliding mob kept its whole
fall. The glide code moves to mobs/ai_glide.rs, keeping ai.rs from
growing.
Fixes#13868
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#14317. A local --base origin/main read the lists at main's tip, so entries main dropped after the branch left it counted as the branch's additions. check_file_size.py already compares with the merge base; CI's HEAD^1 is its own merge base.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A translatable component a server sends ({"translate", "fallback",
"with"}) falls back to the server's own template when the language has
no such key, and format_translation copied the argument's spans for
every %1$s in it. With the argument being another such component the
copies multiply: 300 references nested three deep is a 4 KB server-list
MOTD or chat message that becomes 27 million spans, and the client ran
out of memory. Vanilla's TranslatableContents.decompose keeps references
to its arguments, so its cost stays linear.
format_translation now shows the template verbatim, as it already does
for a malformed one, when the expansion would pass 32768 spans or 4 MiB
of text. Real translations are nowhere near either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
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]>
ScoreHolder.getFeedbackDisplayName replaces the display name's hover
with ShowText of the scoreboard name (an entity's UUID, a player's
name); /team and /scoreboard replies name holders with it. Horizon used
the plain display name. PlayerTeam's displayNameStyle also inserts the
team's name, which Horizon's formatted team name lacked.
test_mob_display_name_parity.py compares the /team and /scoreboard
replies too (10/10; without this commit 5 differ).
Fixes#14279
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::despawn_check on a cod, a salmon and an axolotl 200 blocks from the
player: each with a custom name stays and each without one is removed, and a
zombie's name saves it nothing. Without the name half of removeWhenFarAway the
named cod is discarded like the unnamed one.
AbstractFish.removeWhenFarAway and Axolotl.removeWhenFarAway answer
`!fromBucket() && !hasCustomName()`, so a cod a player has named stays put
however far it swims, and one let out of a bucket is persistent before it.
Only the bucket half was ported: from_bucket is in
requires_custom_persistence, but check_despawn's removable flag knew nothing
of the name, so a name tag bought a fish nothing and it was discarded past the
128-block despawn distance like any other water mob.
named_keeps_it answers the name half for the fish and the axolotl, and
removable consults it beside the feline rule: a named cod, salmon, tropical
fish, pufferfish, tadpole or axolotl is never removed for distance, and no
other mob's name is persistence.
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]>
Windows' libm rounds log differently from Java's x86-64 intrinsic for about
0.1% of arguments (299 in 200,000 polar-method radii, #5064 point 1), and
Gaussian draws, density functions and a few block behaviours call it. The
platform's ln is replaced there by exact::log, in the module that already does
this for sin and cos: x split into a power of two and a mantissa in
[sqrt(1/2), sqrt(2)), ln m as 2 atanh((m - 1) / (m + 1)) in double-double with
a quick path checked against an error bound, and the full series with its
terms summed exactly where the bound leaves the rounding open.
exact::log is correctly rounded. HotSpot's stub is not quite (one ulp off for
about 0.002% of arguments), so this matches the JDK on all but those. On
374,043 arguments exact::log was never off from a 70-digit reference; Java's
Math.log was 7 times and the platform ln 331 times. The tests pin ten
arguments where Windows' ln and the JDK differ, the seven where HotSpot
itself is one ulp off, 28 arguments within 2^-65 of a half-way point, the
edges, and that both paths agree.
Part of #5064.
Co-Authored-By: Claude Sonnet 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
vehicle fall clip (#13005) and boat float height (#13022) waited for
their own forced chunk only. Since #13953 an entity ticks once every chunk
within 2 columns is loaded (ChunkMap.prepareEntityTickingChunk), so their
boats and minecarts fell only when an earlier group had left the
neighbours loaded: boat float height failed all 59 reads on main, and
vehicle fall clip failed whenever a group was added before it. Both now
wait for the 5x5 ring, as the other ticking groups do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the read-only helpers native_vehicle_of(entity) and
native_passengers_of(entity) for server mods (vanilla Entity.getVehicle
and getPassengers, first = controlling passenger). Helper-only fragment
riding_getters.json; API number is assigned when the fragment is stamped.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Entity.getDisplayName adds a HoverEvent.ShowEntity (type, UUID, name)
and the UUID string as insertion to the team-formatted name. Horizon's
command replies named a mob with the team formatting only, and /summon
without NBT with a bare type name.
mob_command_name (commands/display_names.rs) builds it; target_name's
mob arm and /summon use it. New harness test_mob_display_name_parity.py
(4/4; main: 0/4).
Fixes#14220
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Reuse thread-local scratch queues (`RelightScratch`) across BFS light propagation passes, avoiding reallocation during incremental relighting.
- Pass borrowed slices `&[Pos]` to `relight_layer` to avoid redundant `Vec<Pos>` heap allocations.
- Cache consecutive vertical column lookups in `lt_sources` to avoid redundant map lookups.
- Preserve lazy loading of sections in `Region`, only loading and cloning sections that are actually read or written.
- Add `relight_single_block_bench` and update `relight_bench` to verify timings for both single-block and large batch edits.
#14115 moved send_particles and create out of particles.rs into spawn.rs,
and this branch had added `mod simple;` on the same line. Both modules stay.
cargo test -p horizon-client-render --lib particles: 59 passed, including
parallel_tick_matches_serial_tick.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
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]>
Six #[ignore]d tests that need prepared vanilla data had gone stale or never
ran (#14261):
- two datapack fixtures lacked the pack section #11576 requires;
- the piston order test expected a push of 0.5, but #12231 gave primed TNT
vanilla's float box (half width 0.98F / 2, widened);
- the end gateway age test ran /setblock at permission 0 without the command
registries;
- index_on_vanilla_tree and the_humanoid_mesh_bakes_as_vanillas_zombie read
HORIZON_RESOURCES and returned early without it.
Fixes#14261
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflict: particles/tick_bench.rs. Main (c221aa9ec9) adds `cached_chunk` to the
`ParticleLevel` literal of the old benchmark body; this branch had rewritten
that body into `with_scene` and `measure`. Kept this branch's structure and
added the field to the one literal.
That commit also makes the merge fail to build, which no textual conflict
shows: `ParticleLevel::cached_chunk` is a `Cell`, so a `ParticleLevel` is no
longer `Sync`, and `tick_all` shared `&ParticleLevel` between its scoped
threads (E0277). Each worker thread now takes a view of the same world with an
empty one-chunk cache of its own (`thread_view`, in simple.rs, so particles.rs
does not grow), built on the engine thread and moved into the thread. The
thread that runs the first share keeps the caller's level and its cache. A
cache shared between threads could not have held "the last chunk looked up"
for any one of them anyway.
cargo test -p horizon-client-render --lib particles: 59 passed, including
parallel_tick_matches_serial_tick. fmt, clippy on the particle files and
tools/check_file_size.py --base origin/main are clean.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
Mods can read whether a dedicated server runs in online mode, the port it
listens on and whether it takes its players from a Velocity proxy
(forwarding.secret). The values come from the live network config and
listening socket, so they match what the server does. None off a dedicated
server and before it listens. The forwarding secret is never returned.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Identical still textures share one layer (d874278161), and connected-texture rules find their
faces by layer. A rule matching one of them by name (matchTiles, connectTiles) then also took the
faces of the others. Since chained tiles resolve (#13707), a resource pack's rule over its own
transparent tile 19 (`matchTiles=./19.png`, drawing wheat stages) reached every grass block's top
layer, a placeholder with the same pixels, and put plants on the grass.
The rules are read before the models insert their textures, and every name they match keeps a
layer of its own (Sprites::keep_apart). Identical textures no rule names still share.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep one acknowledgement per completed batch while optionally requesting
64 chunks per tick. Preserve vanilla estimation by default, record the
selected mode, and cover the framed receive path in both modes.
Co-Authored-By: Codex (GPT-6) <[email protected]>
`mob_from_nbt` needs the entity's `id` and `Pos`; the fixture only had the
brain, so the test unwrapped `None`. Add both to the compound and to the
never-expiring variant.
🤖 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.
`ObserverBlock.updateShape` reacts only when the direction to the changed neighbour is its
FACING. A change behind it, beside it, above it or below it starts no pulse.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`ObserverBlock.updateShape` schedules a tick two out when the block it faces changes; that tick
turns it on and schedules another two out, which turns it off. A lamp behind it is lit by the
pulse (`getSignal` answers 15 only for its facing); lamps in front of it, beside it and above it
are not.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`TripWireHookBlock.calculateState` scans `for i in 1..42` along the hook's facing for a hook
facing back and ends with `attached &= receiverPos > 1`: hooks attach 2 to 41 blocks apart and
never side by side or 42 apart. Both hooks are placed the way a player places them, because
vanilla runs the calculation from `setPlacedBy`, not from `setBlock`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonBaseBlock.isPushable` ends in `!state.hasBlockEntity()`, so a chest, furnace, barrel or
hopper ahead of a powered piston keeps it retracted and stays where it is.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonBaseBlock.isPushable` answers false for a piston with EXTENDED set, so a push that meets
it fails and the pusher stays retracted, while a retracted one is pushed along. Both kinds.
The extended piston is held extended by a power source of its own, out of the pusher's reach:
unpowered it retracts on the tick, and that retraction updates the pusher, which then pushes it
(vanilla does the same; the first draft of this test asserted the wrong end state).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
26.3 gives `PushReaction.PUSH` to glazed terracotta alone, and `PistonBaseBlock.isPushable`
answers `direction == connectionDirection` for it: a slime block beside it cannot drag it, where
it drags an ordinary block, and in line with the push it moves like any other.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonStructureResolver.canStickToEachOther`: a slime block drags slime and a honey block drags
honey, but a slime block and a honey block never stick to each other. Four pairings, each with
the block beside the one the piston faces either dragged or left where it was.
Built in open air: a slime block on stone drags the floor along too (vanilla's as well), which
would make the check about something else.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonStructureResolver.addBlockLine` refuses once `toPush` would hold
more than twelve blocks, and then the whole push fails: the piston stays
retracted, it does not move twelve of them. Pins 11 and 12 moving, with
the head where the first block stood, and 13 moving none.
The port matched vanilla; the ledger lists `PistonStructureResolver.resolve`
among the ported methods no parity harness covers.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
The five tests from `pistons_pop_fragile_blocks_and_refuse_obsidian` to
`replacing_a_moving_piston_leaves_its_extended_base` move, unchanged, to
level/tests/pistons.rs, beside the barrel and trigger suites that were split
out the same way. They are the piston tests that stood in the middle of a
4176-line file; the piston rules the next commits pin go in the same place,
so one file holds them. This takes 191 lines out of tests.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
TagValueInput.read returns the codec error's partial value, and DFU's list
codec keeps the valid elements as that partial (checked against
datafixerupper 10.0.21): vanilla keeps the other layers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test loaded a compound with no id or Pos, so mob_from_nbt returned None
and it could not pass; it now saves the id, a position and the brain, and
checks a TTL past the counter's range too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.onEffectUpdated(effect, true) runs when a hidden effect
instance is promoted, and its refreshDirtyAttributes clamps absorption
to the new max_absorption. Horizon clamped only when Absorption was
gone, so Absorption II's 8 survived the promotion of a hidden
Absorption I (max 4). tick_effects now clamps whenever the tick changed
max_absorption.
mob_absorption_max_corpus gains a promote case (494/494; main: 493/494).
Part of #11384
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main rewrote the UI Brightness part of the HDR paragraph; this PR only added
the sentence that a shaders/ pack's picture is not lifted. Main's paragraph is
kept with that sentence inserted where the PR put it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`OldMinecartBehavior.pushAndPickupEntities` asks
`EntitySelector.pushableBy(this.minecart)` for what a moving rideable cart
pushes, so the cart's own team's collision rule and the target's both decide;
Horizon passed `None` for the cart's team on mobs and never filtered carts.
A still or unrideable cart keeps vanilla's else branch, which pushes every
pushable minecart with no team filter.
The per-candidate answer comes from `Game::cart_push_allowed` beside the
other team lookups; `pickup_from_cart` gets the cart's team from the same
`team_with_owner`. Two ignored tests on the real server game fixture cover
every collision rule over cart-to-cart pushes, the stationary branch, and a
cow the cart may not board.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Main already attributes the hit branch and has entity_event_by. The
explosion, ignite and burn branches now take the cause in place, so the
separate refused function (which left them dead) is gone; causeDamage keeps
one name. The frost-walker test no longer shadows game() (it did not compile).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dying cube mob is out of every selector's reach and splits 20 ticks later, inside the next group, so
the three groups that end by killing their size-2+ sulfur cubes shrink them first. Horizon resolves an
@s of another level only from that level, so each level's cleanup runs in it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
insert_text auto-merged main's read-only guard with this PR's UTF-16 room;
main's read-only test and this PR's UTF-16 test were appended at the same
place, both kept whole.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tracer read each command from the subprocess.Popen audit event, which on Windows carries the command line
Popen built, not the list: no harness's cargo build was recognised, so the probe prebuild built nothing and probes'
Rust sources never became dependencies. It now splits the line back (CommandLineToArgvW), and dep_info finds an
.exe's .d file.
Cargo on Windows names a program in deps/ without the hash of its features, so a harness building horizon-client
with another selection (-p horizon-client-lib --bins) relinked the file a running harness's game was using:
"Access is denied", and every later build of the common selection failed too. On Windows a harness now waits while
a running harness built one of its programs differently.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A harness that exited or crashed left the rest of its process group running, and on Windows a parity.py or
pr_ready.py that died left its harnesses and their games running too (taskkill /T can't follow a dead parent).
tools/process_tree.py starts each harness in its own Job Object on Windows (KILL_ON_JOB_CLOSE, started suspended
so nothing escapes before it joins) and, on POSIX, kills the rest of its session's group once it exits, before
it is reaped. pr_ready.py runs its Windows checks the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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
ExpirableValue's ttl is a long; narrowing it with `as i32` turned a TTL
above i32::MAX into a negative count, so the piglin loaded calm.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ServerLevel.tick` runs `raids.tick` before `getChunkSource().tick`, so a
wave that lands in a tick joins before that tick's natural and custom
spawns and takes the lower entity ids. `tick_creatures` did the opposite:
natural spawning, then the custom spawners, then the raids.
The three phases move into `Game::tick_spawners`, in vanilla's order, each
with its own span; the LAN sync bracket still covers exactly them. A new
ignored test builds the real server game fixture, records spans and asserts
the phase order.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Enderman.aiStep runs NeutralMob.updatePersistentAnger(level, true): holding
a target names it (angry_at) and keeps 20 to 39 seconds of anger running,
so an enderman that lost its player (a daylight teleport, a reload) stays
angry, and EndermanLookForPlayerGoal takes back a player it is angry at on
sight, without a new stare (isAngerInducing). The goal's continue
conditions (forCombat().ignoreLineOfSight()) have no range, so a held
target is no longer dropped past the 64-block follow range.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
MaceItem.hurtEnemy resets the attacker's fall and sets its motion, and the
server sends that motion to the attacker in a ClientboundSetEntityMotionPacket
right after the hit. A LAN guest's PvP mace smash now sends the motion the
hit gave it, as it already did for mob hits. The server_knockback harness no
longer lists pvp-mace as a known difference.
Fixes#8246
`tools/port_map/game-012.tsv` records `Sheep.readAdditionalSaveData`; its
`Color` read now goes through `mobs/farm.rs::saved_color`, which gives white
for an id the legacy codec does not know.
`mob_from_nbt` is what `Game::load_mob` goes through. A sheep with
`Color: 99b` loads with `extra.color == 0` (white), not the wrapped 3.
Without the codec bound the sheep loads light blue: `left: 3`, `right: 0`.
`Sheep.readAdditionalSaveData` reads `Color` with
`DyeColor.LEGACY_ID_CODEC` and `orElse(DEFAULT_COLOR)`. The codec's id map is
`ByIdMap.continuous(..., OutOfBoundsStrategy.ZERO)`, so an id outside 0..=15
reads as the first colour, white.
Horizon masked the id with `& 15`: a saved `Color: 99b` became light blue (3)
instead of white, and a negative id became a wrapped colour the sheep never
had.
The read now accepts only 0..=15 and gives white for anything else, in
`mobs/farm.rs::saved_color` beside the sheep's other colour code.
`tools/port_map/game2-001.tsv` recorded `EnderDragon.readAdditionalSaveData`
with a gap: `sitting_damage_received` was not read. The row now says
`Dragon::from_nbt` reads it with `getFloatOr`, 0 when absent.
`Dragon::to_nbt`/`Dragon::from_nbt` are the pair the dragon's chunk entity
uses. A perched dragon with `sitting_damage = 12.5` saves
`sitting_damage_received: 12.5f` and `from_nbt` gives 12.5 back. A compound
with no such key loads 0.0.
Without the read the reloaded dragon has 0.0: `left: 0.0`, `right: 12.5`.
`EnderDragon.addAdditionalSaveData` writes `sitting_damage_received` (the
damage a perched dragon has taken since it sat down, which makes it take off
at a quarter of its health), and `readAdditionalSaveData` reads it back with
`getFloatOr("sitting_damage_received", 0)`.
Horizon wrote the field in `Dragon::to_nbt` but `Dragon::from_nbt` ignored it:
a perched dragon saved after some damage but before taking off reloaded with
`sitting_damage` 0 and had to be hit for the whole quarter again.
`Dragon::from_nbt` now reads the float, 0 when absent, as vanilla does. The
health read is folded into one line so the over-3000-line file does not grow.
`tools/port_map/game2-004.tsv` recorded `Piglin.readAdditionalSaveData` as
reading the baby flag and inventory; it now also names the brain's
`minecraft:angry_at` UUID and TTL read by `mobs/piglin.rs::read_guarded_anger`.
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A compound whose `Brain.memories."minecraft:angry_at"`
has the UUID `[90, 91, 92, 93]` and `ttl: 99L` loads a piglin with
`extra.anger == 99` and `guarded_angry_at == Some(Some([90, 91, 92, 93]))`.
A memory without a TTL (the codec's never-expires form) loads the longest
counter, `i32::MAX`.
Without the read both fields stay at their defaults: `left: 0`, `right: 99`
for the counter and `left: None`, `right: Some(Some([90, 91, 92, 93]))` for
the UUID.
`PiglinAi` sets `ANGRY_AT` with an expiry when a player breaks a guarded block
near a piglin, and `Brain.Packed` saves every memory with a codec under
`Brain.memories`: the value is the player's UUID and the TTL is the ticks left.
`LivingEntity.readAdditionalSaveData` reads that brain back.
Horizon wrote `minecraft:angry_at` in `mob_to_nbt` but `mob_from_nbt` never read
it: a piglin that a player had angered unloaded with its chunk and came back
with `extra.anger` 0 and no actor UUID, so it stopped attacking and the guarded
event was forgotten.
The load now reads the memory back: a four-int UUID into
`extra.piglin.guarded_angry_at` and the `ttl` into the existing `extra.anger`
counter, with no TTL read as the longest counter there is. The writer moves to
`mobs/piglin.rs` beside it, which also keeps `entity_nbt.rs` from growing.
`guest_dig_feedback` sends `(int)(progress * 10)`, the stage
`incrementDestroyProgress` computes, and `tick_daylight_detector` gates on
the dimension type's sky light, so the End is excluded like the Nether.
Both rows still described the old gaps.
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
Groups share one world, so an entity a group left stayed visible to later groups' unbounded selectors.
The bell group's persistent zombie burned in daylight and died at a random tick in each engine, which
made the entities group's PersistenceRequired check flaky. The harness now kills, between two groups,
the non-player entities above the groups' floor (y 100 in the overworld, 128 elsewhere), shrinking
cube mobs first and killing twice so drops go too. World-generated entities below stay out of it.
The wolf anger clock diagnostic continues the group before it and is named in CONTINUES_PREVIOUS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
run_command checks InventoryChangeTrigger after every command; vanilla's
/loot give calls no broadcastChanges, so its Diamonds! comes on the
player's next tick, outside the command's record (inline_loot_reference).
/item replace on a player marks what its own check announced
(cmds.broadcast_announced); the harness records those and leaves the
rest for the tick.
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]>
tick_simple, Simple, sprite_from_age_in, tick_all, finish_simple and the thread
settings move unchanged into a child module; particles.rs keeps the calls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
totem_death_effects goes to mobs/living.rs (LivingEntity.checkTotemDeathProtection), and
RandomPos.moveUpToAboveSolid to mobs/random_pos.rs. Pure moves; no behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M6. horizon-server-files gets chat_guard (the ChatGuard trait, Place, Action, Mute, and an
install/load registry). horizon-chat-filter implements the trait and its loader; entry installs it
at startup. Behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.getSecondsToDisableBlocking reads
Weapon.disableBlockingForSeconds. Horizon read the prototype's value, so a
patched weapon disabled a shield for 0 seconds instead of its own.
Test: a diamond sword with disable_blocking_for_seconds 1.5 reports 1.5.
ItemStack.postHurtEnemy uses Weapon.itemDamagePerAttack from the stack's
component. Horizon read the item prototype's weapon damage, so a patched
weapon wore 0 or the prototype's amount instead of its own.
Test: a diamond sword with item_damage_per_attack 3 wears 3 per hit.
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 radial menu's overlay_holds_view gate moves with debug_crosshair_drawn into
game/hud_render.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BannerPatternLayers.CODEC reads its layer list all or nothing: one layer the
codec rejects leaves the empty layer list. Horizon skipped only the bad layer,
so a banner kept the layers before it.
The game workspace pinned winit to an unreleased backport in a personal
fork (crmne/winit) for native Wayland file drops. Every player build would
then depend on a repository outside winit's own, which the project doesn't
take. The drop import itself works wherever winit already delivers
DroppedFile (Windows, macOS, X11, XWayland); native Wayland follows once a
winit 0.30 release carries rust-windowing/winit#4729.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The snapshot carried the panic state but Breed never read it:
getFreePartner skips a panicking partner and canContinueToUse ends the
goal when the partner panics. The test only skipped on CI (no prepared
resources), so it looked green without the check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A run- folder per invocation under .local/<harness>/ is never removed by runner-gc (it only
looks one level down), and these four harnesses aren't this fix's. Main's harness fixes own them.
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]>
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]>
Layer plan M6, step toward dropping horizon-client-lib -> horizon-anticheat. The record
(horizon_anticheat::prediction) is what the engine's movement replay (lan::prediction) plays again:
it moves to lan/prediction/record.rs unchanged. The guest holds it and the client's sprint
(was Player.record / Player.sprinting). Behaviour unchanged.
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]>
goals.rs is over the 3000-line limit and may not grow: the two Breed
goal test modules move to their own files, and the range check uses
Mob::distance_sqr, so goals.rs ends shorter than on main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
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]>
HoglinAi.updateActivity runs after the brain, so a hoglin given an attack
target enters the fight activity at the end of that tick and its
MeleeAttack hits from the next one: the goal now acts only in the fight
activity (brain_step runs after the goals). MeleeAttack.create(40) sets
ATTACK_COOLING_DOWN for 40 brain ticks and MemorySlot.tick erases it on
the 41st, so hits come 41 ticks apart (16 for a baby).
A hit one mob's tick deals another is handled after the mob loop, after
the target's own tick; vanilla deals it inside the attacker's tick, so a
target later in the list counts HurtTime and its damage cooldown down
once more that tick. The pig hit by a hoglin read HurtTime 10 on the tick
of the hit; vanilla's reads 9.
hoglin_repeat_attack reports its rand- rows instead of comparing them:
each hit is attack / 2 + level.getRandom().nextInt(attack), and
Level.random is RandomSource.create(); two vanilla runs disagree on the
pigs' Health.
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.
CountDownCooldownTicks keeps ITEM_PICKUP_COOLDOWN_TICKS while it counts
down and erases it the tick after it reached 0 (stop), and
Allay.isOnPickupCooldown is the memory's presence: an allay loaded with
{value:30} saves {value:0} on its 31st tick, then nothing. ints[3] now
holds the value plus one (allay::pickup_cooldown).
EntityItemAccessor calls containerMenu.broadcastChanges for a player
whose slots /item replace changed, so InventoryChangeTrigger (here the
Diamonds! announcement) comes before the command's message. The command
harness now announces what a command earned before its feedback, as a
guest's command does (announce_guest_advancements); it left the
announcement for the next tick, outside the command's record.
allay_pickup_cooldown reports its rand- rows instead of comparing them:
the natural allay's flight starts at its first item scan
(Sensor.randomlyDelayStart with the allay's own random, which is
RandomSource.create()), and two vanilla runs of the script disagree on
those 30 rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZombieVillager.defineSynchedData picks the profession with
VILLAGER_PROFESSION.getRandom(this.random), and Entity.random is
RandomSource.create(): the first two subjects' profession:none check
passed 1 time in 15 on either side, so 'zombie villager summoned data'
matched 9/10 from run to run. The third subject's profession comes from
its save and stays checked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its name matches no harness pattern; test_command_exec_parity.py --only
"item count predicate" already compares the group with vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It needs --work-dir and --reference-dir, which an automatic parity run
doesn't pass, so without the named_only entry it fails at startup on
every pull request that selects it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AttributeMap.pack lists the instances in the iteration order of an
Object2ObjectOpenHashMap keyed by Holder.Reference, which keeps
Object.hashCode: an identity hash. The same script run under another
main class saves the pig's list in another order, so the oracle's
order is the JVM's, not the game's. All 11 differences on main were
that order. The driver gets unordered_attributes(), which sorts the
entries of every attributes list in a record and keeps their text and
styles; the harness compares through it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With HDR on and a shaders/ (OptiFine/Iris format) pack running, the last
pass read the alpha the pack's final program leaves in the frame as
Horizon's light-source marks and lifted those pixels above paper white.
A pack that leaves alpha below 0.75 had most of its picture brightened as
if it were torches or lava, and HDR Lighting turned that on and off.
Horizon doesn't light the world under such a pack (HDR lighting is
already off there), so the lift is now off too: the pack is shown at
paper white, as a 0.2 pack is. The decision moves into a small pure
function in gfx/output.rs with a unit test. HDR off doesn't change.
Fixes#13807.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.checkBelowWorld runs in AbstractBoat.tick (through Entity.baseTick,
after the fire) and in AbstractMinecart.tick (after its hurt and damage wear
off), before either moves: under the level's floor minus 64, onBelowWorld
discards it. Horizon had no such check for vehicles, so a boat or minecart
that fell out of the world kept falling for ever (#8024 measured one at
y -243 where vanilla had removed it by tick 33).
The discard ejects the mob riders where they are and spills a chest boat's or
container minecart's slots (AbstractChestBoat.remove,
AbstractMinecartContainer.remove), with no ENTITY_DIE and no vehicle item.
The logic lives in game/boats/below_world.rs; minecarts.rs keeps its size.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A projectile added keeps an earlier UUID for its id (a client copy's from the
spawn packet) through or_insert again, so the client copy needs no reset.
Part of #12561
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack made before 1.21.5 ships entity/pig/pig.png, cow.png,
red_mooshroom.png, brown_mooshroom.png and entity/chicken.png in the old
64x32 layout, and its .jem files measure their UVs on them. 26.3 draws
the renamed pig_temperate (64x64) through those UVs: misplaced,
see-through panels. Cold pigs, climate cows and mooshrooms kept
vanilla's textures.
When the packs have an old texture, the adult and its climate variants
draw the adult's model, reshaped by the adult's .jem when it has no
texture of its own, with the vanilla parts' UVs in the old layout (faces
past its bottom, the 1.21.5 cow's muzzle, are dropped). The renderer maps
the 26.3 name (pig_cold, mooshroom_brown) to the old one through
CemLayer::texture_for, so random textures follow it. A variant with a
.jem of its own is left alone.
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]>
OptiFine's CEM animation docs: "this" is the current custom model, "part"
the original one. A replacing entry's `this` wrote the vanilla part's
pose instead. Conquest Reforged's pig sets this.tx/ty/tz to its bone's
translate, which flung the head 7 px off the body.
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]>
The file-size check (docs/layers.md#file-size) does not let a file over
3000 lines grow. Breaking a block entity and dropping its resources both
read the custom name and lock from the world, so a helper does it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The file-size check (docs/layers.md#file-size) does not let a file over
3000 lines grow. The count bounds matcher moves to commands/item_count.rs
and the ignored test module to game/item_count_predicate_tests.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The controller guide offers Equip only when Equippable.swapWithEquipmentSlot
applies: the stack's slot is an armour slot and swappable is true. Horizon
read the item prototype for both.
Test: a helmet patched swappable:0b no longer offers Equip, and its patched
slot is used.
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]>
When drawing held items for non-local entities in held_item_geometry,
set fishing_cast_with_this_item from the remote hooks' active owner
state and the holding hand (with the main hand taking precedence when
both hold fishing rods), matching FishingRodCast and FishingHook's
client owner tracking.
The two native text field replacement tests pinned the old character count:
a maxLength of 4 kept four code points and one of 3 kept three. EditBox's
maxLength is Java String.length(), so the emoji takes two of them: 4 units
keep "a😀b" and 3 keep "a😀", which is what vanilla's setValue truncation
leaves.
Minecraft.pause asks the screen Minecraft set. A dialog is a DialogScreen of
its own: isPauseScreen returns its CommonDialogData.pause, true by default, so
a server's dialog freezes a single-player world unless the dialog says
otherwise. The warning above it is a ConfirmScreen (true) and the waiting
screen answers false. Horizon parsed `pause` and never read it, so every dialog
kept the world running. A lectern's book is LecternScreen, whose isPauseScreen
overrides BookViewScreen's true to false: reading on a lectern leaves the world
ticking, reading the same book in hand does not. Horizon paused for both.
pauses_game now asks the dialog, then the book, then the screen, the way each
of them is its own screen over the last.
EditBox.maxLength caps Java `String.length()`: UTF-16 code units, so a
character outside the BMP takes two of them. Horizon measured the limit in
Rust chars (code points) in both edit box ports, so a chat box with a maximum
of 256 accepted 256 emoji where vanilla accepts 128, and a pasted emoji could
take a place the next character should have been refused.
`ui::EditBox` (the screens' field) and `screen_draw::TextField` (the chat box
and the native screen API's field) now measure the maximum in UTF-16 units and
keep whole characters, which is the prefix vanilla's insertText keeps after
its half-surrogate guard drops the split one.
Main's renderer output for the graphics screen and HDR calibration moves with
shell_mut and shell_events into options_ui/shell_pages.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's output owner (#13233) decides each renderer's frame in gfx/output.rs.
Whether the frame holds linear light is now part of that decision:
`FrameRequest::linear_blend` carries the picked 0.2 pack's `[frame] blend`, and
`resolve_frame` sets `FrameConfiguration::linear_light` where the frame is the
float one. The renderer reads it from its output instead of a field of its own,
so a pack that changes its blend asks for another frame and the game makes the
renderer again through main's frame check, without a separate outcome key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under a 0.2 pack with `[frame] blend = "linear"`, two draws still mixed or wrote
vanilla's gamma values into the linear frame:
- A section fading in mixed the pack's material result, linear light, toward
the gamma fog colour, so new chunks faded in too bright and unlike "gamma".
The fade now mixes in gamma, as the fog does.
- A resource pack's OptiFine custom sky layers, drawn when the pack has no sky
stage, wrote gamma, so that sky looked washed out. They now store their
colour decoded. Multiply and overlay layers convert their factor so the
product stays what it is under "gamma". Add and subtract add light, as
"linear" means to; dodge, burn and screen come out close to their gamma
results.
Under "gamma", and without a 0.2 pack, nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read these structure pieces from saved starts instead of regenerating their
layouts and losing placed chest or spawner state. Follow StructureStart.loadStaticStart,
PiecesContainer.load and the MineshaftPieces, NetherFortressPieces and
StrongholdPieces tag constructors in vanilla 26.3.
Keep whole-start regeneration when a required piece cannot decode. Preserve
stronghold Source flags and vanilla numeric and optional entrance defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
`AbstractHorse.handleEating` and `Camel.handleEating` play the eating sound
only when the mount is not Silent, at its feet, with a pitch from the mount's
own random, and then post `GameEvent.EAT` from the mount. A growing baby's
HAPPY_VILLAGER position draws three doubles from that random first (the
server's `Level.addParticle` shows nothing; the client draws its own).
Horizon played the sound for Silent mounts too, at the body's centre with
the level's random, and posted no game event, so sculk sensors and wardens
never heard a mount eat.
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]>
Match BreedGoal.getFreePartner and TargetingConditions.test by rejecting
partners beyond the spherical range while keeping the existing box query
and nearest-first ordering. Keep partners exactly eight blocks away.
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]>
`Breeze.emitGroundParticles` and `emitJumpTrailParticles` take
`getInBlockState`, else `getBlockStateOn` (`Entity.getOnPos(1.0E-5F)`), which
keeps the supporting block under the feet. Fences, walls and fence gates
collide 1.5 blocks high, so the cell 0.2 below a breeze standing on one is air
and no dust came out. The ground dust also skips invisible blocks as vanilla
does. The crumbs now go through `ClientLevel.addParticle`, so each particle's
own generator (`Particle.random`) makes its draws and the client level's random
is left alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
A hoglin within reach of a #hoglin_repellents block keeps its PACIFIED
memory for 200 ticks, and Hoglin.canFallInLove is false while it is set.
Animal.mobInteract then refuses the crimson fungus: nothing is eaten, no
love mode, no baby. Its baby branch asks canAgeUp instead, so a pacified
baby still grows up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
The examples that make a renderer of their own were still passing a bool where
`Renderer::new` now takes the picked pack's `[frame] blend`; they pass `None`,
no pack, so they draw as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guard before creating fixtures because Cargo runs unit tests from their package directory. Validate the integration test by invoking its built executable directly from an isolated game directory.
Co-Authored-By: OpenAI Codex <[email protected]>
Fixes#11698. Route resource-pack drops through the vanilla confirmation, copy ZIPs and folders without overwriting existing packs, and refresh the available list while preserving pending selections. Keep Create World's data-pack route and cover the UI, copy safety, and both event callbacks.
Co-Authored-By: OpenAI Codex <[email protected]>
A 0.2 pack with `[frame] blend = "linear"` was refused: the game showed the
"isn't served yet" message and drew without it. Now it draws. Its float frame
holds linear light, so its glass, water and smoke blend as light does, while
solid surfaces look as under "gamma".
The pack's material, sky and weather results are stored as they are. Horizon's
own draws into the frame (entities, particles, clouds, weather, the sky, stars,
sun and moon, the world border, outlines and the debug lines, world warp and
the visibility buffer) decode their colour at the end of their shaders. The
clear colour is decoded too. Block breaking keeps darkening as vanilla's, and
the material's fog still mixes in gamma. What a stage reads (scene_color,
opaque_color, the water mirror) is copied instead of decoded, so [post],
[tonemap] and everything after the tonemap (the hand, camera overlays, HUD,
screenshots, HDR output) see the same values under either blend.
Where the device can't draw a float frame, the pack draws on the 8-bit frame
in gamma instead of being refused. Resource packs' core sky, stars and line
shaders write gamma, so under "linear" Horizon's own draw in their place.
Changing a pack's blend now makes the frame again, like changing its API line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Entity makes its UUID from the first two draws of its own random
(Mth.createInsecureUUID), and Projectile.shoot spreads the shot with that same
random, from which an arrow keeps drawing (AbstractArrow.setBaseDamageFromMob,
its later rolls). Horizon shot dispensed, crossbow and mob projectiles with a
fresh random that never drew the UUID, player throws with the level's random,
gave an arrow's later draws a random of their own, and gave projectiles a
clock-seeded UUID when first asked.
An arrow now makes its own random in Projectile::new and draws its UUID first;
another kind makes it at the shot (shoot_own), the same stream since nothing
draws between. Every shot goes through shoot_own, the arrow keeps the random,
and the skeleton's base damage comes from it. add_projectile keeps the UUID; a
player's trident takes its shot's. Loaded and summoned projectiles take their
own random's UUID unless the tag has one.
The seed sync proof reads a dispensed arrow's and snowball's UUID and Motion
and a summoned arrow's UUID: 25/30 on main, 30/30 with this.
Part of #12561
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the winit 0.30.13 backport from rust-windowing/winit#4729, pinned to its reviewed revision. Document the Apache-2.0 dependency and removal condition.
Co-Authored-By: OpenAI Codex <[email protected]>
Screen::choose, Hosted::clicks_step, Shell::focus_soon and
changed_elsewhere, the binding row's chosen text, Capture::Pad on any
shell screen, and where a shell screen is hosted (options_ui/shell_pages.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What vanilla draws in the middle of the view (the crosshairs, the chat's
lines, the toasts, the hands) gives way to an overlay through one query,
so the next overlay adds its condition there rather than to each piece.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved hand_geometry and first_person_arm to game/frame_geometry.rs:
the hands still come from hands_shown there, so an overlay that holds
the view hides them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Which shell each page opens, what a shell's outputs do, and the radial
menu's name entry and rows move out of options_ui.rs as they are, so the
file doesn't grow. pad_clicks_step now also sets Shaders' shell back,
which main put on the shell since.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move: the tests module leaves settings_shell.rs as it is, so the
file is under 3000 lines (docs/layers.md#file-size) and the radial
editor's additions to the shell don't grow a file over the limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main put Shaders on the settings shell and made Screen::undo return
outputs: the radial screens' sheets use Sheet::new and their undo returns
none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/hud_room.rs answers whether an overlay holds the view (the radial
menu, once it shows), and the 2D and F3 crosshairs, the chat, the toasts
and the hands ask it. The four toast draws go into draw_every_toast and
the hands into hands_shown there; draw_pad_over_hud draws the menu and
then the pad's cursor. The respawn no longer closes the menu itself: the
new player's first position does, through the one pad_player_replaced
call in remote.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RadialMode, RadialStick, the opener's thumb rule, each stick's session
opener, can_bind, set_radial_stick and their tests move out of
gamepad.rs as they are, so gamepad.rs stays under 3000 lines. The paths
under horizon_client_input::gamepad stay the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/pad_capture.rs moves to game/pad/capture.rs as it is, its items
visible to the gameplay module as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Controller Settings > Bindings > Radial Menu... opens the pages on
main's settings shell (radial_editor.rs): a sheet per page with its
eight slots by direction, then Rename Page, New Page and Delete Page,
then Reset All Pages and the opener's row.
- A press on a slot opens the list of every action and Empty for it,
which names the slot, marks what it holds and opens on it; Right and
Left still step it (`Screen::choose`, and a controller's Left and
Right count as steps, `pad_clicks_step`). Back from the list, the
focus is on the slot again (`Shell::focus_soon`).
- Rename opens a small page with a name field, Done and Cancel.
- Delete and Reset act at once; Undo puts the pages and the opener back
as the screen found them. At most 6 pages, at least one.
- The opener's row is a `Value::Binding` on `Capture::Pad`: it waits in
place for a button, refuses the ones under the selecting thumb and B,
and its strip says when no button opens the menu or another action
shares it.
- Controller Settings' own Undo leaves the pages to this screen.
e2e: radial_editor, and Settings Pages reaches the new page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/pad_menu_e2e.py gains 28 radial_* cases, each a real offscreen game
driven through horizon-ctl's virtual pad: commits, retention, pages,
cancels (B, Start, a disconnect, focus loss, death with immediate
respawn), the camera and the walking stick, menus and the on-screen
keyboard keeping the opener, held sneak and drop, D-pad rolls after a
commit or a cancel, RT/keys/clicks fighting back, Toggle mode, the left
stick and Either, a bumper opener, haptics, narration, unavailable and
missing actions, a file from before the menu, Nintendo glyphs and the
status rows. A case can seed the controller file and pass extra launch
arguments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Controller Settings > General gains a Radial Menu group on main's
settings shell: Radial Menu (Hold, the default, or Toggle) and Radial
Menu Selects With (Right Stick, Left Stick or Either), each describing
its value in the strip, and stepping with Left and Right as every shell
choice does. Stepping the stick goes through `set_radial_stick`, so an
opener under the thumb that now selects is let go, and stepping back
gives it again. e2e: radial_stick_back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PadSettings gains the radial pages, `radialMode` and `radialStick`, and
the opener's binding, `pad.radial` on D-pad Right; nothing in play reads
the opener yet (it routes as a held button until the menu does).
- `bindLayout:2`: once, for a file from before the opener, a button the
player bound by hand keeps it, and the opener goes unbound rather than
sharing it. A file from before the binds list's defaults binds none.
- An opener under the thumb that selects, or on B, can't be bound
(`can_bind`), and a file with one reads as never saved.
- `set_radial_stick`: a stick that takes the opener's thumb unbinds it;
stepping back gives each stick the opener it had this session, else
D-pad Right where it's free.
- Lines this game doesn't read (a newer Horizon's settings, an action it
doesn't have, a button it doesn't know) are kept as they were, and a
newer `bindLayout` is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The radial menu in play (game/pad/radial_play.rs): hold the opener and
the selecting stick points at one of eight slots; letting go runs the
action once, through the same path as its button. Bumpers turn the
pages. `pad.radial` now routes to it, and the dial draws over play with
the game's icon painter and the frame's motion clock.
- Hold or Toggle, and the right, left or either stick, from the file.
- B, Start, a disconnect, focus loss, a new player (death with
immediate respawn, the End's exit) or a screen cancels; RT/LT, any key
or mouse click cancels and goes on. A flick between polls counts (the
pads' peaks, splitscreen's router included). Holds from before the
open keep working; a held drop restarts its wait.
- A D-pad roll off the opener reaches nothing for 100 ms after the later
of the close and the opener's release.
- What can't run is dimmed: Emote without cosmetics, Quick Actions
without a dialog; Social in single player shows vanilla's "not
available" message, as its key now does.
- While it shows: the chat lines, toasts, crosshair (F3's too), guide
and first-person hand give way; the status rows are kept clear.
- Haptics on reaching a runnable slot, a page turn and a release that
ran nothing; narration of the page and a settled slot.
- horizon-ctl: `pad focus true|false` sends a real focus event.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
radial.rs gains what the menu's pages hold: `RadialAction` (actions that
do everything on their press; Horizon's own namespaced, vanilla's by
their key mapping, old ids read through an alias table), `Slot` (an
action, Empty, or a Missing id kept as it was, which never runs), named
`Page`s of a kind (`Fixed` slots, or `Hotbar`, read and kept), and the
default General and Camera presets, which hold only actions with no
button of their own.
`Pages::parse` and `serialize`: the `radial_<n>_*` lines of
horizon-controller.txt. A page the player hasn't changed is saved as its
preset, so it follows the defaults; `radialLayout` is read and a newer
one kept; unknown fields, kinds and slot ids past the eighth survive a
save; up to 16 pages are read and the menu turns through 6. Edits:
set_slot, rename, add, remove.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub names the selection on a dark disc: the action, and for
Perspective, Hide HUD and Cinematic Camera its state and what a press
turns it to; for a slot that can't run, why; for a missing action, its
id; while a stick must be let go first, "Let go of the stick". The large
icon goes above the name when the hub has room, and its text stays off
every tile (`fit::fit_in_circle`). Toggle tiles get state chips where
there's room for them.
Along the top, the pages are a `tabs::tab_strip` between the bumpers'
glyphs; collapsed, the bumpers name the pages they turn to. With a
bumper for the opener only the other shows. B's Cancel sits on a plate
at the lower right when the view is roomy, in the player's glyphs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client-input's radial.rs: `RadialMenu`, what the opener, the
selecting stick and the bumpers do while the menu is open, knowing pages
only by their count and slots by their index. Eight slots clockwise from
the top; a release commits once; 250 ms of neutral retention; a stick
leant since before the press waits to be let go (`StickHistory`), while a
flick that starts with the press selects; tilts that follow the
deadzone (`Tilt`); toggle mode's latch, with A running the slot settled
on (`A_SETTLE`); `DpadSettle` for a thumb rolling off the opener; and
`Settle` for narration. Every tuning value is at the top of the file.
gamepad.rs: `RadialMode` and `RadialStick`, `opener_refused` (the opener
is never under a thumb that selects, nor on B) and the radial haptics.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
radial_overlay.rs draws the radial menu over play, in the look the
mockups settled on (`docs/ui-kit.md`'s pieces): a dark vignette, the
ring's track with the selected 45° sector lit, and eight tiles on true
45° rays, each vanilla's game mode switcher slot with its action's icon
from the player's own resources. An empty slot is a pip; one that can't
run is dimmed and slashed; a missing action shows its mark. The sizes
follow the view's height (`Layout`, the scaling rule in its docs), and
the ring keeps below the boss bars and above the status rows over the
hotbar (`Keep::StatusRows`, which the HUD's room gains). Its motion
(`Look`) is quiet and never selects: a fade and scale in, the sector
easing to a new slot, the tiles sliding in on a page turn, a commit's
flash and a fade out.
Nothing draws it yet. The hub, the page tabs and the Cancel hint come
next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/pad_capture.rs: `Game::pad_capture` says which overlay over play
owns the controller now, in one priority order (the on-screen keyboard,
then a prompt over the world). The keyboard checks in pad.rs,
pad_actions.rs, pad_menu.rs and the gameplay guide ask
`pad_keyboard_captures()`. Behaviour is unchanged; the radial menu adds
its place in the order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A game answers its control socket before it has drawn a frame: `ui`, `click`, `set`,
`screen` and `click-button`, which read the last drawn frame, now wait for the first one
(UiWait), as `ui`, `click` and `set` already wait for a frame after a click.
- The ui scenario expects Create New World's Game tab, the manage scenario a widget the
Horizon options page still has (ai-agent), and the options scenario takes GUI scale 2 as
half the GUI of scale 1, so display scaling other than 100 % passes.
- The no-ai check honours HORIZON_E2E_HEADLESS like the other scenarios.
Fixes#12972
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pausing the integrated server for the copy ran its pause save (IntegratedServer.tickServer's
saveEverything) during the copy, often after the call that saved had returned. That save
renames level.dat and the player files into place and hands region files to the region
writer, so the copy failed with "The system cannot find the file specified", or could read
a region file mid-write. Now the server thread saves, then waits on its own thread until the
copy is done; the pause flag stays as it was.
Fixes#12977
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ParticleEngine::tick ran every live particle one after another. Most of them (flames,
smoke, poofs, dust, crits and a firework spark's own motion) read and write only their
own state, the world and the sprite sets, so tick_simple now ticks them on up to four
scoped threads once there are 2048 particles. A spark's trail child, which takes the next
engine seed, and every other behaviour still run on the engine thread afterwards, in the
order they had, so the particles, the engine's random and the sounds come out as before.
On the stress_particles mix (isolated benchmark, particles/tick_bench.rs): whole-scene
engine tick 1.81 ms on one thread, 1.40 / 1.27 / 1.18 / 1.15 ms on 2 / 3 / 4 / 6 threads.
parallel_tick_matches_serial_tick runs both over 90 ticks with rockets and compares every
particle, the engine's random and the sounds.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`BreedGoal.getFreePartner` drops a panicking partner and
`BreedGoal.canContinueToUse` requires `!partner.isPanicking()`. Horizon's
`Breed` neither snapshotted the panic state nor tested it, so a pair kept
breeding after one of them started panicking.
With a `shaders/` pack, sections uploaded their solid quads as four
`TerrainVertex`es each instead of one 24-byte record (`mesher::packed`): the
pack's terrain and shadow programs read vertex attributes. At render distance
64 that doubled the section arena (5.9 GB of vertices against 2.9 GB) and
BSL and Photon flights stalled under memory pressure.
- Flat pack draws now pull every input from the arena page with a GLSL port of
terrain.wgsl's `section_corner` (`pack_atlas::PULL_FLAT`): position, UV,
layer, light and colour bit for bit (the colour as the Unorm8x4 fetch
converts it), and the quad's four corners for the normal, tangent and
midpoints. Their pipelines take no vertex buffer.
- Records carry each corner's alpha (3-bit codes in spare bits of words 3 and
4): with `separateAo` it is the corner's ambient occlusion, and one alpha a
quad made almost every face fail the exact check.
- Records always end on a whole quad of slots, so the translucent vertices a
pack's frames keep after them start a quad (their normals were read across
quads).
- `section-layout compact|wide` (horizon-ctl) switches the layout in a running
game for A/B checks; `follow_packed_layout` keeps it on for flat packs.
A GPU test draws records and their wide vertices through a pack's terrain
inputs and requires the same bits for every input.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Metal keeps 512 layers per sprite array, so the 8 arrays terrain.wgsl binds
hold at most 4096 block textures and a set with more (10,271) was refused
at startup. The split now grows to fit the arrays (up to the GPU's limit
per array) only when the textures need it; up to 4096 it stays 512, as
before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Post BLOCK_CHANGE at the frame from ItemFrame.interact, hurtServer and
dropItem, after the changes and drops vanilla performs first. Preserve
BlockAttachedEntity.kill ordering and discard unsupported frames without
a spurious ENTITY_DIE.
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]>
/damage: keeps this branch's villager reputation flush after a hurt and
main's enderman teleport settle (they act on different mobs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main split projectile_hit into a wrapper and projectile_hit_inner: the
reputation flush now runs in the wrapper after the inner hit. Trident and
wind charge hits keep both the reputation flush and main's absorbed-damage
and post-attack calls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep both sides of the potions.rs test modules; the contents fixture passes
the impact motion that main's splash now takes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
entity_portal_delay walks every entity list; it ran for every item, mob,
boat, cart and pearl in the End each tick.
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]>
Review: Entity.applyImplicitComponent applies two components, the custom
name and minecraft:custom_data (saved as `data`), and BoatItem.use /
BoatDispenseItemBehavior turn the boat to the player's or dispenser's yaw
after the stack's config, so entity_data's Rotation does not keep it.
Also moves place_minecart's doc line back onto it.
Harness group "vehicle item stack config": the three new cases 24/27
before, 27/27 now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecartItem.useOn, BoatItem.use and the dispenser's
MinecartDispenseItemBehavior / BoatDispenseItemBehavior apply
EntityType.createDefaultStackConfig to the new vehicle: the item's custom
name (applyComponentsFromItemStack), then minecraft:entity_data loaded
over it (updateCustomEntityTag) when it names the vehicle's type, an
op-only type only from an op player and never from a dispenser. Horizon
placed default vehicles. Game::apply_vehicle_stack_config does both,
through the /data merge loader, with the spawn egg's guard.
Command harness group "vehicle item stack config": 12/23 -> 23/23.
Fixes#11195
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:36:09 +03:00
1033 changed files with 78965 additions and 12699 deletions
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
@@ -47,6 +48,29 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- **Render passes keep HDR's light.** A pass that reads, filters or redraws the world's picture (upscaling,
anti-aliasing, sharpening, frame generation, post effects, captures) follows [docs/hdr-passes.md](docs/hdr-passes.md).
## Don't lose time
Where agent time went, measured on sessions and CI in November 2026: reading code, compiling `horizon-client`,
waiting in loops, and work someone else had already done. Tests were a small part.
- **Check nobody else is on it.** One in three pull requests closed unmerged was a duplicate. Before you write
code, look at the issue's linked pull requests and run
`gh pr list --state open --search "<issue number, function or file name>"`; then `/claim`.
- **Find code with the map, not by reading.** Start from the tables below and
`python3 tools/port_ledger.py where Class.method`. Read the function you change and its callers, not whole
files.
- **Compile cheaply while you work.** `cargo check -p <crate>` after an edit. Clippy, fmt and tests run once, at
the end, through `pr_ready.py`. `cargo clippy -p horizon-client` and `cargo build -p horizon-client` are the
slowest commands here: never after every edit, and build the game only when you need to run it.
- **Don't wait in loops.** Start a long command in the background and carry on, or wait for it once. No
`sleep` or `until grep` polling of builds, harnesses or CI.
- **One test per changed behaviour.** None for a refactor or a file move, none that reads source text, and no
harness where vanilla has nothing to compare (Horizon's own features, performance work).
- **A red check isn't always yours.** When `verify` fails in a test your change can't reach, say so in the pull
request and move on; don't hunt it.
- **Unsure about a rule? Finish the work.** Take the reading that lets you finish, and say in the pull request
which one you took. Don't stop, and don't change direction halfway.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
@@ -96,6 +120,7 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
- Use independent expected values or a vanilla oracle; do not derive the answer from the code under test.
- Test outcomes, not only calls to stubs. Use mocks to isolate behavior, not as the behavior itself.
- Skip tests of trivial constants, derives and getters, or behavior already covered by another test.
- One test per changed behaviour is enough. A refactor or a move that changes no behaviour gets no new test.
- Avoid pinning implementation details; a harmless refactor should not break a behavior test.
- Make slow tests faster without removing cases or weakening their coverage.
- Never delete parity harnesses or tests that pin vanilla behaviour: they are the spec.
@@ -103,8 +128,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
## Build and test fast
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests; paste its block in the PR
python3 tools/pr_ready.py --harnesses # also the affected harnesses (by default only when you change a harness, oracle, probe or the runner, as in CI)
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
> Adopted 2026-11-09 by the founder as his last decision as head of the project. It takes effect when he
> announces that he is stepping down. From then on only the council changes it, by a 2/3 poll (section 15).
>
> Amended 2026-11-10 by the founder (three kinds of action, elections by tier, who may do what, and council poll P41:
> the mods decide moderation matters). The council can overturn this amendment by poll.
Horizon has no boss. A **council of equals** decides by polls. **Theme groups** do the work in their areas, each
with a **lead**. The **Horizon Team** reviews and merges code. **Moderators** keep the Discord server and the
@@ -50,13 +53,13 @@ the Horizon Team, for example.
| Level | What it can do | How you get in | How you leave | 18+ |
| --- | --- | --- | --- | --- |
| **Member** | Play, chat, report bugs, suggest ideas, open issues and pull requests, pick group roles | Join the Discord server or the GitHub repo | Leave, or get banned | No |
| **Contributor** (⛏️ Iron, 🥇 Gold, 💎 Diamond, 🟪 Netherite ranks) | Write in #contributors; `/claim` issues; nominate people for group lead (if active in that group) | One merged pull request in a public horizon-rust repo, linked with `/link-github` and `/verify-github`. Ranks come from merged pull requests: 10, 40, 100 and 200 | Contributor status and ranks stay | No |
| **Group member** | Read and write in the group's channel; get the group's pings; take part in the group's advisory poll on its lead | Pick the group's role in Channels & Roles, or with `/groups` | Remove the role | No |
| **🤝 Support** | Answer and close posts in #horizon-support | `/apply`; vote of the mods and the Team (as today) | Resign, 30 days inactive, or removed by the Chief of Mods or the council | No |
| **🛡️ Moderator** | Time out, delete messages, manage threads, quarantine. No bans | `/apply`, then the selection filter; mods' vote; the council may object within 24 h | Resign, 30 days inactive, suspended by the Chief of Mods, removed by a council poll (simple majority) | **Yes** |
| **🛡️ Chief of Mods** | A moderator's powers, plus bans; leads the mods; suspends a mod at once pending the council | Council poll (simple majority), from the mods with at least 30 days | Resign, 30 days inactive, council poll (simple majority) | **Yes** |
| **🧭 Group lead** | Decides inside the group's topic (section 6); manages the group's channel; triage on the group's GitHub issues | Nomination, the filter, a 24 h public discussion with an advisory group poll, then a council poll (simple majority); 30 days probation | Resign, 30 days inactive (automatic), council poll (simple majority); confirmed again every 6 months | **Yes** |
| **🌌 Horizon Team** | Review and merge pull requests (GitHub `horizon-team`, write access); `/nightly` and triage commands (trusted reviewers); #horizon-team | Nomination, the filter, a 24 h public discussion, then a council poll (2/3); 30 days probation | Resign, 60 days without a merge or review (automatic), council poll (2/3, without the person) | **Yes** |
| **Contributor** (⛏️ Iron, 🥇 Gold, 💎 Diamond, 🟪 Netherite ranks) | Write in #contributors; `/claim` issues; vote in their group's elections (if active in that group) | One merged pull request in a public horizon-rust repo, linked with `/link-github` and `/verify-github`. Ranks come from merged pull requests: 10, 40, 100 and 200 | Contributor status and ranks stay | No |
| **Group member** | Read and write in the group's channel; get the group's pings; vote in the group's elections and votes when active | Pick the group's role in Channels & Roles, or with `/groups` | Remove the role | No |
| **🤝 Support** | Answer and close posts in #horizon-support | Given by any moderator (a free action, section 2.1) | Resign, 30 days inactive, or removed by a moderator, the Chief of Mods or the council | No |
| **🛡️ Moderator** | Time out, delete messages, manage threads, quarantine. No bans | Elected by the mods (section 5.3); council veto within 24 h | Resign, 30 days inactive, suspended by the Chief of Mods, recalled by the mods, removed by a council poll (simple majority) | **Yes** |
| **🛡️ Chief of Mods** | A moderator's powers, plus bans; leads the mods; suspends a mod at once pending the council | Elected by the mods, from the mods with at least 30 days (section 5.3); council veto within 24 h | Resign, 30 days inactive, recalled by the mods, council poll (simple majority) | **Yes** |
| **🧭 Group lead** | Decides inside the group's topic (section 6); manages the group's channel; triage on the group's GitHub issues | Elected by the group's active members and the Team (section 5.3); council veto within 24 h; 30 days probation | Resign, 30 days inactive (automatic), recalled by the same electorate, council poll (simple majority); confirmed again every 6 months | **Yes** |
| **🌌 Horizon Team** | Review and merge pull requests (GitHub `horizon-team`, write access); `/nightly` and triage commands (trusted reviewers); #horizon-team | Elected by the Team, 2/3 (section 5.3); council veto within 24 h; 30 days probation | Resign, 60 days without a merge or review (automatic), recalled by the Team (2/3), council poll (2/3, without the person) | **Yes** |
| **🏛️ Council** | Decides everything else by poll (section 4); bans; emergency actions by any 2 members | Nominated by a council member, the filter, a 24 h public discussion, then a council poll (2/3); 30 days probation | Resign, inactivity (section 5.5), council poll (2/3, without the person) | **Yes** |
**The account holder** isn't a level. The founder (Ludovic) still holds the project's accounts (GitHub organisation
@@ -64,6 +67,55 @@ owner, Discord server owner, the Microsoft app approved by Mojang, the domains,
council's decisions on them and hands them over step by step (section 11). As a council member he has one vote,
like everyone else.
### 2.1 Three kinds of action
Most things need no vote at all. A vote is for power over other people, and for decisions that bind a whole body.
| Kind | What | How |
| --- | --- | --- |
| **Free** | Everyday actions inside your own role (the list below, and anything of the same weight) | Just do it. Kira logs the ones she sees in #staff-log. The tier above can reverse it; if people disagree, the tier above votes. A pattern of abuse is a recall |
| **Body vote** | Giving or taking a role that carries power (an election), or a decision that belongs to one body: the mods, the Team, a group | `/elect` or `/vote body:` in Kira (section 5.3). Any council member can call a veto within 24 hours |
| **Council poll** | Rules, money, accounts and ownership, the council's own members, vetoes, anything that spans several bodies | `/poll` (section 4) |
Free actions, for example:
- a lead gives or removes their group's helper role (🧩 Horizon Modding Team), through Kira;
- a moderator gives or removes 🤝 Support, through Kira;
- a moderator times out, deletes, quarantines, or changes a channel's slowmode or topic; the Chief of Mods bans and
unbans;
- the Team merges and labels;
- a server operator restarts the official server, or prunes backups and chunks by the written rule;
- a lead pins, renames or creates threads in their group's channels;
- Kira's automatic actions.
**The 24-hour rule.** Every vote lasts 24 hours at most, and closes earlier once its result can't change. Only council
polls on this document, the licence, money and the GitHub organisation's ownership last 48 hours. A veto is called
within 24 hours of a result.
### 2.2 Who may do what
What each post decides, and what it holds on Discord, on the official server and on GitHub. Anything not listed is a
council matter.
| Post | Decides | Discord | Official server | GitHub |
| --- | --- | --- | --- | --- |
| **Member** | — | Chat, threads, picks group roles | Play | Issues, pull requests, `/claim` |
| **Group helper** (🧩 Horizon Modding Team) | — | Writes in the group's team channel | — | — |
| **🤝 Support** | Closes support posts | Manages threads in #horizon-support | — | — |
| **🛡️ Moderator** | Moderation matters, by the mods' vote (section 7) | Time out, delete messages, manage threads and nicknames, audit log, voice mute and move; quarantine through Kira; the staff channels | In-game op if also elected operator | — |
| **🛡️ Chief of Mods** | A moderator's, plus bans and unbans; suspends a mod at once; breaks ties in the mods' votes | Plus Ban | As a moderator | — |
| **Server operator** | In-game moderation by the server rules | — | Op (the server's ops list); no panel, no `server.toml` | — |
| **🧭 Server & SMP lead** | The server's settings, by the written rules (backup first, warning in game) | As every lead | A panel sub-user once a council accounts poll gives one | As every lead |
| **🧭 Group lead** | The group's free actions; gives and removes its helper role | In the group's channels: delete messages, manage and pin threads, ping the group; the helper role through Kira | — | Triage on the group's labels through Kira (no repository role) |
| **🌌 Horizon Team** | Merges; elects Team members | #horizon-team and the staff channels. The role still carries timeout and delete; whether it keeps them is a mod matter | — | `horizon-team`: write on horizon, horizon-mods, horizon-mixins, horizon-native-mod-template and horizon-server; also kira and horizon-site if P37 passes |
| **🔧 Server Manager** | — | Manage Channels and Manage Roles (the server's layout); given by council poll | — | — |
| **🏛️ Council** | Council polls, vetoes, emergencies (any 2) | A moderator's permissions plus Ban, mention @everyone and pin; #council | Panel sub-users for two members if P38 passes | The `council` team: code-owner reviews and the release tools' owner powers if P35 passes, security managers if P36 passes; a second organisation owner if P28 passes |
| **Account holder** (the founder) | Carries out decisions on his accounts (section 11) | Server owner | The panel's owner account | Organisation owner |
Kira applies a passed election everywhere she can: Discord roles herself, GitHub team membership when the council has
given her a token for it. Whatever she can't do (a panel sub-user, the ops list, a GitHub team without a token) she
posts as a list of clicks for a named person.
## 3. Each level in detail
### Member
@@ -84,13 +136,13 @@ an issue or a pull request on GitHub, or a request for a council poll (any counc
Each theme group (section 6) has a role that anyone can pick and drop. It opens the group's channel and receives the
group's pings. Picking a role grants no power. A group member counts as **active in the group** when Kira has seen at
least one counted action in the group's area (section 5.1) in the last 60 days. Only active group members can
nominate a lead or vote in the group's advisory poll.
least one counted action in the group's area (section 5.1) in the last 60 days. Active group members vote in the
group's elections and decisions (section 5.3).
### 🤝 Support
Unchanged: Support members help players in #horizon-support. They answer every post there and mark posts solved.
The role carries no permission, so it has no age rule. They join through `/apply`, and the mods and the Team vote.
The role carries no permission, so it has no age rule. A moderator gives it, and takes it back, through Kira.
### 🛡️ Moderators and the Chief of Mods
@@ -151,7 +203,7 @@ don't go through the filter or a probation. They do make the 18+ declaration (se
| --- | --- |
| Who starts one | Any council member, with `/poll` (Kira). Anyone else asks a council member to start it |
| Where | #polls-and-decisions, in public. Polls about a person (removal, appeals, the age rule) run in #council, and only their result is public |
| How long | 24 hours. It closes early only if every council member has voted |
| How long | 24 hours; 48 hours for this document, the licence, money and the GitHub organisation's ownership. It closes early only if every council member has voted |
| Ballot | Yes, No or Abstain. You can change it until the poll closes |
| Quorum | At least half of the council votes (Abstain counts). With 6 members, 3 votes |
| Simple majority | More Yes than No |
@@ -165,20 +217,20 @@ don't go through the filter or a probation. They do make the 18+ declaration (se
| Simple majority | Two-thirds |
| --- | --- |
| Day-to-day project decisions: direction, priorities, settling disputes between groups, a public statement | Adding council members and Horizon Team members |
| Group leads and co-leads: appointing, removing, confirming every 6 months | Removing a council member or a Team member (without the person concerned) |
| Moderators, the Chief of Mods, Support: confirming, removing | Rules: this document, the server rules, the contributing policy, the licence |
| Day-to-day project decisions: direction, priorities, settling disputes between groups, a public statement | Adding council members |
| Vetoing a body's vote or election (section 5.3) | Removing a council member or a Team member (without the person concerned) |
| Removing a group lead, a moderator, the Chief of Mods or Support (their bodies can also recall them) | Rules: this document, the server rules, the contributing policy, the licence |
| Second-level moderation appeals | Waiving part of the selection filter (section 5.4) |
| Deciding for a body that has fewer than 3 voters | Waiving part of the selection filter (section 5.4) |
| Bringing back someone who stepped down for inactivity, within 6 months | Changing the council's size |
**When in doubt, two-thirds.** Any council member can say a poll needs 2/3, and Kira then counts it at 2/3.
### 4.4 Discussion
Polls that add someone to the council, the Team or a lead post start with a **24-hour public discussion** (section
5.3). Other polls are discussed in their own thread while they run. A member who wants more time votes No and says
Polls that add someone to the council start with a **24-hour public discussion**. Other polls, and elections, are
discussed in their own thread while they run. A member who wants more time votes No and says
why. A failed poll can come back after 7 days.
### 4.5 Conflicts of interest
@@ -220,7 +272,7 @@ false age declaration (section 9).
## 5. The selection filter
Nobody becomes a lead, a Team member or a council member just because they are well liked or talk a lot. Kira
measures the work, and only people above a floor can be nominated. The council then decides among them.
measures the work, and only people above a floor can be nominated. Their body then elects them (section 5.3).
### 5.1 What Kira measures
@@ -254,25 +306,63 @@ A nomination is refused below these floors.
The numbers are checked again once a year (section 15). They should stay reachable for people who really work on
Horizon and out of reach for people who don't.
### 5.3 The path to a role
### 5.3 Elections
1.**Nomination.** Run `/nominate member: role: [group:]` in Kira.
- A **group lead** can be nominated by an active member of that group or by a council member. A self-nomination
needs a second from another active group member or a council member.
- A **Team member** can be nominated by a council or Team member. Someone can also nominate themselves with
`/apply`, if a council or Team member sponsors them.
- A **council member** can only be nominated by a council member.
- **Moderators** apply with `/apply`.
2.**The filter.** Kira checks the floor and the 18+ declaration. Below the floor, Kira refuses and says which line
is short.
3.**Public discussion, 24 hours.** Kira posts the nomination and the evidence card in a thread of
#polls-and-decisions. Contributors and the council can comment. For a lead, Kira also runs an **advisory poll** in
the group's channel for the same 24 hours. Only active group members vote in it, and the council sees the result.
4.**Council poll, 24 hours.** Simple majority for a lead, 2/3 for the Team and the council. For moderators, the mods'
vote replaces this step, and any two council members can object within 24 hours; the council then decides by poll.
5.**Probation, 30 days.** The role is held in full, except for the Team limits in section 3. At day 25, Kira asks the
council in #council whether anything is wrong. Probation ends by itself at day 30, unless a council poll (simple
majority) ended it earlier.
The body that works with a person elects them. Kira runs it: `/elect post: member: [group:]`, and `action: remove`
for a recall.
| Post | Who votes | Passes with | Who can start it |
| --- | --- | --- | --- |
| **🛡️ Moderator** | The mods (🛡️ Mod and 🛡️ Chief of Mods) | Simple majority; the Chief of Mods breaks a tie | Any mod, a council member |
| **🛡️ Chief of Mods** | The mods; candidates are mods for 30 days or more | Simple majority | Any mod, a council member |
|**Server operator** | The mods and the 🧭 Server lead | Simple majority | The Server lead, the Chief of Mods, a council member |
| **🧭 Group lead** (a co-lead once the group has 8 active members) | The group's active members, and the Team | Simple majority | The current lead, a Team member, a council member |
| **🌌 Horizon Team** | The Team | Two thirds, and at least 3 Yes | A Team member, a council member |
| **🏛️ Council** | The council, by poll after a 24-hour discussion (section 4) | Two thirds, and at least 3 Yes | A council member |
| 🤝 Support, a group's helper role | Nobody: a free action (section 2.1) | — | A moderator; the group's lead |
An **active group member** has held the group's role for 14 days or more (Kira counts from when she first saw it)
and has at least one merged pull request or review with the group's label in the last 60 days.
1.**Start.** Kira checks the floor (5.2) and the 18+ declaration. If the candidate falls short, the person starting
gets a private "not now", and the candidate a private message saying what is missing. Nothing is posted and no
reason is ever shown to anyone else. Kira checks what she can measure (verified GitHub, merged pull requests and
reviews, overall and by the group's label, rank, days in the server, Kira level, clean record, the declaration);
the voters judge the rest.
2.**Vote, 24 hours,** in the body's channel, discussed in the vote's thread. Yes, No or Abstain, changeable while it
runs. Kira shows how many voted, never who. It closes early once the result can't change.
3.**Result,** applied at once. Kira gives or takes the Discord role and, with a token, the GitHub team; she lists the
rest as clicks for a named person. Only the result is published, never the ballots.
4.**Veto, 24 hours.** Any council member can call it with the button under the result. A council poll (simple
majority, without the people involved) decides; if it passes, Kira undoes the result.
5.**Probation, 30 days,** for leads and Team members. It ends by itself unless someone who can vote starts a recall.
The council can always remove someone by its own poll. **Body votes** work the same way for a body's own decisions:
`/vote body:mods`, `body:team` or `body:group`, simple majority, 24 hours, the same veto.
#### Hard cases
| Case | What happens |
| --- | --- |
| Fewer than 3 can vote | The council decides by poll |
| Quorum | A third of the electorate, and at least 3 ballots (Abstain counts). Without it, nothing changes |
| Tie | Fails, and nothing changes. In the mods' votes, the Chief of Mods' ballot breaks it |
| The candidate | Doesn't vote on themselves, and can decline (the election ends). The person who started it votes normally |
| Conflicts | Whoever the vote is about, or who gave or received the moderation action in question, doesn't vote |
| Several candidates for one seat | Nominations within the first 12 hours join one approval ballot. The seat goes to the most approvals if that is at least half of the voters; otherwise a 24-hour run-off between the top two |
| A seat that is held | Changes only by a recall or by the 6-month confirmation |
| The council is already voting on it | No election starts for the same person and post |
| Cooldown | A failed or vetoed election can't be started again for the same person and post for 30 days. At most 3 elections run at once in a body |
| Vacancy | When a lead or the Chief of Mods leaves, Kira opens the seat's election herself: 24 hours of nominations, then the vote with everyone nominated. Nobody nominated: the seat stays open, and the tier above acts meanwhile. Kira never picks a candidate |
| Abuse of a free action | The tier above reverses it; a disagreement goes to its vote. A pattern is a recall |
| Emergency | Section 4.6. The Chief of Mods can still suspend a mod at once |
| Kira is down | The body may run the same vote as a Discord poll in its channel for 24 hours; Kira records the result when she is back |
#### Reminders, not nominations
Once a month in a group's channel, Kira says how to propose someone, only when the group has no lead or someone in it
meets a post's floor. When a seat opens, she posts once in the electorate's channel. Once a month in #council, she
lists privately the people who meet a floor. These are suggestions: only people start elections.
### 5.4 Exceptions
@@ -299,8 +389,8 @@ holder or an org owner removes GitHub access when the Team role ends.
### 5.6 Terms
- **Group leads** are confirmed every 6 months by a simple-majority poll. If someone else in the group passes the
floor and is nominated, both go on the same poll.
- **Group leads** are confirmed every 6 months by an election in their group. If someone else in the group passes the
floor and is nominated within its first 12 hours, both go on the same ballot.
- **Council and Team** have no fixed term. They stay as long as they are active and keep the council's trust. Removal
is always possible (section 4.7).
@@ -339,8 +429,9 @@ the design system:
- technical and design direction inside the area, when vanilla behaviour and the default look don't change;
- accepting or declining an idea or a proposal in the area. Any two active group members or one council member can
take that decision to the council;
- giving and removing the group's helper role, through Kira (a free action);
- how the group organises itself (threads, meetings, the channel's rules on top of the server's);
- asking the Team for reviews, adding `needs-proof`, and nominating people.
- asking the Team for reviews, adding `needs-proof`, and starting elections in the group.
### 6.4 What goes to the council
@@ -356,21 +447,28 @@ the design system:
The work goes on. The Team reviews and merges, and Kira still routes the group's issues and questions to its
channel. Until a lead is chosen, any council member may take a lead's decision for that group and says so in the
channel. Kira pins a "lead wanted" note with the floor. **No lead is better than a lead who doesn't do the work:**
channel. Kira posts once that the seat is open and how to start its election (section 5.3). **No lead is better than a lead who doesn't do the work:**
nobody is appointed just to fill the seat.
## 7. Moderation and appeals
- **The mods decide moderation matters** (council poll P41, imposed by the founder on 2026-11-10): who joins or
leaves the mod team, the mods' permissions and the staff channels, bans and unbans, and channel settings for
moderation. Everyday actions are free (section 2.1). Anything bigger is a mods' vote (`/vote body:mods`, simple
majority, 24 hours), and the Chief of Mods breaks ties. **The council keeps a veto:** within 24 hours of a mods'
decision, any council member can call a council poll, and a simple majority overturns it.
- **The server rules** are in #rules. Moderators enforce them, the Chief of Mods leads the mods, and the council can
change the rules (2/3).
- **Every action has a mod case**: who acted, what they did, and why. Kira keeps the cases.
- **First appeal:** within 14 days, through #appeal (for a quarantine) or mod mail. A moderator who wasn't involved,
or the Chief of Mods, answers within 72 hours.
- **Second appeal:** for bans, timeouts over 24 hours and quarantines, a council poll in #council (simple majority).
Members who were involved don't vote. The answer is final for 90 days.
- **Second appeal:** for bans, timeouts over 24 hours and quarantines, a mods' vote in #mod-chat. Mods who were
involved don't vote, and the council's veto applies. The answer is final for 90 days.
- **Not appealable here:** actions that Discord's rules or the law require. Those are reported where the law says.
- **Removing a moderator:** the Chief of Mods can suspend a moderator at once, and the council decides within 72 hours
(simple majority). The council removes the Chief of Mods the same way.
- **Removing a moderator:** the Chief of Mods can suspend a moderator at once, and the mods decide within 72 hours by
a recall vote. The mods recall the Chief of Mods the same way. The council can also remove either by poll.
- **Council members keep the ban permission** for emergencies and when no moderator is around. They log the ban like
any other, and the mods can reverse it by vote.
- **Staff are moderated like everyone.** When a council member is involved, the council decides without them.
## 8. Conflicts and hard cases
@@ -432,7 +530,7 @@ unable to act, and never puts an account that is tied to a person into anyone el
| Account | Today | Can it be shared? | Plan |
| --- | --- | --- | --- |
| **GitHub organisation** (horizon-rust) | One owner (the founder) | Yes: an organisation can have several owners | **Step 1:** a second owner from the council, chosen by a 2/3 poll in the council's first week, with two-factor authentication on. **Step 2:** a `council` team; CODEOWNERS reviews and the release tools' owner powers (`paused`, `/nightly remove`) move from the founder to that team |
| **GitHub organisation** (horizon-rust) | Two owners since 2026-11-11: the founder and rdbtCVS (P28); a `council` team holds CODEOWNERS (P35) and is the security managers (P36) | Yes: an organisation can have several owners | **Step 1:** a second owner from the council, chosen by a 2/3 poll in the council's first week, with two-factor authentication on. **Step 2:** a `council` team; CODEOWNERS reviews and the release tools' owner powers (`paused`, `/nightly remove`) move from the founder to that team |
| **Discord server** | Owned by the founder | Partly: a server has exactly one owner; powers can be given through roles | **Step 1:** the 🏛️ Council role gets moderation and ban permissions; Kira applies the outcomes of passed polls, so nobody needs Manage Roles; two-factor authentication becomes required for moderation. Ownership itself stays with the founder until a legal entity exists |
| **Kira** (bot application, Railway service) | The founder's Discord developer account and Railway | The bot: yes (a Discord developer team). Railway: adding members may need a paid plan | **Step 1:** a Discord developer team with two council members. Railway members once the council decides on money |
| **Official server** (hosting panel) | The founder's account | Yes: panel sub-users | **Step 1:** sub-users for the Server and SMP lead and for one council member |
@@ -458,7 +556,9 @@ and the founder carries it out.
## 13. Transparency
- **#polls-and-decisions:** every poll and every result, posted by Kira, and the 24-hour discussions of nominations.
For polls that aren't about a person, each member's vote is shown once the poll closes.
For polls that aren't about a person, each member's vote is shown once the poll closes. The results of elections
and body votes are posted there too; for elections, only the result.
- **#staff-log:** the free actions Kira sees (roles given or taken, bans, unbans), so the tier above can check them.
- **The decisions log:** Kira keeps an append-only record of every poll, emergency action, appointment and
step-down, with its date and result. It is public and machine-readable, so the agents can check a decision there.
- **This document** lives in horizon-rust/horizon as `GOVERNANCE.md`. A short summary is pinned in #rules and in
Needs Rust 1.90+, Python 3.10+, JDK 25, and on Linux `libasound2-dev libudev-dev`. `horizon-prepare` builds the game's resources from **your own copy** of Java Edition 26.3 (checked against `research-lock.json`) and never downloads it:
Needs Rust 1.90+, Python 3.11+, JDK 25, and on Linux `libasound2-dev libudev-dev`. `horizon-prepare` builds the game's resources from **your own copy** of Java Edition 26.3 (checked against `research-lock.json`) and never downloads it:
1.**Client:** play 26.3 once in the official launcher. Horizon reads `~/.minecraft` (or `--minecraft-dir` / `HORIZON_MINECRAFT_DIR`).
2.**Server jar:** the 26.3 server jar (`downloads.server.url` in `versions/26.3/26.3.json`), passed once with `--server-jar` or `HORIZON_SERVER_JAR`.
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.
"Hides and turns off every AI feature: agent control, horizon-ctl and the MCP servers. The launcher's switch too."
}
Item::DiscordPresence=>{
"Shows Playing Horizon on your Discord profile while Discord runs: menus, single player or multiplayer, the dimension and the time played. Never a world's name or a server's address."
}
Item::Splitscreen=>{
"In a world, another controller can join as a new player: press A when it asks, then pick a profile and where to play."
}
@@ -856,24 +757,6 @@ impl Item {
Item::ReproKeys=>{
"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 1033 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.