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.
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.
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.
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]>
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.
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.
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
🤖 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.
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]>
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]>
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]>
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]>
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]>
🤖 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).
🤖 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]>
🤖 Agent: the engine takes the window icon from the layer above, and entry.rs keeps main's pack_formats::install() after horizon_look::give(), so the icon is still given before any window opens. The head now also carries the menu_style stack (#14026, #14027, #14029): graphics_style's and ui_theme's colours are one OnceLock read per call from menu_style, Horizon's BLOCKS_DARK values come from horizon_look.rs at startup so the menus look the same, and the renderer and client lib no longer depend on horizon-theme. Merges cleanly with main.
🤖 Agent: step 2a-ii of spec #14091 draws the cosmetic items on the body through PlayerDraw instead of calling the cosmetics from the engine; stacked on #14214.
🤖 Agent: the presence now starts only from horizon-official's client_started hook, so horizon-client's own executable (plain builds, harnesses) never opens Discord's socket; the e2e passes 4/4 on the official binary and verify is green on this head.
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 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]>
🤖 Agent: matches LecternBlock.getAnalogOutputSignal (HAS_BOOK checked before the block entity, 26.3 bytecode); I pushed 1d52dd4: the new test had taken the next test's doc comment, and it now also checks has_book=true reads 15.
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]>
🤖 Agent: mob piston pushes now restitute after the move, reading the landing block as Entity.move does, so a pushed mob bounces 1.0 on slime; the corpus case puts the player back so the command_exec oracle runs through (only a known #13022 boat tick differs vs main); run-parity covers the TNT commit.
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]>
🤖 Agent: villager POI searches now take the PoiSections lock and column lookup once per chunk instead of once per section; on the 122-player SMP bench (190 villagers, 3 rotated rounds) villagers went from 20.3-21.3 to 15.7-15.9 µs/mob (ratio to chicken/cow/sheep 2.0-2.1 -> 1.75), ~0.5 ms/tick normalized; same POIs in the same order (20k-step differential test vs the old per-section get loop).
🤖 Agent: graphics_style's and ui_theme's colours are now one OnceLock read per call from menu_style; Horizon's BLOCKS_DARK values come from horizon_look.rs at startup, so the menus look the same; the renderer and client lib no longer depend on horizon-theme.
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]>
🤖 Agent: studio::EDITOR is a compile-time constant (true by default, so the default build runs no check); without horizon-extras recording/playback stay and the editor input, drawing and commands are off.
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.
🤖 Agent: pure move checked by line multiset; the editor is reached through five calls (save path, replay opened/advanced/closing/closed) at the places its code was.
🤖 Agent: playback state (driver, camera type to restore, standalone, playback clock) moves to game/replay.rs Replay; Studio keeps the editor; callers ask replay.playing(); game.rs does not grow.
🤖 Agent: pure move: game.rs's inline Studio blocks and the replay key shortcuts become game/studio.rs methods with the same bodies and early returns; the engine asks replaying()/flying()/active().
🤖 Agent: Row::Button/Text are #[non_exhaustive] variants built through constructors, so later options do not break crates; the options shell pairs two half buttons into one row and a press on an inactive one reaches nothing (tested).
🤖 Agent: step 2a-i of spec #14091 adds PlayerDraw to horizon-hooks (plain types only) so crates can draw on player models in the world pass; creatures.rs is main's, with the swap moved to creatures/mob_geometry.rs.
🤖 Agent: F2 and world-icon PNGs are written on a thread after map_async; a second F2 in the same second now skips the name still being written (agent fix 1ffa9ed), and agent screenshots keep the synchronous path.
🤖 Agent: the player break now pops the named stand item and then goes through brokenByAnything, which plays the break sound before the loot as ArmorStand.brokenByAnything orders it; stand_break_loot, stand_command_hurt and armor_stand_equip_events pass with it, and command_exec on a union with main shows no armor stand mismatch.
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]>
The case left the player at 1.5 200 1.5; with that, vanilla's command_exec oracle did not load the chunks of the later teleport lands group (required chunks did not load, twice). Every other group that moves the player puts it back at 8.5 230 8.5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 277584e4df6f0b090655d50d19138c89dad4b77a)
🤖 Agent: removes itarqos5 from .github/trusted-reviewers.txt, the Team and reviewer lists in docs/release-flow.md and the pinned login in tests/test_nightly_set.py; the nightly, claim and triage test modules (102 tests) passed per the PR.
🤖 Agent: step 3b makes names.rs, scoreboard.rs and perspective.rs call the engine's dispatch (Game::hooks_name_marks builds one PlayerModel per tag drawn) instead of the cosmetics directly.
Read the actual landing block for authoritative TNT after applying the
piston push. Preserve client fallback ordering and exhausted-budget
returns. Validate five exact vanilla landings and server/client controls.
Correct the neighboring order test's rounded TNT-width expectation using
the pinned vanilla result without widening its tolerance.
Co-Authored-By: Codex (GPT-6) <[email protected]>
🤖 Agent: the official server now reaches the list only through horizon-official's started hook, so horizon-client's own executable (plain builds, harnesses) has neither the official server nor the withdrawn partner's address; verify and launcher green on this head.
🤖 Agent: the API half of cosmetics step 3: Hooks::name_decoration returns a Copy NameDecoration (fixed &'static mark rectangles, optional badge, tag lift) so no heap data or engine type crosses the hook.
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.
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.
🤖 Agent: cosmetics step 1 of spec #14091: the emote feed and presence go through plain-type frame and tick hooks in horizon-hooks, so no engine or renderer type crosses the seam.
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]>
🤖 Agent: seat() is a thread-local the engine sets at Split::turn and around a guest's join/profile load (the profile used to load before the guest's turn, into P1's seat); a new profile saves on P1's seat and reads its stores back into the guest's. Without splitscreen it is always 0. Main merged in (horizon_platform::hooks exports both seat and main's tunnel).
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]>
🤖 Agent: one pass over the big lists per RemoveEntities packet, per-id cleanup order kept (dragon before its crystal); moved to remote_removal.rs so remote.rs shrinks (agent fix 8cf1e6b).
🤖 Agent: re-checked the new head: only main merged in, and the PR's own diff is unchanged. shader_pack_kind().is_none() is still exactly the old native/pack_post test, so HDR Lighting greys only where the running pack lights the world.
🤖 Agent: re-checked the new head: the only change is that FluidFog/FluidFogs moved to the render crate's pack_palette after #13959, converted from horizon_polytone's type once when the pack loads. The fog still runs last and is vanilla's without a modifier.
`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]>
The governance the founder adopted on 2026-11-09 as his last decision as
head of the project: a council of equals deciding by 24-hour polls, theme
groups with leads, the selection filter, moderation and appeals, the 18+
rule for roles of trust, the agents, and the account handover path.
README and CONTRIBUTING link to it with one line each.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Util.shuffle runs every pass again, so the level random keeps vanilla's draws, and nothing is cached across ticks any more. What stays saves allocations: Counted streamed instead of collected, the SpawnPlayer buffer reused, and a single-player collect without the neighbour scan. Fake players stay out of the anchors and in the level, as on main. Merged with main on a branch together with #13795 and #13800: clippy and 3827 tests pass; natural_spawn runs in CI parity.
🤖 Agent: every hopper again records tickedGameTime each tick, so the 8/7 transfer cooldown matches vanilla, and is_full keeps #13763's insertion_item for pots. The gains stay: slot iterators instead of a Vec per transfer, empty hoppers with nothing above them or within reach skip the whole pass, one block lookup per tick. Merged with main on a branch together with #13795 and #13811: clippy and 3827 tests pass; the hopper harnesses run in CI parity.
🤖 Agent: with the offline capture tools removed, the pitch now goes through RespawnConfig, the personal respawn save (old records load pitch +0.0) and only the free-point branch of find_respawn_and_use; bed and anchor keep pitch 0, as RespawnData's block cases do. Merged with main on a branch together with #13800 and #13811: clippy and 3827 Rust tests pass on the desktop.
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]>
🤖 Agent: ui_theme's palette, sizes and Geist fonts come from menu_style; with no fonts given (NEUTRAL) the Horizon overlays measure and draw no text instead of panicking.
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]>
Stacks the Discord crate on the official-servers crate: horizon-official
registers four parts; horizon-client-lib has no horizon-extras feature left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the verify failure was the two rustc-compiled fixtures (test_compute_fov_hook, test_world_input_capture) still modelling game.studio_freecam/studio_replay; they now carry the Studio struct game.rs reads, and both pass on this head.
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]>
🤖 Agent: gameplay now asks controllers only through input_move, input_holds, input_look and input_feedback in play_input.rs, which still call the same pad functions, so the pad modules can later answer through a registered input source.
🤖 Agent: the game now plays only as the selected account (else the first), which must own the game itself (launch_for_game); old accounts.json/settings.json load and drop offline, offline_selected, looks, skins/ and dev_offline on the next save, covered by files_from_launchers_with_offline_names_load_and_lose_them_on_save.
🤖 Agent: a bounced arrow goes back by join number through JoinList::put_back (unit-tested), so it keeps its EntityTickList place; moved out of creatures.rs, which may not grow.
game.rs's CursorMoved arm now asks self.studio_mouse_dragged(b.width)
instead of the inline timeline drag; the fixture leaves that call out of
its slice as it left the inline block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Generic row options a page drawn as its own screen needs (Play with Friends'
page, #14167 step 2a): Row::button(..).active(false)/.half()/.footer(),
Row::text(..).quiet()/.groups(), Row::Space. The options shell shows text as
notes, pairs half buttons and ignores space; Extras shows active buttons only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
🤖 Agent: a stonecutting result with a components patch goes through SmithingResult's template create, so a pack's custom-named result keeps its name; plain results stay on the cheap path.
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]>
🤖 Agent: AnvilScreen.init's setInvertHighlightedTextColor(false) on the rename box; moved apart from #14094's EditBox lines so both merge in either order.
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]>
Drops the trusted-reviewers line, the Team lists in docs/release-flow.md and the pinned login in
test_nightly_set. GitHub team memberships (horizon-team, contributors) are already removed.
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]>
🤖 Agent: bee sounds no longer rescan every mob per bee each tick and the sound engine takes one lock per channel update; silent one-frame/empty sounds stop again (agent fix 23ca4a8).
🤖 Agent: the fast path only takes one-expression programs whose kind is known and is checked bit for bit against the interpreter; 468 lines after the timing test moved to #14185. Queue AFTER 14089.
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]>
🤖 Agent: OfferFlowerGoal's (6,2,6) box plus forNonCombat().range(6) with sight, isBrightOutside's fixed-time check, the golem's own random for the repair pitch; the defence sight test the body named is now in the diff (agent fix 4643ea9).
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]>
🤖 Agent: the emissive suffix is read once per pack load through PackFormats::emissive_suffix; atlas building only compares a stored string per sprite as before. layer plan M6 step 3.
A fluid modifier's fog_colormap (a flat colour), fog_radius and fog_fade apply to the
water's or lava's fog after everything else: the colour replaces it, the radius scales the
fog, and a fade of 0 makes it a hard wall. Measured on the real mod (red fog at 0.3 of the
radius, within the scene's variance of the vanilla baseline). Colormap ids and expressions
aren't read yet.
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]>
🤖 Agent: AttachFace wraps through from3DDataValue and Peek/Color stay raw bytes; texture() reads the colour signed, so -1 draws white as DyeColor.byId's ZERO strategy does.
🤖 Agent: the LAN owner's warden tracker now goes into g.saved before playerdata::store rewrites player_extra from it, so warning level and cooldown survive a save; with_warden keeps lan.rs (over 3000 lines) from growing.
🤖 Agent: handleFallFlyingCollisions now compares lastSpeed (before updateFallFlyingMovement) with the delta Entity.move leaves, collided axes zeroed, so a glider pressed on a wall stops replaying the crash sound.
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]>
🤖 Agent: the glass About card now builds location_row (Move…, confirm, retry/default), and the row keeps the Open button beside Move (agent fix eb3157d; needs the launcher job green: no local launcher build).
🤖 Agent: only os_reported (Windows) sets peak_guessed, so Wayland and macOS keep their labels; main merged, the docs paragraph re-applied on main's text, and main's ui_nits test given the field.
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
🤖 Agent: pack colours (OptiFine color.properties, Polytone colors) are now read through pack_hooks::PackFormats once per pack load; the engine keeps a plain PackColors table, so per-block tint lookups stay table reads. layer plan M6 step 2. Merging main moved its new Polytone damage-bar colours behind PackFormats::item_bars, test moved with them.
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]>
🤖 Agent: the world depth is kept whenever post effects run (use_depth_buffer reads it), only in that case; move_off_page moved out unchanged so gfx.rs doesn't grow.
🤖 Agent: checked line by line: every removed line reappears in minecarts/nbt.rs or minecarts/index.rs (pure move, exempt from the 500-line count); the new head adds only main merges (same tree as the reviewed head merged with main).
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.
🤖 Agent: EnchantmentHelper.updateEnchantments returns EMPTY when the component is absent, so enchant is now a no-op on a stack whose enchantments (or a book's stored ones) were removed.
🤖 Agent: a play PingRequest is answered with PongResponse on its own connection (handlePingRequest) and kept through the held/handover filter; it doesn't touch last_action_tick. 5/5 ping tests pass with resources.
🤖 Agent: rusqlite 0.41 with fallible_uint keeps the u64/usize conversions the server stores; fmt, clippy -D warnings and its 59 tests pass inside the telemetry workspace (pr_ready skips it).
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]>
🤖 Agent: SeenGrid cells stay sorted (binary insert on update), so candidates merges 1-4 cells without sort_unstable and still returns snapshot order (the 2000-round randomized test holds); main merged (world.rs), bench seed's hex grouping fixed for clippy.
🤖 Agent: docs only, one CONTRIBUTING paragraph pointing mod-related pull requests to arismoko/horizon (the project decision of 11-09); mods.md and native-mods.md never said where to send them, so they stay.
🤖 Agent: adds horizon_hooks::pages::Place::Extras, a page with no entry of its own whose rows join the Extras page group named by their title (or get a group of their own after the page's groups); stacked on #14153 (layers/pages-pause).
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]>
🤖 Agent: with no page registered for the pause menu, pause_rows is the split row alone (or empty), so the grid, icon row and focus ids are exactly as before; registered pages take ids 40+index and open the options screen at Page::Added like the split row does.
🤖 Agent: horizon-ctl status's display object is built by window::display now, the same width/height/refresh/scale fields as before, so tools reading it see no change.
🤖 Agent: a registered slider is a real Opt (Opt::Page(page,row)), so drag, arrow keys, narration, focus and search come from the existing slider code; its arms read the page's rows only for that variant, and options_ui.rs still shrinks (horizon_page_tooltip moves out).
🤖 Agent: per-frame cost with no layer registered is one sealed-slice load and is_empty in draw_extras (no canvas, no corner pass); a layer's place() uses native_corner_shown, so F3 hides the same corners for it as for mods' boxes.
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]>
Replay playback moved out of Studio into game/replay.rs's Replay; the
pointer-capture fixture carries game.replay.driver and Game::studio_up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fov.rs and the pointer-capture methods now ask self.studio.flying() and
replaying(); the fixtures mirror those three methods of game/studio.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pushed one fix: the control socket's sign on a read-only book again answers 'not a book and quill' (the catch-all arm had turned it into a silent success).
🤖 Agent: pushed one fix: the stack's modifier amount is read as a double (it went through float_value, so 0.1 became 0.10000000149), and the test now pins 0.1.
🤖 Agent: plain git revert -m 1 of #13968, which put the shaders/ pack-format config into an engine crate (horizon-render-pipeline); the layer rule puts that format in platform, and no layers.json allowed entry changes.
🤖 Agent: with no page registered the options screen is unchanged (Horizon Settings gets no extra row, nothing is asked per frame); a registered page's rows are rebuilt only while it is open, and every act sends Event::Changed so the crate's settings store is saved.
test_compute_fov_hook and test_world_input_capture paste fov.rs and the
pointer-capture methods into a fixture Game; those now read
self.studio.{freecam,replay,rmb_down}, so the fixture carries a Studio.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote.rs is over 3000 lines and may not grow (verify's "Big files don't
grow" failed): remove_entity, remove_entities and their test move to
game/remote_removal.rs, a child module of remote, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#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
FrogModel.setupAnim applies FROG_WALK with applyWalk(pos, speed, 1.5F, 2.5F)
on land (1.0F is the swim animation's).
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]>
🤖 Agent: crafting_dye and crafting_imbue entries follow DyeRecipe.display and ImbueRecipe.display exactly (OnlyWithComponent dye, DyedSlotDemo; WithAnyPotion source and result), checked against the 26.3 bytecode.
🤖 Agent: pure move of 82 lines that puts the moderation durations into horizon-server-files, the first step of the chat filter's engine hook (#13942 is stacked on it).
🤖 Agent: only unknown codes spend the nameplate bucket, so once a code exists an attacker's joins stop draining it and a friend waits at most one refill (12 s at 5/min).
🤖 Agent: pure move; mob_geometry and mob_geometry_inner go from creatures.rs to creatures/mob_geometry.rs, only super:: paths one level deeper and two rustfmt reflows.
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
tick_remote_level asks the input source (input_move) instead of pad_move, so
the order check (load completes, then the player moves) looks for that call
(verify run 42649031940).
Co-Authored-By: Claude Opus 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]>
🤖 Agent: verified pure move: every removed line reappears in creatures/spawn_eggs.rs once visibility and super::super:: are normalised; creatures.rs 8896 -> 8732 counted lines.
🤖 Agent: joining a friend now works only in horizon-official (the shipped game); the friends harness was switched to that binary in the same PR, so CI's friends_integration run is the proof that release builds still join.
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: pure move; ParticleEngine::send_particles and create go unchanged from particles.rs to particles/spawn.rs, so particles.rs shrinks by 67 lines.
Main retranslated 252 Belarusian launcher strings; this PR removes the 28
offline-name keys. be.json is main's file with those 28 keys taken out (7 of
them were among the retranslated ones); every lang file parses and none keeps
a removed key.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pure move of the Friends screen out of the engine library; the build without Friends keeps naming the pause icon "Play with Friends" from friends_off.rs, so the widget tree and controller focus are unchanged.
🤖 Agent: docs only: docs/forking.md now says forks register their own Azure app and may not ship or fetch Horizon's client ID; nothing found to change.
🤖 Agent: speed_bonus now reads finalize::transient, the same minecraft:baby modifier /attribute reports, so modifier remove slows the baby and a baby piglin gets 1 + 0.2f, not 1.5.
🤖 Agent: Mob::do_move now casts Entity.move's FALLDAMAGE_RESETTING/WATER ray only when fall_distance != 0 and the move is a block or more, so it costs nothing on an ordinary tick; boats share the same helper and now query the water on a tagged block too.
🤖 Agent: release_after_block_move runs AbstractArrow.move's shouldFall check after every non-SELF move, including a push limitPistonMovement or the collisions cut to nothing; #10243's part is already on main, so the net change is piston_push.rs plus the new harness.
🤖 Agent: TagValueInput.getLongOr takes any NumericTag (longValue), so an Int LootTableSeed now seeds the roll as in vanilla instead of falling back to the position seed.
🤖 Agent: the extra cost is one O(mobs + boats) scan per body a moving piston or shulker lid pushes, the same set item_entity_collisions gives items; the new harness pins the snowball stopping at the boat's side.
🤖 Agent: the engine keeps only the friend: address form; a build without a resolver refuses such an address with the same invalid_address key the old cfg(not(feature = "friends")) path used, so reconnect still never retries it.
🤖 Agent: git 2.5x runs a detached geometric auto-maintenance after each commit that gc.auto=0 does not stop; the test now runs git with no user/system config and maintenance.auto=false, so the 12 packs it counts stay 12 (desktop: 10/10 failures before, 10/10 OK after).
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.travelFallFlying takes lastSpeed before the glide update, as
move_h does; the post-update speed left move_h unused (verify failed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its `mod` line moves from gfx.rs (over 3000 lines, docs/layers.md#file-size) into
surface_setup.rs, the device request that reads it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-hooks gains Hooks::player_layers with a plain PlayerDraw (bone
matrices, the frame's ticks, the body pose, sprites, draw of an Arc<Mesh>
with its parts posed) and the Mesh/Quad/Bone/Pass types. The engine
implements it on the GPU skin path (each mesh made a Part once, cached by
its address while a crate holds it) with the vertex path as fallback, and
dispatches it from creatures.rs's world pass one call for one call; the
cosmetics still draw there until 2a-ii.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: docs only (docs/layers.md): the Platform registries spec as built in #14036/#14070/#14081/#14096/#14110 (HUD place() returns None under F3, seat() for splitscreen), and the launcher/horizon-server exception rows removed after #13905.
🤖 Agent: the earlier verify failure was main's options_ui.rs growth measured against this branch's old base; with main merged in it passes, and mob_ai passed with this change on union19b, so a stung player stays the bee's angry_at through the chunk save as Bee.addAdditionalSaveData writes it.
🤖 Agent: the prepare lock now unlocks explicitly on drop, so a child that inherited the descriptor between fork and exec no longer holds the flock past the preparation (#8001); the new test holds a dup'd descriptor to show it.
🤖 Agent: pure move; pub(super) from render_tick.rs is narrower than it was in game.rs, but the only mention outside game/ is a doc comment in clock.rs, so nothing loses access.
🤖 Agent: the official and partner server entries now live in official_servers.rs behind a lib horizon-extras feature, and servers.dat's lock moved to horizon_protocol::servers::lock(); a plain build adds no server and carries no afri-network address.
Remote entity client update now calls frog::walk_target for Species::Frog so limb_speed advances at the vanilla 25x distance rate instead of 4x. Also restore FrogAnimation.FROG_WALK speed factor to vanilla 1.0F in body_anim and guard keyframe looping against zero length.
🤖 Agent: both skips only move side-effect-free checks (box intersection, empty effect lists) ahead of the lookups; the set and order of mobs that push or swirl is unchanged.
🤖 Agent: text_start_x is EditBox.updateTextPosition's centred textX ((width - text width) / 2 from getX), and position_at measures the click from the same start, as findClickedPositionInText does.
As ludovic111 asked on #13780: asking the device for ray queries, and the log line
saying why it is on or off, happen only in a `horizon-extras` build
(horizon-client's feature now enables horizon-client-render's). A plain build
requests the device it requested before, whatever HORIZON_RAY_TRACING says
(`only_an_extras_build_asks_for_ray_tracing`, run with and without the feature).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
names.rs, scoreboard.rs and perspective.rs call the engine dispatch
(name_decoration, player_mark, camera) instead of the cosmetics; the
cosmetics answer from their per-seat state: badges become sprites by id
put in the atlas from the frame hook, covered slots read PlayerModel's
gear, the camera pull comes from the emote that pulled it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-hooks gains Hooks::name_decoration (a Copy NameDecoration: mark
rectangles, a badge cut from the crate's sprite, the lift over the body),
player_mark for the player list, camera (a share of the third-person
distance), and sprites by id: Sprites read while drawing, SpriteStore
filled in Hooks::frame (skin slots, hooks/<hex>). PlayerModel carries
gear and invisibility; Frame says whether the world is the window's own.
horizon-platform re-exports them. The engine fills the store; no crate
answers the new hooks yet (step 3b).
Co-Authored-By: Claude Opus 5.5 <[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]>
Step 1 of #13780 (carrying on the #1180 spike): the device requests wgpu's
EXPERIMENTAL_RAY_QUERY when the variable is set and the adapter has it, and
logs why it stays off otherwise. Nothing draws with it yet; without the
variable the device opens exactly as before. The unsafe opt-in lives in
horizon-hdr, which already holds the GPU calls the game crates can't make.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
studio::EDITOR = cfg!(feature = "horizon-extras"): without it the bars,
camera path, editor mouse and keys and the freecam/keyframe/path and
studio screen commands are off; recording and playback stay (platform).
The default build is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: ParticleLevel::block is World::block with a one-chunk Cell in front (same air fallback), and the other edits (lava smoke, bubble removal, lifetime_alpha) keep their random draws and results. Queue BEFORE 14097 (its Ctx impl sits beside this PR's Ctx<'a, 'l>).
🤖 Agent: packaging only; Cargo.lock has no git sources, so buildRustPackage needs no outputHashes, and nothing the source filter drops (launcher/, nix/, docs pages) is read by the build.
🤖 Agent: the delay is per button identity (row id + kind) with vanilla's strict elapsed > 500 ms, and costs one Instant::now() per frame on the social screen only.
Pure move: open/close/seek/advance, record start/stop/status, the world
snapshot and the record/replay words of /studio and horizon-ctl studio
leave game/studio.rs, which keeps the editor (free camera, path,
timeline, top bar, its own command words). Where playback touched the
editor it calls it: save_studio_camera_track, studio_replay_opened,
studio_replay_advanced, studio_replay_closing, studio_replay_closed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The project decided that mod-API work (native hooks, native, content and
wasm mods, mod builds and the registry) is reviewed and merged on Ari's
fork. Say so where contributors look first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's Polytone item bar colours (#fb354374e3) move behind the new
PackFormats::item_bars hook (glue pack_formats/item_bars.rs, test moved with
them), and environment.horizon_height joins the engine's Environment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lan.rs is over 3000 lines and may not grow (tools/check_file_size.py): the
tracker now goes in on the line that picks the owner's guest, and the
comment moves to playerdata::with_warden.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs is over 3000 lines and may not grow (verify's file-size check):
the join-order insertion moves to join_seq.rs, with a unit test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pure move; poll_stationary_client, poll_remote and poll_remote_events go byte-identical from remote.rs to remote_poll.rs (a #[path] child of remote), only visibility and super::super::server_links change.
Recording and playback are platform, Studio's editor is Horizon
(docs/layers.md, Replays): the driver, the camera type to restore,
standalone and the playback clock move to Replay; Studio keeps the free
camera, timeline and top bar. Callers ask self.replay.playing(); the own
player's tick fraction is Game::own_player_alpha(). No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its own workspace still resolved rusqlite 0.37 -> libsqlite3-sys 0.35,
which bundles a SQLite older than 3.54 (RUSTSEC-2026-0459). rusqlite 0.41
resolves libsqlite3-sys 0.39.1, as the root workspace already does.
rusqlite 0.38 moved the u64/usize conversions behind fallible_uint: turned
on, so the stored values convert as before (an error past i64::MAX).
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 channel of one frame or none was skipped before the mixing loop, so it
never stopped and kept its channel; the inaudible fast path also computed
(frames - 1) in usize, which underflows for an empty sound. Both now stop
it, as the loop does; a step of 0 no longer stops a sound the loop keeps.
- Mixer::camera_epoch was written and never read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Sim::tick_block`'s chest arm is `ChestBlock.tick`/`EnderChestBlock.tick`:
it recounts the openers through `recheck_container_openers`
(`ContainerOpenersCounter.recheckOpeners`), broadcasts the lid event and
re-schedules the five-tick tick while anyone has the container open. The
rows still said only the copper golems' openers were rechecked.
🤖 Agent: LastOutput is now saved as a component, keeping the red, click-to-suggest feedback of a failed command (BaseCommandBlock / ComponentSerialization.CODEC). The case keeps to failed commands, so no new score holder changes the later /scoreboard players list order. command_exec: 19063/19065; the 2 left are the zombie PersistenceRequired pair, which fails the same way on main 8321f11360.
🤖 Agent: a non-list messages now empties the side as SignText.CODEC does; still open: 26.3 also fails a side whose messages are missing or not exactly 4 (listOf(4, 4)), a follow-up.
🤖 Agent: the cushion now takes lava damage at the check interval, as vanilla does; cushion_placement (2852 cases), fall_damage and living_fall pass on a union with main.
🤖 Agent: OptiFine custom sky now reaches the engine only through pack_hooks::PackFormats::custom_sky, resolved once per pack load (the per-frame draw keeps the same layer list and shader). layer plan M6 step 1. verify green on this head; no vanilla behaviour changed (parity.py selects 633 harnesses only because game.rs is touched; none observes pack skies).
#14094 adds EditBox::centered beside text_color and a test at the end of
ui.rs's tests; this PR's field, initialiser and test touched the same lines.
Moved further down so both merge in either order. No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps this PR under the project's 500-line limit (519 -> 468): the ignored
custom_ticker_cost timing test follows in a separate PR. Ctx's field() moves
to the end of its Host impl, so this merges cleanly beside #14089, which
changes the impl's own line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Sim::pickup_block` returns false for a waterlogged barrier unless the
breaker is creative, which is `BarrierBlock.pickupBlock`'s gate; the row
said the rule was not there.
The glass About card's row had Open; building location_row there dropped
it. location_row's action is now Open and Move side by side (only the
glass page builds it: the old settings page is unreachable).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PNG is now written on its own thread, so the first press's file may not
exist yet when a second press picks its name: both took the same path and
two writers raced on one file. screenshot_name also skips the paths of the
screenshots still being written (UiState::screenshot_messages).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Mob::on_climbable` takes an open trapdoor over a matching ladder, as
`climbing::on_climbable` does for the player, and `Game::mount_events`
posts `entity_dismount` for players and mobs alike. Both rows still
described the old gaps.
DefendVillageTargetGoal.canUse finds the villagers through attackTargeting
(forCombat().range(64)), which tests the golem's line of sight to each: a
villager behind a wall no longer sets the golem on the player. The PR body
named this change but the commit missed it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.getColor treats the byte as signed: 16 and above are undyed, and a
negative id reaches DyeColor.byId, whose OutOfBoundsStrategy.ZERO gives
white. Keeping the raw byte (255 for -1) sent it to the undyed texture.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Split::enter loaded a guest's profile before its turn began, so the
crates' settings stores read the guest's lines into P1's seat. The load
now runs on the guest's seat; a new profile is saved on P1's seat (the
stores write P1's values, as the rest of the profile copies P1's) and
its stores are read back into the guest's (Settings::load_stores).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-hooks gains Hooks::frame and Hooks::tick with a plain player-model
view (PlayerModels, BodyPose moves here), re-exported by horizon-platform.
The cosmetics' client, emote feed, owner check and presence live in a
per-seat static behind those hooks; only a window's game calls them, so
the server thread no longer runs tick_emotes. net::Session gets a serial
that names the connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the event is posted only after start_riding_jump accepted the jump, so a refused or unsaddled jump stays silent as in Camel.handleStartJump; the new harness now gates only on its non-diag rows (0b5a2c1). The functional change is ~20 lines, the rest is tests and the harness.
🤖 Agent: isAir plus SnowLayerBlock.canSurvive's two tags (cannot_support first, then support_override, then the full top face), four tag lookups per snow golem tick.
The world pass discarded its depth unless a shader pack, TAA, OIT or a
native mod read it, so a resource pack's post chain that samples
minecraft:main with use_depth_buffer read cleared depth (0, the sky)
everywhere. The freepack's sun glare tests the sky around the sun in
that depth, so it shone through hills and walls. The depth is now kept
whenever post effects run, as GameRenderer hands them the main target.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move that makes room for the next commit: `gfx.rs` is over 3000 lines and
may not grow (docs/layers.md "File size"). `move_off_page` leaves it unchanged, imported back
under the same path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game.rs's inline Studio blocks (bars, camera path, timeline drag, mouse
buttons, free-camera flight) and play_keys.rs's replay shortcuts move into
game/studio.rs methods; the engine reads Studio through replaying(),
flying() and active(). Pure move, no behaviour change.
Co-Authored-By: Claude Opus 5.5 <[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
🤖 Agent: screen_time advances in tick_particles (once per client tick, not per frame) and resets on a different Screen variant; two containers of the same variant share a count, a small gap next to the measured frames.
🤖 Agent: checked line by line: every removed line reappears in entity_nbt/hanging.rs; only mod hanging, the pub use, use super::* and the module doc are new.
🤖 Agent: reads() is conservative (any token naming the varying keeps it), and hz_/horizon varyings always travel, so the only change is fewer locations; ~120 of the 539 lines are new, the rest moved verbatim into translate/varyings.rs.
🤖 Agent: maintenance.auto=false is set only in the test's temporary repositories, and shutil.which("bash") finds the same bash on Linux and macOS, so CI is unchanged while Windows with git 2.54 stops failing.
🤖 Agent: anger_end_time is now the absolute value vanilla keeps (only startPersistentAngerTimer/stopBeingAngry move it), and around_tick costs two compares per NeutralMob tick; the h262 expectation 151→150 is the vanilla value for a NoAI wolf. Parity runs on this head before the merge.
🤖 Agent: with no content mod loaded, a swimming mob's tick no longer scans its box twice or reads each water cell's block again: the mod-fluid scan is gated on fluid::mod_fluids_loaded().
🤖 Agent: canChangeValue in 26.3 is set by setFocused (mouse or Tab) and flipped by Enter, not the drag; the sprites take can_change_value || dragging, so a drag no longer lights the track and a Tab-focused slider keeps vanilla's lit handle.
🤖 Agent: Auto keeps today's HUD at paper white, and an older hdrUiBrightness becomes that share of a hand-set Paper White in nits; main's output_apply test now asks for 150 nits under a reported 250 (0.6).
🤖 Agent: an old horizon-graphics.txt with hdrKey still loads (the parser skips unknown keys, pinned by the new test), and main's hdr_failed_start stays beside it after the merge.
AttributeModifier.amount is a double; going through float_value turned
0.1 into 0.10000000149. The test now uses 0.1 to pin it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
begin_signing does nothing for BookScreen::View, so the catch-all arm
turned that error into a silent success.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game's ten studio_* fields and last_replay_tick become one field, studio:
studio::Studio. Pure gather: every access is renamed, nothing else changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A crate above the engine adds rows to Horizon Settings' Extras page: they
join the page's group the section's title names, else a group of its own.
Auto-reconnect moves out on it next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pure move, no behaviour change: native_rows, the native key wait (the two duplicated Act::NativeKey handlers now call one method) and the key binding go to options_ui_pages.rs, a child module that keeps access to the screen's private types; options_ui.rs 8441 -> 8391 lines.
Part of #13007. Each removed id ran its own retain over the mobs, orbs, dropped
items, falling blocks and primed TNT, and a scan of the passenger map, so a
packet of n ids made n passes over lists that hold thousands of falling blocks
while sand drops. RemoveEntities now removes its ids together: the per-id side
effects keep their order (the dragon's nearest crystal still follows the
crystals one id at a time), and those five lists and the passenger map are
filtered once against the packet's ids.
8,000 falling blocks, packets of 256 ids (debug test build): 26.1 ms -> 1.3 ms
per packet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mob_geometry and mob_geometry_inner, the per-frame drawing of mobs, players and
projectiles, move unchanged into a child module of creatures.rs, so a change
to them (#12276's client_entity_render) no longer grows creatures.rs, which is
over the 3000-line limit. Only the visibility (pub(in crate::game)), the
super:: paths one level deeper, and rustfmt's reflow of two lines follow the
new module.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The revert re-added `const BUFFERS` and a blank line, so the file grew from
5859 to 5860 lines over the 3000-line limit (verify run 42650015942). The
import does the same in no extra line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the riding pickup box is right (Player.aiStep: union with the vehicle's box, inflated 1/0/1). The partial-pickup half was not: Inventory.add returns true once any of the stack goes in, so playerTouch still calls take, awards ITEM_PICKED_UP with the stack's original count and calls onItemPickup. I restored that, using vanilla's count.
`tools/port_map/game2-002.tsv` said `Shulker.readAdditionalSaveData` reads
`AttachFace`, `Peek` and `Color`; the row now names the codec's wrapping and
the two raw bytes the port keeps.
`mob_from_nbt` is what `Game::load_mob` goes through. A shulker with
`AttachFace: 7b`, `Peek: -1b` and `Color: -1b` loads with `variant3 == 1`
(`abs(7 % 6)` is up), `counter == -1` and `color == 255`.
Without the coercion they load clamped: `left: 5`, `right: 1` for the face,
`left: 0`, `right: -1` for the peek and `left: 16`, `right: 255` for the
colour.
`Shulker.readAdditionalSaveData` reads three fields with different rules:
- `AttachFace` is `Direction.LEGACY_ID_CODEC`, whose `from3DDataValue` wraps
the byte through `abs(id % 6)`;
- `Peek` is `getByteOr("Peek", 0)`, a raw byte put straight on the synched
data;
- `Color` is `getByteOr("Color", 16)`, a raw byte too.
Horizon clamped all three to the setters' ranges (`0..=5`, `0..=100`,
`0..=16`): an `AttachFace` of 7 became east (5) instead of up (1), a negative
`Peek` became 0 instead of the byte vanilla stores, and a negative `Color`
became 0 instead of 255.
The load now wraps the face and narrows `Peek` and `Color` to bytes, exactly
as the codec and `getByteOr` do.
`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.
🤖 Agent: the registry-name read goes through SyncedNames, the same builtin+dynamic tables the hub syncs to clients, so a mod sees the names a vanilla client gets; Hub::synced_name and interact_location moved out (file-size rule).
🤖 Agent: the sea level now follows the level's generator through levels.rs's locator swap, so flat worlds send -63 on login and respawn; lan.rs/lan_actions.rs kept from growing with two pure moves (ada4756).
🤖 Agent: the section cache gives the same states as Chunk::block, including air for a missing chunk or section. With the contextual test now on the registry's flag (6ee08b4), the cached shape is used only where collision_shape_with_context would return it too, so each raycast voxel saves an Arc clone and a lookup without changing a hit.
store_owner wrote the tracker into level.player_extra, then playerdata::store
replaced that with the guest's loaded snapshot, losing the warning level and
cooldown. Put the tracker into the owner guest's saved compound first.
Part of #9965
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The project decided that Horizon plays only with a Microsoft account that
owns Minecraft: Java Edition. The launcher loses, in every build:
- the hidden Developer options section (seven clicks on the sidebar
version) and Settings::dev_offline;
- offline names: adding, selecting and playing as one, the game's made-up
name as a player, their skins and capes (skins.rs, skin_page.rs), the
quick switch's and Settings > Account's offline entries, and the CLI's
`account offline`, `account forget-offline` and `account skin`;
- the `--name` and HORIZON_OFFLINE_SKIN* it gave the game for them.
The game plays as the selected account (else the first), and
account::launch_for_game requires that account to own the game itself:
another account's ownership no longer lets it start under a name of its
own. With no network, its last answer still stands, as before.
Files from older launchers still load: accounts.json's `offline`,
`offline_selected` and `looks` and settings.json's `dev_offline` are
ignored on read and gone from the next save, which also deletes the old
names' skin and cape files.
The 28 strings only these used leave the 86 lang files and the
lang_review copies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Early-out for stationary listener in Audio::set_listener (bypasses mutex lock contention completely when camera does not move, ~5.25 ns vs ~36 ns)
- Eliminate redundant distance sqrt in stereo_gains: projections onto orthonormal basis preserve Euclidean distance, so reuse d.length()
- Replace asin in elevation with sqrt(1 - u^2) using identity cos(asin(u)) = sqrt(1 - u^2), avoiding inverse trig entirely
- Single-camera fast path in mix to avoid splitscreen camera search loop in normal single-player / multiplayer mode
- Distance squared early-out for linear attenuation (bypasses basis projections and stereo gains for inaudible sounds outside attenuation radius)
- Fast sample skipping for silent / zero-gain audio frames without decoding interpolation
- Batch channel parameter updates via Backend::update_channel, reducing 3 mutex lock acquisitions per ticking instance to 1
- Batch stopped query via Backend::filter_stopped, replacing N individual channel mutex locks per tick with a single query pass
- Avoid O(N*M) entity loop in Game::start_mob_sounds by collecting bee indices in initial filter pass
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AnvilScreen sets setInvertHighlightedTextColor(false) on its rename box, so a
selection leaves the white text alone instead of painting it black on the
highlight. EditBox always inverted it.
🤖 Agent: a registry version revoked by sha256 may now keep its entry, so a lost signing key can be rotated (baritone #276); the PR's tests, clippy and fmt were clean.
🤖 Agent: pending_uploads drains every frame independently of meshing (world_update's budget loop), so counting it against the mesh room is pure backpressure with no stall path; the room counts columns while the backlog counts sections, which only makes the hold stricter at wide distances.
🤖 Agent: item_bar_color takes a read lock and scans ITEM_BARS per drawn damage bar; with no Polytone item modifiers that Vec is empty, so vanilla packs pay nothing measurable. The two stacked commits are already on main (#13703, #13704).
🤖 Agent: settings store only routes lines at load/save time: with no store registered Settings::load/save read and write exactly what they did (horizon-extras.txt unchanged, no new file); a store's own file keeps lines no store owns, the shared horizon-extras.txt does not (Extras rewrites it).
A page registered with Place::Pause gets a full-width button in the
pause menu, above Options and under Splitscreen's settings, labelled
with its title; it opens the options screen at the page. The split row
and the pages' rows share one list (pause_rows), so the grid, the icons
and controller focus place them alike. With nothing registered the
pause menu is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M5: discord_presence.rs moves to the new Horizon crate
horizon-discord (file move), which the official executable registers on two
new platform hooks: Hooks::client_started (every game window, automated
runs included, never a dedicated server) and Hooks::activity (what the
player does, dispatched on change only, P1's). game/discord.rs becomes the
engine's game/activity.rs, which reports Activity (Hidden while the Discord
switch is off, until the switch moves to the settings store). The discord
Cargo feature goes: horizon-client's own executable has no presence.
discord_presence_e2e.py runs the official executable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The two entry points that bring particles into the engine move unchanged into
a child module, so a change to them (#12277's client_particle hook) no longer
grows particles.rs, which is over the 3000-line limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: discord_presence.rs now uses only horizon_command's GameMode and a max the caller passes (Play::Hosting { players, max }, still lan::MAX_PLAYERS), so the presence client can move to a Horizon crate as a plain file move.
🤖 Agent: insert path and the client's jukebox_takes now read the stack's jukebox_playable like the comparator and song tick (pushed 2fe3429), so a removed component is refused and any item given one is taken, as JukeboxBlock.useItemOn does.
🤖 Agent: rows and the Mined column match ItemStatisticsList (Block.asItem, BlockItem.getBlock) and the comparator's sortOrder × (value, Item.getId); I pushed 8396106b2e: the sort looked each row's registry id up with a linear scan of ~1500 items, several times a frame while the screen is open, now one HashMap per sort.
`ServerRecipeBook.addRecipes` sends one entry per display of a recipe.
`book_entries` had no writer for `crafting_dye` or `crafting_imbue`, so it
fell through to "no display": a learnt leather-dyeing or tipped-arrow
recipe never reached the player's book, though removing it sent the right
Remove packet.
- `crafting_dye` writes `DyeRecipe.display`: the target and the dyes kept
to the DYE component, the `minecraft:dyed` demo of the two, then a
crafting table; the placement is the target and the dye.
- `crafting_imbue` writes `ImbueRecipe.display`: the 3x3 shaped display of
four materials, the `with_any_potion` source, four materials again, and
the result wrapped in `with_any_potion`; the placement puts the source
in the middle cell.
Both are single-display recipes, so they also get their ghost display now.
Covered by unit tests of the decoded entries and by the all-real-recipes
round trip, and the variants parity harness now grants and takes back
leather helmet dyeing and tipped arrows beside map cloning.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
ai::speed_bonus hard-coded 1.5 for baby zombies and piglins, so a baby
whose minecraft:baby modifier was removed with /attribute kept running
fast, and a baby piglin ran at 1.5x where Piglin.SPEED_MODIFIER_BABY is
+0.2. It now applies finalize::transient's MovementSpeed modifier
(ADD_MULTIPLIED_BASE), the one /attribute reads.
Part of #11994
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: matches checkHotbarKeyPressed's getCarried().isEmpty() gate; merged main in, where a double-click test had landed at the same spot, both tests kept.
Horizon's Azure app registration and Mojang's approval of it cover Horizon's
own builds only. Forks and derived launchers register their own app, seek
their own approval, and must not ship or fetch Horizon's client ID.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A registered page's Slider row is one of the options screen's own
sliders (Opt::Page(page, row)): dragging, the arrow keys, narration,
focus and search work as for the engine's options, and the page hears
Act::Slide. Page::tooltip (default none) gives a row's hover text, which
may follow its value.
The registered pages are read from horizon_hooks::pages directly (a
test sets its own per thread), and horizon_page_tooltip moves to
options_ui_pages.rs: options_ui.rs shrinks again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An invite code is eight Crockford symbols: four nameplate symbols the
relay routes by (20 bits) and four secret symbols the host checks (20
bits). The relay sees only the nameplate, so it can count joins whose
nameplate does not exist, not wrong secrets for one that does; the host
already retires a code after five of those.
This adds a budget for unknown codes for one nameplate (5 a minute,
burst 10), spent whichever address asked and checked before the lookup:
without it, one address set can take the whole global unknown budget
for a single short code. The check records nothing for a nameplate it
has not seen; only spending an unknown code creates the entry, so a
join refused by another budget leaves no state behind.
Verified: cargo test -p horizon-relay (31 tests) and clippy
--all-targets with -D warnings.
BookSignScreen sets its 15 character title box centred, so the title and the
caret sit in the middle of the 114 pixel box. EditBox had no centred mode: the
text (and a click on it) started at the box's left edge.
The tunnel extension point moves to horizon-hooks (re-exported by horizon_platform::hooks),
offered by a hook instead of a startup registration. horizon-friends' new game feature holds the
resolver, and horizon-official registers it with the other Horizon parts. The friends harness
runs the official executable. Layer plan M5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client-lib no longer depends on horizon-theme outside its tests
(layer plan M6). menu_style gains the overlays' colours, ui_theme's type
scale and shapes, and its fonts (none given: the overlays draw no text);
ui_theme, the on-screen keyboard, the missing-resources window and the
Windows boot paint read them. horizon_look gives Horizon's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client-render no longer depends on horizon-theme outside its tests
(layer plan M6):
- hud_kit::motion: a Tween takes a cubic-bezier curve; the solver lives
here, tested against the design system's curves.
- The HDR calibration logo is the window icon given at startup.
- The font tests take Geist Mono as a dev-dependency.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
menu_style (horizon-client-render): the colours of the settings pages
(graphics_style) and the HUD kit's tab strip, given once at startup;
vanilla's neutral greys when none is given. graphics_style's and tabs'
palette consts become functions reading it. horizon_look gives Horizon's
(layer plan M6; render -> theme goes in the next PR).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
remote_pushes looked up every mob's player entry and scoreboard team
before testing whether its box meets the player's; effect_swirls worked
out every mob's width and height before seeing it had no effects. Both
skips come first now, for the same pushes and particles. At 10,000 pigs
the client tick takes about a quarter less of the main thread.
Fixes#14053
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: prepare now picks a Minecraft folder that holds every library of the version, which is what the azure-json failure in #13872 was missing; the prepare tests pass.
🤖 Agent: checked against 26.3's bytecode: AbstractContainerScreen's constructor sets skipNextRelease = true, so the release of the click that opened the chest no longer picks up the slot under the pointer. I merged main (it conflicted with #13946's test) and kept both tests.
Entity.collide's getEntityCollisions: a pushed entity collides with the
entities it can collide with (canCollideWith: a boat, a living shulker, a
happy ghast held still), not only with blocks and the world border.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pure move: emotes.rs and emote_wheel.rs keep their bodies under game/cosmetics/, their state joins Cosmetics, and game.rs loses its two emote fields (9 lines shorter)
Maintainer feedback from PR #13906 requested keeping signatures identical and
placing the one-chunk lookup cache directly inside ParticleLevel via
Cell<Option<((i32, i32), Option<&'a Chunk>)>>, avoiding threading _cached twins
and preventing particles.rs from growing beyond base size.
- Store cached_chunk inside ParticleLevel and read through ParticleLevel::block.
- Decouple Ctx lifetime parameter in polytone.rs (Ctx<'a, 'l>) so ParticleLevel's
invariant 'a does not constrain stack evaluation borrows.
- Keep particles.rs strictly under line size limits (5,195 non-test lines vs 5,197 base).
- Passes check_file_size.py, cargo fmt, clippy -D warnings, and particle unit tests.
Since git 2.5x, `git commit` starts `git maintenance run --auto` detached
after every commit, and its default strategy repacks geometrically even
with gc.auto=0. The packs the tests build were folded while they were
counted: 12 built, 3-6 found on the desktop (git 2.54, every run), and
intermittently in CI verify (run 42647063573 on #13970).
The tests now run git with no user or system config, no inherited GIT_*
variables, and gc.auto=0, maintenance.auto=false, no detached gc. The
assertions are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Windows, Auto takes the display's peak from DXGI's MaxLuminance, which is
mostly the display's EDID figure and often runs high. The HDR page still said
the display was already set up ("Calibrate Anyway..."), though calibration's
Peak step works there, since PQ passes through unchanged.
Now the Windows reading marks its peak as a guess (`Reported::peak_guessed`),
and the page's Calibrate row reads "Calibrate..." and says the reported peak is
often too high, while Peak Brightness and Paper White stay on Auto. Auto still
uses the reported values, which beat the 1000-nit default. Once the player has
calibrated or set either value, the row offers "Use Automatic Setup" as before.
There the Peak Brightness row no longer says it is set up for the display:
its Auto is the guess. Paper White, the player's own slider, still does.
Compositor reports (Wayland) and macOS, which reports no nits, are unchanged.
The setup decision moves next to `Reported` (`Graphics::hdr_setup`), out of
graphics_ui.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
poll_stationary_client, poll_remote and poll_remote_events move unchanged
into game/remote_poll.rs, a module of remote.rs, so a change to the loop
(#12271's cancellable packets) no longer grows remote.rs, which is over the
3000-line limit. Only the visibility (pub(in crate::game)) and one path
(super::super::server_links) follow the new module.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: pure move of crack_particle and particle_sprite into the existing game/block_particles.rs, bodies identical. I merged main in (daf537e): main's game.rs with only these two fns removed, after #13856/#13877 moved their neighbours.
The render thread waited for the GPU to finish the screenshot's frame
(poll wait_indefinitely), then converted the pixels and encoded the PNG
inline: at 4K or a Screenshot Quality above 1x, a second or more of frozen
game. F2's and the world icon's readbacks are now mapped asynchronously and
written on a thread of their own; the result reaches the chat a frame or a
few later. Agent screenshots and screenshot-sequence keep the synchronous
path, which hands them the result with the frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fast path resolves the methods without arguments expressions read most (age, life,
custom, x, y, z, temperature, downfall) when the program compiles; a host may answer them by
that Field (Host::field) instead of comparing names, and one that doesn't, or a case it
doesn't cover, goes through the name as before, with the same value. The particles' host
answers a particle's own fields, the colormaps' block host its position and biome climate.
The equivalence test now also runs a host that answers fields.
Custom particle tick (custom_ticker_cost, release): 376 ns before, 344 ns now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Polytone expression ran through the interpreter's Values and Results: about 263 ns for a
firefly's alpha that costs 7 ns written in Rust. An expression of one statement whose every
part is a number (constants, arithmetic, comparisons of numbers, math functions, ternaries,
and root.method(...) calls) now compiles a second time into a tree of f64s. It answers
exactly what the interpreter would: the result's kind (number, boolean, or a method's own
value at the top), method arguments as the interpreter passes them, Value::truthy for
methods in conditions; anything that could tell them apart (strings, == on a method's
value, locals, game functions) stays in the interpreter. A test runs a corpus of
expressions through both and compares every value and every host call.
Custom particle tick (custom_ticker_cost, release): 715 ns before, 376 ns now. A section's
colormap tints (colormap_tint_cost): 178 µs before, 135 µs now, same colours.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
custom_ticker_cost (ignored) ticks a firefly-like custom particle (five expressions with
sines and a named custom) and prints the time a tick takes, about 0.7 µs in release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: this answers #13740's review: the queue is per Game (window and server thread apart), jobs run on their own horizon-mod-worker pool, and panics are contained with the mod named. The fragment stamps cleanly (tests.test_stamp_native_hooks OK), and the test-module reorder in game.rs is what keeps that file from growing.
🤖 Agent: matches TargetingConditions.test: range * getVisibilityPercent floored at 2.0, with the targeter's type passed so mob heads count as they do for the other users of visibility_percent. Moving Tempt and the tests out of goals.rs is a pure move; that file is over 3000 lines and may not grow.
🤖 Agent: reads_only counts any Root or Host expression as reading everything, so a colormap is only shared up a column when its axes can't see the height. Measured 2.16 ms down to 178 µs per section, with the same colour checksum. It carries #13806's commits; land it after that one.
🤖 Agent: everything new runs once, when the mesh tables are built (biome_functions, Colormap::flat), and with no pack setting biome_colors the tables come back unchanged. horizon_height defaults to vanilla's 63, so without a pack the sky disc doesn't move.
🤖 Agent: matches FallingBlockEntity.tick: the (0.7, -0.5, 0.7) landing slowdown happens before the MOVING_PISTON test, and the 0.98 drag at the end of the tick. Neither placement nor cancelDrop runs there, so the block stays falling for the piston to push. The new command-exec group is now in CI's parity run.
🤖 Agent: own_inventory() reads the player's Inventory.items 0-35 straight from the synced inventory, so it answers the same with a chest up or the window's own inventory, where menu_stacks() is None; the client test checks hotbar slot 0 with a menu open. main merged in (docs conflict beside slot_roles).
🤖 Agent: the 1000 ms hold matches LoadingOverlay.isReadyToFadeOut's fadeIn gate, counted from spawn rather than from the first drawn frame (a frame's difference). The new test slept 50 x 1 ms against an 80 ms hold, which a loaded runner can overrun, so I pushed 308bf3d: it holds for an hour, waits for the worker, then lifts the hold.
🤖 Agent: verify checks out with fetch-depth 0 and runs on refs/pull/N/merge, so HEAD^1 is the main it lands on; pull_request.base.sha goes stale after a push, which is the false size failure in #13874.
🤖 Agent: vanilla's paths don't change with no mod fluid loaded (holds() falls back to the caller's tags, add_mod_tags adds nothing). add_mod_tags does run on every entity's fluid update, but it's a 5-row scan then.
Splitscreen players share one process and thread, so a crate's statics
are shared by them. The engine now says whose turn it is
(horizon_hooks::seat(), re-exported in horizon_platform::hooks; 0 for
P1), set where split.rs switches turns and while a joining player's game
starts, so settings stores, pages and HUD layers can keep per-player
state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the launcher and horizon-server now reach the engine only through horizon-platform (feature-gated re-exports, so horizon-telemetry still builds without horizon-world); import paths only, no behaviour change; 7 layers.json exceptions removed.
🤖 Agent: no change without a pack, since vanilla 26.3 ships no textures/entity/lead.png and the walked entity/ folder only picks one up from a pack; the per-row UV split keeps both faces' rows consistent (near and far quads swap top and bottom).
🤖 Agent: matches ChatListener.showMessageToPlayer and FilterMask.applyWithFormatting, including narrating the filtered text; one difference left on purpose: vanilla's nextClearBit can run past the text's end and print extra #s for a mask longer than the message, Horizon stops at the text, which no real server sends.
🤖 Agent: Items::tags_only gave the sword max damage 0, so the anvil test checked nothing past that point. With the full table its trim and enchanting parts run for the first time, on prepared data only (CI has none).
horizon_hooks::hud (re-exported as horizon_platform::hud): a crate above
the engine registers a Layer that draws on a Canvas (size, tr,
text_width, text, fill, place). The extras' HUD pass draws the layers
after the minimap, PvP widgets and mods' boxes, stacked in the same
corners; a corner the debug screen covers gives None. With no layer
registered the engine builds no canvas.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: checked against 26.3's isPauseScreen overrides: SocialInteractionsScreen is false and the game-master block editors inherit Screen's true, so the table now matches; one match over every variant also makes a new Screen variant a compile error instead of a silent 'keeps ticking'.
🤖 Agent: pure move (only save_level and tick_world_icon gain pub(super)); the 4 port-map rows follow the code, and it stacks cleanly with #13887 and #13908 on main.
🤖 Agent: checked with --color-moved: the only lines that aren't moved are the pub(super) prefixes on sync_tick_chunks and tick_moving_pistons; it merges cleanly with main and with #13907/#13908.
🤖 Agent: the plain shadow_water program is now translated on every pack load, even when the GPU takes the patched one; that's load time only, and the full-outputs GPU test shows the fallback only kicks in at the device's output limit.
🤖 Agent: a blocking shield now loses durability from the stack it actually holds after a save round trip (item_nbt), and the dispenser harness fixture checks it; the merger waits for CI parity on this fork head.
🤖 Agent: anim::setup keeps its None fast path; an emoting player costs one dyn BodyPose call per pose, and set_playing only reallocates when the emote changes, not every tick
🤖 Agent: GitHub's +1605 is stale. With #13623 merged, the real diff against main is gfx.rs and output_apply.rs, +433/-26. The grace costs one Option check per update_output and a single extra display read at the 500 ms deadline, so a frame with HDR off pays nothing. It also merges cleanly together with #13810.
🤖 Agent: reusing #12913's swept bubble-column pass for the piston move is the right shape: moveEntityByPiston ends in applyEffectsFromBlocks, so the push gets onInside/onAboveBubbleColumn as the projectile's own move does. I swapped drain(..).collect() for mem::take to clear clippy; the new harness runs in CI parity.
🤖 Agent: /DEPENDENTLOADFLAG:0x800 makes Windows load the launcher's import DLLs from System32 only, and tools/test_launcher_dll_search.py checks the flag in the built PE so a build change can't drop it.
The flake built only the launcher, for Linux. It now also builds the game's programs from this
workspace, with nixpkgs' rustPlatform and Cargo.lock, on x86_64-linux, aarch64-linux and
aarch64-darwin (nix/):
- horizon-engine: horizon-client --no-default-features, the headless game (no ALSA or udev);
- horizon-server, with horizon-engine linked beside it, where the server looks for the game;
- horizon-prepare, with a JDK 25 as its default JAVA_HOME and its root (research-lock.json, the
exporters' sources) in share/: a wrapper copies that root somewhere writable unless --root is given;
- horizon-client with horizon-ctl, the libraries wgpu and winit open at run time on its rpath.
The launcher and the dev shell stay as they were, on Linux. docs/nix.md says how to use them. Nothing
of Mojang's is fetched or packaged. Part of #13841.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WorldOptionsScreen gives portEdit the PORT_INFO_TEXT tooltip, so hovering the
LAN port box says what it is; Horizon drew no tooltip for it. The box stays
visible with LAN off (only its editing stops), so the tooltip shows there too.
PlayerEntry sets TOOLTIP_DELAY (500 ms) on its report, mute and show
buttons, so their tooltips appear only after the cursor has rested on the
same button for half a second (WidgetTooltipHolder). Horizon's social screen
drew the row button's tooltip on the first hovered frame.
social_screen::TooltipDelay now tracks the hovered Tip and its start time and
answers whether the tooltip shows; the screen's draw passes the current tip.
BookSignScreen's constructor sets its title box to titleValue, the text saved
when Cancel left the screen. Horizon reopened the sign screen with an empty
box while its title field still held the old title, so the next keystroke
started from nothing and Enter signed a title the box did not show.
BookScreen::begin_signing now fills the title box from the kept title, as the
constructor does, and both ways into the sign screen use it.
A pure move, for the file-size rule: spawn_egg_offspring,
spawn_egg_fluid_hit, spawn_egg_allowed, use_spawn_egg and
apply_spawn_egg_entity_data go to game/creatures/spawn_eggs.rs. Only
visibility and the `super::` paths change. creatures.rs goes from 8896
counted lines to 8732.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
After main filtered fake players out of the spawn anchors, building the
level from that list would drop them from natural_level too; main builds
the level from every player and SpawnPlayer from the filtered list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Util.shuffle in ServerChunkCache.tickChunks draws from the level's random
whether or not a category may spawn, so skipping it moved every later draw
of that random off vanilla's. The candidate cache keyed on player x/z and
the chunk count went stale when a chunk's ticket level or entity loading
changed under a player standing still (an AFK farm). Both go; the allocation
savings (streamed Counted, reused SpawnPlayer buffer, the single-player
collect) stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_hooks::pages (re-exported as horizon_platform::pages): a crate
above the engine registers a Page (rows: heading, note, toggle, cycle,
button, key) with its entry on Horizon Settings. The options screen draws
its rows with its own widgets, asks for them only while the page is open,
and hands presses, Shift-presses and bound keys back to the page. With
nothing registered, Horizon Settings is unchanged.
The Horizon Settings rows move to options_ui_pages.rs with the entries,
so options_ui.rs shrinks (8391 -> 8363 lines).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An animation with mode screen_time runs over the ticks the open screen has been open,
stretched over time_cycle_duration, and holds its last frame once that has run: measured on
the real mod with a recipe book button of four frames over 200 ticks (the first at 2 s,
the last at 10, 18 and 27 s). The GUI atlas's animated sprites now read the modes too
(time of day, weather, screen time), as the terrain's did; their expression modes still
run on ticks. animate_sprites moves to hud/animate.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The name test (scaffolding, powder_snow) missed the liquid rule, so with
an entity's context a lava block took its cached empty shape, where
collision_shape_with_context gives a strider the lava's stable shape.
Read the registry's contextual flag instead, as collision_shape_with_context
does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With get_mut, an idle hopper never recorded the tick it ticked, so a
hopper that had already ticked this tick and then received an item from
another hopper took cooldown 8 instead of 7 (HopperBlockEntity.tryMoveInItem
skips one when its tickedGameTime >= the source's). Restore the per-tick
entry and the original prune; the other optimizations stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move, for the file-size rule: painting_to_nbt, attached_block_pos,
painting_from_nbt, item_frame_to_nbt and item_frame_from_nbt go to
entity_nbt/hanging.rs, re-exported with `pub use` so callers stay as they
are. entity_nbt.rs goes from 4830 counted lines to 4726.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob in water read every cell of its box a second time each tick (the mod fluid scan),
and each vanilla fluid cell of its current once more (mod_fluid_at), with no content mod
loaded. Both are skipped now: the scan when the table holds no mod fluid, the second
read for a vanilla cell.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- graphics.rs: main's hdrFailedStart arm stays; this branch reads hdrUiBrightness as an
older file's share of Paper White in its own arm.
- graphics_ui_hdr_tests.rs: main's held-HDR tests and this branch's UI Brightness test, both.
- gfx/output_apply.rs (new on main): its UI brightness of 0.6 is now 150 nits under the
reported paper white of 250.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move out of options_ui.rs (8441 lines, may not grow): the native
mod's page rows, its key wait and its key binding go to
options_ui_pages.rs, a child module, so the page registry can land there
(docs/layers.md, Platform registries).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main added Graphics::hdr_failed_start beside hdr_key: the field, its default, its
parse arm and the round-trip test keep it, without the shortcut key this branch removes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: layout now collects a Vec per label even for measure, one small allocation per text per frame; a two-pass over unformatted() would avoid it if a menu ever shows up in a profile.
🤖 Agent: the switch to the creative screen was in set_game_mode, which runs on the server thread; the window changes mode through the game event (set_mode_id), so it never saw it. I moved the call into set_mode (pushed 470f9d0), which both paths reach. The way back (CreativeModeInventoryScreen.containerTick) is still a gap. Also, the PR body is the literal text @/tmp/hudbodies/b-inv2.md: the body file never got read.
🤖 Agent: a name given as {translate:...} is shown as the raw key (component_text keeps the key, creative_tab_title never translates it); small follow-up. tag_match scans exported_search per item, fine since it runs only at rebuild.
🤖 Agent: only test_friends.py (600 s) and test_native_client.py (60 s, frame-limited) pass --timeout-seconds today, so no other harness changes behaviour.
🤖 Agent: checked against ChunkGenerator.findNearestMapStructure's bytecode: ring loop outside, placements inside, return on the first ring with a hit; the concentric result already in best plays vanilla's starting nearestDistance.
🤖 Agent: custom_particle_options decodes more of ParticleTypes.CODEC than /particle's own parser in commands/world_cmds.rs; that command could reuse it later. Block/item/vibration types still fall back to the potion swirl, as the body says.
26.3's AbstractSliderButton sets canChangeValue in setFocused (focused with the
mouse or Tab) and flips it on Enter (keyPressed); getSprite/getHandleSprite read
only that flag. Horizon's can_change_value is that flag for the keyboard, and a
mouse drag focuses the slider with it set. So the sprites take both again:
a Tab-focused slider keeps its plain track and lit handle, an arrow-focused one
lights its track, and a drag no longer lights the track (the bug this PR fixes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the pot adapter only ever sits behind hopper_insertion_view, so generic discovery (comparators, pulls) stays untouched; a pot still has no modeled block entity, so hopper extraction from a pot remains a gap for a follow-up. Overlaps #13800 only in is_full/move_in_item signatures.
They only re-project captures kept on the author's machine and start no
engine, so the test_*_parity.py name would register a harness CI cannot
run. The Rust fix and its unit and guest-death tests stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: confirmed tick_end_flash builds SoundInstance::directional and directional_position adds the 10-block view offset, so the row now cites the real port instead of its caller's caller.
🤖 Agent: confirmed end_predictions calls block_meets_player then snap_target/snap_player_to (remote.rs:2967-2969, unit-tested at 12726), so the old gap note was wrong.
🤖 Agent: checked on main: animate.rs has only SpriteAnimator::animate_with now, so both rows pointed at a function that no longer exists; TextureManager.tick as inline matches its own note.
horizon_hooks::settings (re-exported as horizon_platform::settings): a Store
names its file (horizon-extras.txt or its own), the keys it owns, and loads
and saves its lines. Settings::load and Settings::save route the lines; a
shared file gets the stores' lines after the engine's, an own file keeps the
lines no store owns. The hooks list and the registries share one Registry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M5: official_servers.rs moves (pure file move plus its hook) to
the new Horizon crate horizon-official-servers, which the official
executable registers through the platform's hooks: Hooks::started now gets
the game folder (Started::game_dir), and horizon-platform re-exports
servers.dat as horizon_platform::servers. Its addresses come from
horizon-addresses: the withdrawn partner's joins the list file, so its
allowed entry in tools/addresses.json goes. horizon-client's own executable
(plain builds, harnesses) no longer touches servers.dat at startup.
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]>
🤖 Agent: lands GabrielNichols' #11446 (fuel alternatives in the Recipes cooking card); its seven JEI sprites go into jei_sprites.rs and the fuel list's wheel shares the bundle's check, so hud.rs and game.rs don't grow.
The corpus declares diag- rows as recorded diagnostics of separate
differences (the /ride mount event, RootVehicle); the PR itself reports 6
of them still differing, so exiting 1 on any difference made the new
harness fail on every automatic parity run. They stay in the report.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M6, a pure move before the chat filter's engine hook: /tempban (engine) and /tempmute
share parse_duration, duration_parts, duration_text and now_millis, which now live in the engine's
horizon-server-files (duration.rs). horizon-chat-filter re-exports them. Behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemAttributeModifiers is a component, so a patched stack replaces the item's
prototype modifiers. Horizon's mob attack and armour comparisons read
ItemInfo.modifiers, ignoring the stack's own.
Test: a sword with a patched attack_damage +5 reports that modifier.
horizon-client-input's window code no longer embeds Horizon's icon: the
startup gives it (window::set_icon_png), and with_horizon_icon becomes
with_icon. horizon-client-input no longer depends on horizon-theme outside
its tests (layer plan M6).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemStack.enchant uses EnchantmentHelper.updateEnchantments, which returns
EMPTY when the component is absent. Horizon added the enchantment to the
modelled list anyway, so enchant_randomly and enchant_with_levels enchanted a
stack whose !minecraft:enchantments patch removed the component.
Test: a sword with !minecraft:enchantments stays unenchanted after enchant;
an enchanted book with !minecraft:stored_enchantments does too.
StonecuttingRecipe.assemble creates the result from ItemStackTemplate, so a
result's components (a data pack's custom name, damage, ...) belong on the
output. Horizon built the result from its id and count only.
Test: a stonecutting recipe whose stone result names it "Cut Stone" yields a
named stone.
hub.rs is over 3000 lines and may not grow (tools/check_file_size.py); the
method moves unchanged to a file of its own beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-render-pipeline is an engine crate, and the config it held is the
shaders/ format's buffer and shadow configuration (colortex buffers, Iris
half-lives, shaders.properties settings). docs/layers.md: the engine never
holds a shader or pack format vanilla doesn't read; pack formats and the
shader API are platform. The M6 shader-pack move restarts from a spec.
This reverts merge 75b85d258f.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move, for the file-size rule: `impl Minecart`'s to_nbt/load go to
game/minecarts/nbt.rs, `CartIndex` and `cart_at` to game/minecarts/index.rs.
Only visibility changes so minecarts.rs still reaches them. minecarts.rs
goes from 3230 counted lines to 2967.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The translator gave every vertex output a location, read or not. Under GL the linker drops an
inactive varying, so packs declare more than they pass on: a deferred pack's shadow program
writes 30 vertex outputs and reads 17, which left no room on a GPU with 31 inter-stage locations.
A pack's varying its fragment stage never names now takes no location; a vertex output stays a
global of the vertex stage. Ours (the built-ins, the boats' patch place) always travel. The
assignment moves to translate/varyings.rs (mod.rs is over the file-size limit).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bare "bash" is WSL's on Windows, and git 2.54's automatic maintenance
after a commit folds the packs the tests build, so the suite failed on
Windows (and would with a newer git anywhere).
game.rs: main moved the neighbouring item and hand helpers out (#13856,
#13877); kept main's file and removed only crack_particle and
particle_sprite, as this branch does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep PingRequest through held/handover filtering, handle it in Hub transport,
and exclude it from action-time updates. Preserve the existing reverse Pong path.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
input_keyboard_used, input_made_pointer_event, input_player_replaced,
input_takes_stack_drop and input_hud_begin replace gameplay's direct writes to
the pad's state.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new test was inserted between the getRedstoneSignal doc comment and the
test it documents. It now also checks that a lectern whose state has
has_book=true reads the book signal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's check_file_size no longer counts test modules, so moving the two
inline test modules out no longer offset the new sea_level lines; it added
#[path] lines instead. The tests are back inline, and two self-contained
functions move to child modules unchanged (pure moves):
level_event_except to lan/level_event_targets.rs and horizon_channel to
lan_actions/horizon_channels.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
JukeboxBlock.useItemOn asks the stack for JUKEBOX_PLAYABLE, so the insert
path and the client-side jukebox_takes now read the stack the same way the
comparator and the song tick do. An inline song definition is playable but
has no id to play.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeutralMob.addPersistentAngerSaveData writes anger_end_time as
readPersistentAngerSaveData read it; only startPersistentAngerTimer and
stopBeingAngry change it. Horizon re-stamped it from the countdown on
every read, so a past end read back as -1 and a mob that missed ticks
moved its end forward.
The end time now stays as loaded while the countdown agrees with it or
nothing changed it, and the stopBeingAngry sites write -1 (#13076).
An arrow that bounces off an entity (deflect REVERSE) never leaves the level
in vanilla, so it keeps its place in EntityTickList. Horizon's projectile
pass drops it and finish_arrow_hit adds it again with add_projectile, which
appended it behind every projectile already waiting: those then ticked
before it. add_projectile now puts a projectile that has already joined back
at its join position; a new one still goes last.
Found with the Orbital Skybender: the arrow that reads each digit missed
every other wind charge, so only half the digits were read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
50 one-millisecond sleeps could pass the 80 ms hold on a loaded runner and
fail the test; hold for an hour, wait for the worker, then lift the hold.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractArrow.move checks shouldFall after every Entity.move with a mover
other than the arrow, including a push limitPistonMovement or the collisions
cut to nothing. #10243's release ran only in apply_push; it is now
release_after_block_move, called after every push.
Harness: tools/test_piston_arrow_fall_parity.py; unit test
a_stuck_arrow_whose_push_a_wall_stops_falls_when_nothing_holds_it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
set_game_mode runs on the server thread; the window changes mode through
the server's game event (set_mode_id), so the switch to the creative
screen now lives in set_mode, which both reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's clip queries the block shape and the fluid shape of every
block it visits. The vehicle ray skipped the water on a tagged block, so
a waterlogged ladder or vine never fixed the shared water shape there as
vanilla's first query does; the mob, vehicle and item rays now share one
helper that queries the water first, as the player's does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A success's feedback names the player with an entity hover, a new score
holder (which changes a later scoreboard players list) or a bracketed
value, whose components the two engines build differently. The case
keeps the two failures, whose styled feedback is what the save must keep.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The verify job compared big files against this branch's old base, so main's own growth of options_ui.rs since then failed the size check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 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]>
SignText.CODEC reads a side's messages as a list; any other tag fails the side
and leaves SignText.EMPTY. Horizon ignored the bad field but still applied the
side's colour and glow.
Inventory.add returns true once any of the stack went in (count < the
count before), so ItemEntity.playerTouch's take, ITEM_PICKED_UP stat and
onItemPickup run on a partial pickup too. The stat counts the stack's
count before the add, as playerTouch's local count does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RandomizableContainer.tryLoadLootTable reads LootTableSeed with getLongOr,
which takes any numeric tag. Horizon's loot_seed matched only a Long tag and
rolled an unseeded container from its position instead.
A project whose signing key was lost (horizon-mods#276, baritone) can't keep
its old versions listed: the index checks every version against the entry's
one key. The review refused removing them even after revoked.toml took their
files down by sha256. Now such a removal is a note, not an error; a version
removed without a take-down by hash is still refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge move/jei-sprites (#13977): this PR's seven sprites go into jei_sprites.rs instead of hud.rs. The fuel list's wheel joins the bundle's in game.rs's container scroll (both take the wheel before the screen's own scrolling), so neither big file grows (docs/layers.md#file-size).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve static launcher imports from System32 before program entry and
exercise launcher/setup startup beside unrelated DLLs in release checks.
Co-Authored-By: Codex <[email protected]>
(cherry picked from commit 6d913502bf)
Main's own tool cases joined the case list beside the blocking ones. The
branch had replaced lab.game_env with os.environ for the game it starts,
which test_game_home (every script that starts the game uses game_env)
refuses: the game then wrote into the real HORIZON_HOME. --game-dir and
the .exe name on Windows stay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A launcher folder with 26.3's version JSON and jar but a missing library
(an official launcher that never finished downloading) was picked over a
complete copy in another launcher (the CurseForge app), and preparing
failed on the missing jar (#13872). game_files now tries folders with every
client library first, then the old order, so the error still names the
library when no folder has them all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Union of both sides: main's JEI notice (it already names the sprites), main's button/search sprites and settings button beside this PR's fuel sprites, tooltip and popup; en_us.json keeps main's keys and adds the three fuel strings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.move casts a ray along any move of a block or more
(ClipContext.Block.FALLDAMAGE_RESETTING, a tagged block as a full block;
ClipContext.Fluid.WATER) and a hit resets the fall before the move's
drop counts. Mob::do_move never cast it, so a mob falling fast past a
vine kept its whole fall and landed hurt (or dead).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Gameplay reads the analog move vector, held mappings and the view turn, and
reports what the player may feel (Feedback), through input_move, input_holds,
input_look and feedback in play_input.rs. Only these four bodies name the pad
modules, which answer today; Feedback is an engine type the pad maps to Rumble.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A release with an empty hand throws nothing whether skipNextRelease is set or
not; only a carried stack makes mouseReleased click, which is the hand that
opens a chest while holding one.
moveEntityByPiston's applyEffectsFromBlocks: a projectile a piston pushed
through a bubble column gets Projectile.onInsideBubbleColumn /
onAboveBubbleColumn, as after its own move.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/emotes.rs and game/emote_wheel.rs move to game/cosmetics/, their state into
Cosmetics (one Horizon field on Game), and the name mark comes from game/cosmetics.rs:
only game/cosmetics* names horizon_cosmetics now (layer plan M5, preparing the move
to a Horizon crate). No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
friends_screen.rs moves from horizon-client-lib to game/friends/screen.rs, beside the host and
join glue; the pause menu asks friends_shown() and names the icon from game/friends (or
friends_off). Pure move, layer plan M5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractContainerScreen's constructor sets `this.skipNextRelease = true`, so
a mouseReleased with no press before it on that screen returns before any
slotClicked. That is the release of the click that opened the screen: a
chest's right-click sends its UseItemOn, the server opens the menu, and the
button goes up with the screen already open, over whatever slot the pointer
was on. Horizon's Clicks started with the flag false (and cancel_drag cleared
it), so that release picked up the slot now under the pointer.
Clicks::default and cancel_drag now start a screen with skipNextRelease true,
as a fresh AbstractContainerScreen does.
monitor() moves from the game's telemetry module to horizon-client-input's
window module (engine), with display() giving its size, refresh rate and
scale as plain data. horizon-ctl status reads it there; telemetry's
note_display keeps the same values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M5 (preparation): the game mode comes from horizon_command, and
the party's size from game/discord.rs (Play::Hosting's max) instead of
crate::lan, so the presence client can move to a Horizon crate as a pure
file move once the seam has an activity hook. No change for players.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
layer plan M6 (pack formats), step 3: the terrain's emissive overlays take
the pack's suffix from pack_hooks (OptiFine's emissive.properties, _e by
default); with no pack formats installed a pack has none. atlas_sources
checks a texture path stays in the pack root itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
layer plan M6 (pack formats), step 2: the colours a pack replaces (dyes,
sheep, redstone, effects, the experience number, entity shadows, the
effect fogs) are an engine table (pack_palette::PackColors) that
Polytone's colors.json fills through pack_hooks; the polytone_colors
re-export goes away.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
goals.rs is over the 3000-line limit, so the goal moves to mobs/goals/tempt.rs (a pure move) and the visibility
scaling lands there: TemptGoal reads TEMPT_RANGE with TargetingConditions' two-block floor.
Part of #11857. reset_inventory_clicks and player_menu_sound join game/menu_clicks.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #11857. visual_ticks, texture_clock and visual_millis join game/render_tick.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #11857. bucket_hit and bucket_target (BucketItem.use's place position) move to a new game/buckets.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #11857. save_level and tick_world_icon (GameRenderer.tryTakeScreenshotIfNeeded) join game/saving.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
layer plan M6 (pack formats), step 1: the renderer's custom sky layers are
engine types (crate::custom_sky: Blend, Draw, Conditions, the SkyLayers
trait); OptiFine's sky format fills them through crate::pack_hooks, a hook
the platform installs at startup. The OptiFine glue moves to pack_formats/,
which only it names horizon_optifine and moves to a platform crate later.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The section #13011 landed predates #13847 and this branch; it now says
which test code the check skips.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They hold the tests moved out of a file (`#[cfg(test)] mod tests;`), and a
normal build doesn't compile them, the same reason inline test modules
don't count. The failure message sends new tests to a sibling
`<file>_tests.rs`, yet two such files were already over the limit
(mod_content_tests.rs 5,268 lines, level/tests.rs 4,175).
Fixes#13892
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The engine keeps the friend: address form and the join; a resolver registered at startup
(net::tunnel) reads invite codes and opens the tunnel. Play with Friends registers its own
(game/friends/tunnel.rs) under the friends feature. Layer plan M5.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::emote holds a trait object the engine defines (anim::BodyPose) instead of
horizon_cosmetics::Playing; game/emotes.rs wraps Playing in it. horizon-client-lib
no longer depends on horizon-cosmetics (layer plan M5).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M5 (preparation): Horizon's entries in the multiplayer list get
their own module in horizon-client-lib, gated by a new lib feature
horizon-extras (horizon-client's horizon-extras turns it on). It touches
servers.dat only through horizon_protocol::servers, whose new lock() replaces
multiplayer.rs's private SAVING mutex, so the module can later move to a
Horizon crate as a pure file move. A plain build now neither adds the
official server nor carries the withdrawn partner's address.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M6: horizon-platform gains feature-gated re-exports (version,
no_ai, prepared_files always; world, protocol, prepare, server-files behind
features) and the two Horizon-layer crates use them instead of depending on
horizon-core, horizon-prepare, horizon-protocol, horizon-world and
horizon-server-files. Seven layers.json exceptions go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
UI Brightness was a share of Paper White, 25 to 100%: the HUD and menus could
only be dimmer than the world's white, and they moved whenever Paper White
did. It is now set in nits, as HDR games and mods set it: Auto (the default)
puts the HUD and menus at the paper white in use, as 100% did, so nothing
changes on screen by default; a value set by hand, on Paper White's steps
(10 to 480 nits by 10), holds them at that many nits whatever Paper White is,
below or above the world's white. The row reads like Paper White's
("UI Brightness: Auto (203)", "UI Brightness: 160") and shares its row code
and steps with it and Peak Brightness.
HDR output still takes a share of paper white, now UI nits over paper white
nits (resolve_output); its 0.25..1 clamp goes, so the HUD can be brighter
than paper white. The shader and the HUD composite are unchanged.
horizon-graphics.txt saves hdrUiNits, 0 for Auto. An older file's
hdrUiBrightness reads as Auto at 100% or with Paper White on Auto (its nits
aren't in the file), and otherwise as that share of the hand-set Paper White
in nits, to the nearest step. Calibration doesn't touch UI Brightness.
The frame-identity tool's float_visbuffer session pins a 200-nit paper white,
so its older file's 50% reads as exactly 100 nits and builds from before draw
the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Introduce Host-driven task offloading API for native mods (mod_async_worker).
Tasks run on a dedicated mod worker pool ('horizon-mod-worker') isolated from chunk
and terrain generation. Panics in mod background jobs and deferred main-thread callbacks
are caught and reported per mod via crash attribution and contain without crashing pool
workers or the main game loop.
Callback queues belong to individual Game instances (window and server thread in single
player), reached exclusively through Host without global ACTIVE_QUEUE state or test races.
LivingEntity.handleFallFlyingCollisions in vanilla 26.3 compares the
horizontal movement vector passed to move() with the delta movement left
after move() (where restitution zeroes out colliding axes), not the
position delta travelled vs initial velocity.
Using post-move velocity prevents the crash sound from repeating on
subsequent ticks while pressed against a wall, while still playing the
single impact sound in both survival and creative mode.
Fixes#12304
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
The old test asserted the drag outline; it now checks the four cases vanilla
distinguishes (hover, drag, keyboard focus, focus plus drag) and that an
inactive slider lights nothing.
AbstractSliderButton.getSprite is `isActive() && isFocused() &&
!canChangeValue` and getHandleSprite is `isActive() && (isHovered ||
canChangeValue)`, where canChangeValue is the mouse drag. Horizon drew the
track outline while dragged (vanilla suppresses it then) and never on
keyboard focus, and lit the handle on keyboard focus, which vanilla leaves
plain.
Part of #11857. sync_tick_chunks, tick_moving_pistons and tick_scheduled join game/block_ticks.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Items::tags_only has no items, so the anvil part of
prepared_vanilla_data saw a diamond sword with max damage 0: not
damageable, no repair, cost 0 where vanilla gives 1. The test now loads
Items::load, and the whole test passes on freshly prepared data.
Fixes#13883
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shadow pass's translucent terrain takes one vertex output more for the boats' water patches
(the vertex's camera-relative place). A pack whose shadow program already writes as many as the
GPU takes had that program refused, and with it its translucent terrain's shadows. It now gets the
program without the patches instead (`Post::shadow_water_plain`), with a note: the water's light
shows in the boats there, as before the patches, and the rest of the shadow pass stays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
commands.rs is already over the 3000-line limit and may not grow, so the
screen swap moves to game/game_mode.rs beside set_mode, which is where the
mode's own effects live. The call site is one line, and the file ends a line
shorter than the base.
The targeting tests move to mobs/goals_targeting_tests.rs, so goals.rs (over 3000 lines) shrinks from 3740 to 3611
while the new tempt-range test joins them.
verify checks out GitHub's merge commit, but these three checks compared
it with pull_request.base.sha: main at the pull request's last push. As
main moves on, main's own changes since then counted as the pull
request's, so pull requests failed for files they never touched (on
#13027's merge: 17 files flagged, 16 not in the PR), and a file main had
shrunk could grow unnoticed. HEAD^1 is the main the merge commit puts
the pull request on. Pushes to main run as before, without a base.
Fixes#13874
Co-Authored-By: Claude Opus 5.5 <[email protected]>
InventoryScreen.containerTick replaces its screen with a
CreativeModeInventoryScreen once the player has infinite materials, so the
player stays in a menu. Horizon closed the inventory instead, which lost the
screen a creative player expects after /gamemode creative. The spectator path
still closes it (vanilla has no branch there and leaves it open).
LoadingOverlay.isReadyToFadeOut holds a mid-game reload's overlay while
Util.getMillis() - fadeInStart < 1000L, so a pack that stages in a few frames
still shows the progress for a second instead of flashing for one or two.
drive_pack_reload applied the staged pack the moment its worker finished.
Part of #11857. particle_sprite, crack_particle (ClientLevel.addBreakingParticles) and transform_particle join game/block_particles.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fake holder (#cb) created here stays tracked after its objective is
removed, and the later /scoreboard players list then lists the holders
in another order than vanilla's hash map. Horizon's open-hash order for
that insertion is a separate bug; this case is about LastOutput.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ColorParticleOption keeps the colour's alpha (ARGB_COLOR_CODEC: an int's
top byte or a fourth float); colour lists have a fixed length and are
packed to 8-bit channels (ARGB.colorFromFloat); dust scale, sculk_charge
roll and shriek delay are required, and dust scale must be in [0.01, 4];
a spell's colour defaults to white. What fails to decode falls back to
the potion swirl, as vanilla's failed decode leaves no custom particle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pot_hopper needs --side and --work-dir, which an automatic parity run
doesn't pass. tests/test_pot_hopper_contract.py pinned the corpus row
counts and the harness's own compare, not game behaviour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's creative_tab_modifiers change a creative tab's icon, its name (text and colour),
and its items: removals by the wiki's item predicates (or, and, not, tag_match,
items_match, itemstack_match by item, id_match, true), then additions at the end or
before or after the first item a predicate matches. Measured on the real mod (icon and
red name on the building blocks tab, with the inventory opened through XTEST).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BaseCommandBlock.save writes LastOutput with ComponentSerialization.CODEC:
the time-stamped feedback component with its colours and click events.
Horizon wrote its plain characters as a string.
Fixes#13077
Co-Authored-By: Claude Opus 5.5 <[email protected]>
respawn_world_point_tests.rs.orig is main's copy of the test file; the
spawn-info Python fixture and its Java decoder match no harness name, so
no runner ever calls them. The Rust test covers the sea level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The HDR page loses its Shortcut group and row: a key that opened the
graphics screen at HDR from the game. No other graphics page has a key of
its own, and the row was out of place there. The game no longer watches for
the key, and Done after that screen no longer has a way back into the game
that only the key used.
The hdrKey setting goes from horizon-graphics.txt. An older file's hdrKey
line loads like any unknown key, without error, and the next save drops it.
Its three strings go from every language file; nothing else used them.
Horizon's other key binds (sort, zoom, emotes) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A villager's POI search (AcquirePoi, occupied POIs, homes) visited each
section of 9x9 columns through PoiSections::get: a mutex lock, a hash
lookup and an Arc clone per section. PoiSections now keys its lists by
column, and PoiSections::column takes the lock and finds the column once,
handing back only the sections that hold POIs, in the same order.
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]>
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]>
The mesher's pack colormaps (the freepack's grass and leaves) sampled every block of a
section, each with its own biome zoom, through SipHash maps. Now:
- a section of one biome skips the per-block zoom (as vanilla tints already did);
- a colormap whose expressions don't read the height (no o.y) is sampled once per column
over one biome (Program::reads_only says what an expression reads);
- the tint caches use the Fx hasher, and expression calls keep up to four arguments on
the stack instead of a Vec.
Same colours (checksum of the timing test unchanged). One section, 16 layers of 256 tints
with a biome-id and noise colormap at Biome Blend 2 (release): 2.16 ms before, 178 µs now;
one layer 274 µs before, 138 µs now (mesher::tint_tests::colormap_tint_cost, ignored).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The sky's dark disc draws below a pack's horizon height instead of vanilla's 63 (flat worlds
keep their floor), measured on the real mod; sky_render_distance changes nothing there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
biome_colors.grass/foliage/dry_foliage/water (a colormap or a flat colour) colour every
block tinted by that function, under the block modifiers' own colormaps; without it, a block
modifier's colormap on grass_block, oak_leaves or leaf_litter (or a fluid modifier's on
water) becomes the function, as the wiki says Polytone does. Measured on the real mod:
biome_colors.grass also wins over a dark forest's special grass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HDR Lighting is one thing: all of Horizon's own lift above paper white, light
sources, the sun and the lit surfaces. HORIZON_HDR_LIGHTING=0 switched off only
the surfaces' part, a second switch with another scope; it is gone, and the
setting does the job. docs/graphics.md now describes the setting.
Under a shaders/ pack, and under a 0.2 pack on the float frame, Horizon lifts
nothing, so the row did nothing there. It is now greyed and says "The shader
pack lights the world itself." Under a 0.1 pack on the 8- or 16-bit frame the
last pass still lifts the light sources and the sun, so the row stays live
there, as it does for a pack that is refused, missing or whose mod is off. The
page reads this from the renderer's output, which now carries whether the
running pack lights the world. HDR's own reasons still come first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FallingBlockEntity.tick neither places, drops nor discards a block that
comes down where a piston is extending, and checks that before cancelDrop:
it slows like any landing and stays a falling block, so the piston pushes
it on. Horizon took the block out of the list before the landing returned
Landing::Continue, so the block was lost.
Fixes#13793
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, and the sea level field adds a line in each. Hub's
spawn_info moves to lan/hub/spawn_info.rs; three inline test modules
move to their own files, unchanged.
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 guest's START_RIDING_JUMP handler moves beside the
mount code it calls, and the camel test module is declared there too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A settings change at the end of Auto's grace (a slider, F11) applies
the output before the frame's own display poll. It ended the grace on
the report read when the grace started, so a display that had come
back meanwhile still turned HDR off, and the frame's poll after could
no longer confirm it: the timer was gone.
The output now reads the display before the grace ends, whichever comes
first, the frame's poll or a settings change. A read taken since the
deadline counts, so each end of the grace reads the display at most
once. The two-second poll is unchanged otherwise, and Wayland's events
need no read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The row pointed at tick_end and said the flash sound has no position. The
10-block directional position is implemented and the flash sound uses it
from tick_end_flash.
The note said the player is not moved when the restored block would hold
them. end_predictions checks block_meets_player and snaps through
snap_target/snap_player_to.
LecternBlock.getAnalogOutputSignal returns 0 unless the block state has
HAS_BOOK, before it asks the block entity for its page signal. Horizon used
the block entity's book alone, so a lectern whose state says it has no book
still fed a comparator.
BrewingStandBlockEntity.loadAdditional reads speed_multiplier with
ValueInput.getFloatOr, which takes any numeric tag. Horizon only accepted a
Float tag and defaulted every other numeric form to 1.0.
JukeboxSong.fromStack reads the stack's own minecraft:jukebox_playable
component, and a removed component names no song. Horizon resolved records
through the item's default component only, so a disc that overrode or removed
the component played, or stayed silent, against the stack in the slot.
With HDR on Auto, a display that wakes, a window moved to another
display, or a moment around Win+Alt+B can report SDR or nothing for one
read and HDR again at the next. Each such read turned HDR off at once
and on again: two swapchain reconfigurations, and on the layered float
frame a new renderer and a remesh, so the player saw black flashes and
a stall for nothing.
Now, while Auto presents HDR and the surface still offers that
encoding, a report that turns false or unknown keeps HDR on for 500 ms,
at the paper white, peak and black last applied. The applied output,
the shader Frame, the settings' active state and the frame the game
asks for stay as they were. A positive report before then clears the
timer with no swap; a loss that lasts turns HDR off once. Where the
system is polled, the display is read again at the deadline rather
than trusting a read up to two seconds old; where it sends events, the
deadline is served without one. The two-second poll stays otherwise.
Off and On act at once, entering HDR is never delayed, and an encoding
the surface no longer offers is never kept: the output switches to the
one offered, or SDR, at once. The timer is each renderer's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A control-socket run started with --timeout-seconds never stopped when its
harness died. The flag now applies to every run; a frame-limited run keeps
its 120 s default and a run without either has no limit. --help lists it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SulfurCube defines MAX_FUSE (an int, -1 until primed) before FROM_BUCKET
(a boolean), right after AbstractCubeMob.ID_SIZE (18). Horizon sent and read
them the other way round, so a 26.3 client tracking a sulfur cube got a
boolean at 19 and disconnected (#13721). The server now sends the fuse at 19
with vanilla's -1 default and the bucket flag at 20, and the client reads
them there; a fuse of -1 is not primed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uploads take a few ms of each frame. At a wide render distance and a low frame
rate they fall behind, and meshing kept going: a heavy modpack at 64 chunks held
4.3 GB of finished meshes in pending_uploads during a flight, enough to swap a
24 GB Mac. The room for new mesh jobs now subtracts that backlog.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A resource pack's lang can give strings `§` colour codes ("§6Options...")
and private-use characters its `font/default.json` maps to bitmap icons.
Geist printed the codes literally and drew tofu for the icons. `Ui::layout`,
shared by `measure` and `text_line`, now drops `§` codes like
`StringDecomposer.iterateFormatted` and draws a character Geist lacks with the
pack's default-font glyph: vanilla's 9px line scaled onto Geist's, or at GUI
scale when the whole label is such glyphs, as the pack draws it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 5 (second part) of #6301: a mob's fluid scan finds the mod fluid it is
deepest in (a mod liquid block holds its fluid only in the fluid table). The
mob moves in it as in water, floats and rises in it when can_swim
(Mob::floats now holds the brains' and FloatGoal's shared condition), loses
air when can_drown, is put out when can_extinguish, is pushed by its own
current at motion_scale when can_push_entity, and has its fall distance
scaled by fall_distance_modifier. Mob currents no longer pick a mod fluid by
its template.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 5 (first part) of #6301: each mod fluid gets its own tag in
EntityFluidInteraction, as NeoForge tracks each FluidType, so it is not
water to the game. Its current pushes the player, items, orbs, armour
stands and boats by motion_scale when can_push_entity; the player moves in
it as in water, swims and rises when can_swim, loses air when can_drown, is
put out when can_extinguish, and has the fall distance scaled by
fall_distance_modifier. Unset numbers are the template's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An item modifier's bar_color of the damage type (an <item>_bar.png alone, or a json with
bar_color "damage" and its png) colours the durability bar by the stack's damage: the first
pixel undamaged, the last at full damage, rounded, as measured on the real mod. The item
colormap (tint) changes nothing on the real mod's Fabric 26.3 build, so the freepack's
grass item modifier already looks the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client decoded the player chat packet's `FilterMask` but only used it
to check the signature: on a server that filters chat, a fully filtered
message was still shown and narrated, and a partly filtered one showed
without its `#` masking.
As `ChatListener.showMessageToPlayer` does, a fully filtered message is
now hidden (and counted as processed, like a blocked sender's), and a
partly filtered one shows and narrates
`FilterMask.applyWithFormatting(signedContent)`: each masked UTF-16 unit
a dark grey `#` with the `chat.filtered` hover.
Fixes#13674
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's entity/lead.png textures the leash: each of its 24 segments, from the leashed
end, takes its row of the texture, unshaded, as measured on the real mod. Without one the
leash keeps vanilla's brown checker. The custom heart containers (polytone/heart/container_*)
change nothing on the real mod's 26.3 build.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Minecraft.pause is `hasSingleplayerServer() && Gui.isPausing()`, and
Gui.isPausing is the open screen's isPauseScreen. Horizon kept that answer in
a matches! list beside pauses_game instead of on the Screen, and two screens
disagreed with 26.3. SocialInteractionsScreen overrides isPauseScreen false,
so the world keeps running behind it; Horizon paused. StructureBlockEditScreen,
JigsawBlockEditScreen and the two test block editors inherit Screen's true, so
opening one freezes a single-player world as a command block's screen already
did; Horizon left them out of the list.
The answer is now `Screen::is_pause_screen`, one match over every variant, and
pauses_game asks it. The book in hand, the win screen and a dialog still answer
from their own state where they live.
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]>
AbstractContainerScreen.checkHotbarKeyPressed answers nothing unless
`this.menu.getCarried().isEmpty() && this.hoveredSlot != null`: with a stack
on the cursor, keySwapOffhand and keyHotbarSlots send no slotClicked at all.
The mouse hotbar keys asked `carried.is_none()` at their call site, but the
keyboard path (Game::inventory_hotkey from Screen::keyPressed) did not, so
pressing 1 or F over a slot with a stack carried sent SWAP. The menu's SWAP
branch does not look at the carried stack, so it moved the hovered slot's
item into the hotbar slot while the stack stayed on the cursor.
The guard now lives in `container_screen::hotbar_swap`, the Rust counterpart
of checkHotbarKeyPressed, and `inventory_hotkey` asks it for the off hand and
the nine hotbar slots.
`make_campfire_smoke` made both of its particles with the engine's
`create`, which skips `ClientLevel.doAddParticle`'s limiter, so with
Particles on Minimal or Decreased, or a campfire more than 32 blocks
away, a smoking (doused) campfire still drew every plain `smoke` puff.
As `CampfireBlock.makeParticles` does, the cosy/signal smoke now goes
through `addAlwaysVisibleParticle(.., true, ..)` (`add_with(.., true,
true)`: always made, but it still rolls the Particles option) and the
plain smoke through `addParticle` (`add`).
Fixes#13663
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Items tab keyed its rows on the raw `minecraft:mined` stat key, a
block id, so wheat, redstone wire, tripwire, wall torches and other blocks
whose item has another id got a row for a non-item with "-" under Mined,
while the real item (wheat seeds, redstone, string, torch) showed no mined
count. Rows now take `Block.asItem` of a mined block (air dropped), and a
block item's Mined column reads its block's count, as vanilla's
`StatsScreen.ItemStatisticsList` and `ItemRow` do.
Sorting follows `ItemRowComparator`: "-" counts as -1, equal values are
ordered by item registry id, and the first click on a column sorts
biggest first (it sorted smallest first).
Fixes#13664
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ArmorStand.hurtServer breaks a stand through brokenByPlayer on a second
player hit within five ticks, and at once for an always_kills_armor_stands
type: the stand item (with the stand's name) pops at its block, then
brokenByAnything plays the sound, rolls the loot table and pops the
equipment. Horizon's punch dropped the item and equipment without the
loot table, and /damage refused every can_break / always_kills type.
Part of #12514
Co-Authored-By: Claude Opus 5.5 <[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]>
`tools/port_map/animal-1.tsv` had no answer for `Bee.addAdditionalSaveData`
or `Bee.readAdditionalSaveData`. Both now name `mobs/bee.rs::to_nbt` and
`from_nbt`: the hive and flower positions, the nectar and sting state, and
`NeutralMob`'s `anger_end_time` and `angry_at`.
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A bee angry at the player (`Bee.angry_at =
Some(Who::Player)`, its timer 500) saves `angry_at` as the offline player's
UUID; the reloaded bee has the same player target and the same timer. A calm
bee writes no `angry_at`, even over a compound that still names one.
Without the write the save has no `angry_at` (left `None`, right `Some(IntArray([...player...]))`),
and the reloaded bee has no target (left `None`, right `Some(Player)`).
`Bee` implements `NeutralMob` and its `addAdditionalSaveData` ends with
`addPersistentAngerSaveData`, which writes `anger_end_time` and `angry_at`
(the UUID of the entity the bee is angry at). `readAdditionalSaveData` reads
both back with `readPersistentAngerSaveData`.
Horizon modelled the bee's target in `Bee.angry_at` and saved only
`anger_end_time`, so a bee that had stung a player came back after a chunk
reload with its anger timer still running but no `angry_at`:
`BeeBecomeAngryTargetGoal` asks for `Some(Who::Player)` there, saw none, and
the bee stopped attacking, while an anger at another mob was forgotten the
same way.
The save now writes `angry_at` as the target's UUID (the player's own, or the
level's answer for a mob id, nothing when there is no target), and the load
reads it back as the player or as a mob the level may not have loaded yet.
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]>
host.own_inventory() (native_own_inventory) gives the 36 slots of the
player's own inventory (0-8 hotbar, 9-35 main) with their components,
whichever menu is open. menu_stacks() is None for the window's own
inventory, and living.equipment only covers armor and the held item.
Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_011R7zRUK5udBTJmDgHhoXY2
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
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]>
A mod hearing packets through native_client_packet gets registry references
as numbers (a Stack's item and components, AddEntity's type, Sound,
LevelParticles) and could not name them. native_registry_name(registry, id)
resolves one against the ids the connection uses: the built-in registries,
those the server synced and a mod's own data components. On a server it asks
the hub's SyncData the same way.
Helper-only fragment registry_name.
Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_011R7zRUK5udBTJmDgHhoXY2
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]>
Explicitly unlock when a preparation finishes so a descriptor retained by
a forked child cannot extend the lock lifetime. Cover retained duplicates
while preserving the existing contention test.
Co-Authored-By: Codex <[email protected]>
2026-11-03 07:36:34 -06:00
826 changed files with 45095 additions and 16371 deletions
7. [Moderation and appeals](#7-moderation-and-appeals)
8. [Conflicts and hard cases](#8-conflicts-and-hard-cases)
9. [The age rule (18+)](#9-the-age-rule-18)
10. [The agents](#10-the-agents)
11. [Accounts and the handover path](#11-accounts-and-the-handover-path)
12. [Money](#12-money)
13. [Transparency](#13-transparency)
14. [Rules that are never voted on](#14-rules-that-are-never-voted-on)
15. [Changing this document](#15-changing-this-document)
## 1. Principles
- **Equals.** Every council member has one vote. Nobody, the founder included, stands above the others.
- **The founder.** Ludovic keeps the title of **Founder**: his role stays at the top of the member list and he is
credited as Horizon's founder everywhere. The title is symbolic. It carries no extra vote or power: on the council
he counts exactly like every other member.
- **Evidence, not popularity.** You get a role by doing the work first. Kira measures that work: merged pull
requests, reviews, triage, support answers and days active. A vote then confirms it.
- **Small, clear powers.** Each role can do a short, written list of things. Anything else goes to the council.
- **Roles are on loan.** Every role has a 30-day probation and an inactivity rule. Stepping down for inactivity is
not a sanction, and you can come back.
- **Public by default.** Polls, results and rules are public. Only matters about a person stay private, and even then
the result is published.
- **18+ for roles of trust** (section 9).
- **Mojang's and Microsoft's rules come first.** They are never put to a vote (section 14).
## 2. The levels at a glance
From the most open level to the most trusted. A person can hold several levels at once: a group lead can also be on
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; 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
owner, Discord server owner, the Microsoft app approved by Mojang, the domains, Railway, money). He carries out the
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
Anyone in the Discord server or on GitHub. Members follow the server rules and Horizon's
[contributing guide](CONTRIBUTING.md). They can propose anything: an idea in #horizon-ideas, a bug in #horizon-bugs,
an issue or a pull request on GitHub, or a request for a council poll (any council member can pick it up).
### Contributor
- **Discord:** the 🛠️ Contributor role writes in #contributors and in the threads of #polls-and-decisions. A rank
role (⛏️ Iron at 10 merged pull requests, 🥇 Gold at 40, 💎 Diamond at 100, 🟪 Netherite at 200, counted over every
public horizon-rust repo) shows how much has been merged. Ranks only go up.
- **GitHub:** no repository role. Anyone can `/claim` and `/unclaim` their own issues.
- **Alone:** their own work.
### Group member
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. 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. A moderator gives it, and takes it back, through Kira.
### 🛡️ Moderators and the Chief of Mods
- **Discord:** time out members, delete messages, manage threads and nicknames, read the audit log, quarantine
through Kira, and see the staff channels. **Moderators don't ban.** Bans are done by council members and the Chief of
Mods. Kira's automatic actions (the honeypot, clear scams, illegal images) stay as they are.
- **Official server:** server operators (ops) are moderators in game. They need the 18+ rule and are confirmed the
same way (section 6, Server and SMP).
- **Alone:** a timeout, a deleted message, a quarantine, with a reason in the mod case.
- **The Chief of Mods** is the first contact for moderation questions. They organise the mods, can ban, and can take a
moderator's role away at once when there is a problem. The council confirms or reverses that within 72 hours.
### 🧭 Group leads
Section 6 says what a lead decides alone and what goes to the council.
- **Discord:** in their group's channel only, delete messages, manage and pin threads, and ping the group role. The
🧭 lead role carries no server-wide permission.
- **GitHub:** triage on the issues and pull requests that carry their group's label (`/label`, `/unlabel`, `/close`,
`/reopen`, `/unclaim`), from a list of group leads that grants no repository role. Leads merge only if they are on
the Horizon Team too.
### 🌌 Horizon Team
The Horizon Team **merges pull requests**. Its rules are in [the release flow](docs/release-flow.md): read the whole
diff, never merge your own pull request, and use `needs-proof` for anything risky.
- **GitHub:** member of the `horizon-team` team (write access to the Horizon repos), on the trusted reviewers list
(`/nightly` and triage commands).
- **Discord:** the 🌌 Horizon Team role, #horizon-team, and writing in #contributors and the read-only channels.
Moderation permissions belong to the moderators and the council: a Team member moderates only if they also hold
one of those roles.
- **During the 30-day probation:** they merge only ordinary pull requests. They don't merge `needs-proof` pull
requests, don't approve files that CODEOWNERS guards, and don't use `/nightly`.
### 🏛️ Council
- **Discord:** the 🏛️ Council role: the moderators' permissions plus ban and mention @everyone; #council (private);
starting polls in #polls-and-decisions.
- **GitHub:** the `council` team. Over time it takes over the founder's CODEOWNERS review and his owner powers in the
release tools (section 11).
- **Alone (any one member):** start a poll; pick up a member's request for a poll; do anything their other roles
allow.
- **Any two members together:** an emergency action (section 4.6).
- **Everything else** is decided by poll.
## 4. The council and its polls
### 4.1 Founding council
kacpherek, levzzz5154, rdbtCVS, Online5277, dezer-x and Ludovic: six members, one vote each. The founding members
don't go through the filter or a probation. They do make the 18+ declaration (section 9). The council has at most
**9** members. It adds a seat only when a 2/3 poll says it needs one.
### 4.2 How a poll works
| Rule | Value |
| --- | --- |
| 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; 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 |
| Two-thirds | Yes is at least 2/3 of the Yes and No votes, **and at least 3 Yes votes** |
| Tie | Nothing changes |
| No quorum | Nothing changes. Kira reminds the members who haven't voted 12 hours and 2 hours before the end |
| About yourself | You don't vote, and you don't count in the council's size for that poll |
| Asking again | A poll that failed can't be asked again for 7 days, unless two members agree that new facts justify it |
### 4.3 Which threshold applies
| Simple majority | Two-thirds |
| --- | --- |
| 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 |
| 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 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
A member doesn't vote on a poll about themselves: their own role, their own removal, an appeal of their own
moderation action, or money paid to them. They say so in the poll's thread and Kira leaves them out of the count.
### 4.6 Emergencies
For **security** (a leaked secret, a malicious pull request or mod, a hijacked account), **Mojang or Microsoft** (a
request from them, a risk to the approved sign-in) or **legal** matters (a takedown, the law), **any two council
members act at once**. They may:
- pause releases and the Nightly;
- take a mod down from the registry;
- remove someone's roles or access for now;
- lock or hide channels and content;
- ask the account holder to rotate a secret or a key.
They record it with `/emergency` in Kira, and both of them confirm it there. Kira opens a council poll at once
(simple majority) to confirm it. If the poll doesn't pass, the action is undone where it can be. An emergency never
adds members, changes rules or spends money.
### 4.7 Removing a council member
A 2/3 poll of the other members, in #council. The person concerned doesn't vote and doesn't count in the quorum.
Only the result is published, never the reasons. Other ways to leave: resigning, inactivity (section 5.5), or a
false age declaration (section 9).
### 4.8 When the council drops below a quorum
- **Fewer than 5 members:** Kira opens nominations for the council at once. The council fills seats within 30 days.
- **Fewer than 3 members, or fewer than 3 who voted in the last 30 days:** an **interim council** forms. It counts the
remaining council members, the Horizon Team and the group leads past probation. Its only task is adding council
members, by its own 2/3 poll with a quorum of half. Any two of its members can take emergency actions. All other
decisions wait. The Team keeps merging and the leads keep leading.
- **Nobody left:** the Team and the group leads form the interim council in the same way.
## 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. Their body then elects them (section 5.3).
### 5.1 What Kira measures
| Signal | What counts | Source |
| --- | --- | --- |
| **Merged pull requests** | Pull requests merged in a public horizon-rust repo. For Modding, also in the mod API's fork (arismoko/horizon). Counted overall, and per group by label or by files touched | GitHub search, as the ranks are counted today |
| **Reviews** | Others' pull requests you reviewed: approved, asked for changes, or left a review with comments. One per pull request | GitHub search `reviewed-by:` and Kira's GitHub webhook |
| **Triage** | Triage commands run (`/label`, `/close`, duplicates), issues labelled or closed, bug posts routed on Discord | The triage workflow's comments and Kira's webhook |
| **Support answers** | #horizon-support posts you answered that ended solved | Kira's support feature |
| **Active days** | Distinct days with any of the above, or with messages in #contributors or a group's channel. Chat elsewhere doesn't count | Kira |
| **Tenure** | Days since your first counted contribution (since you joined the server, for moderators) | GitHub, Discord |
| **Clean record** | No timeout, quarantine or ban in the last 90 days | Kira's mod cases |
| **Verified GitHub** | `/verify-github` done | Kira |
| **18+ declaration** | Made (section 9) | Kira |
Kira turns these into an **evidence card**: the person's numbers, overall and in the group's area, for the last 60
days and in total. Anyone can see their own card with `/evidence`. A nomination shows the card publicly.
### 5.2 The floors
A nomination is refused below these floors.
| Role | Floor |
| --- | --- |
| **Group lead** | Verified GitHub; tenure of 30 days or more; **in the group's area: 8 or more merged pull requests, or 20 or more reviews** (Translations: 5 or more merged translation pull requests; Server and SMP: 8 pull requests, or 30 days as an official server operator with activity); 15 or more active days in the last 60; clean record; 18+ |
| **Moderator** | In the server for 30 days or more; Kira level 10 or more; 15 or more active days in the last 60; clean record; 18+ |
| **Chief of Mods** | Moderator for 30 days or more, past probation; 18+ |
| **Horizon Team** | 🥇 Gold rank (40 merged pull requests); 20 or more reviews of others' pull requests; tenure of 30 days or more; 20 or more active days in the last 60; clean record; verified GitHub; 18+ |
| **Council** | Horizon Team member, group lead or Chief of Mods for 60 days or more, past probation; 20 or more active days in the last 60; clean record; 18+ |
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 Elections
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
Some valuable people don't show up in the numbers, such as a designer for the website. The council can **waive the
tenure or the area floors** by a 2/3 poll that says why. The age rule and the clean record are never waived.
### 5.5 Inactivity
Kira checks every day. It warns the person in a DM and in #council, then posts the step-down in
#polls-and-decisions with neutral wording ("stepped down: inactive"), and removes the Discord role. The account
holder or an org owner removes GitHub access when the Team role ends.
| Role | What counts as activity | Warning | Steps down |
| --- | --- | --- | --- |
| Group lead | A counted action in the group's area, or messages in the group's channel | Day 21 | Day 30 |
| Horizon Team | A merge or a review | Day 45 | Day 60 |
| Moderator, Chief of Mods | A moderation action, or messages in the staff channels | Day 21 | Day 30 |
| Support | An answer in #horizon-support | Day 21 | Day 30 |
| Council | A vote in a poll. At 30 days with no vote, while at least 2 polls were held | Day 23 | Day 30, or 45 days with no counted activity at all |
- **Away.** Anyone can announce an absence with `/away` for up to 45 days, once every 6 months. Away days don't
count. A lead names someone from the group to cover for them.
- **Coming back.** Within 6 months: a simple-majority poll, with no new probation. After 6 months: the normal path.
### 5.6 Terms
- **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).
## 6. Theme groups
### 6.1 The groups
| Group | Covers | GitHub | Discord |
| --- | --- | --- | --- |
| **Modding** | The mod API, native and content mods, mixins, the mod registry, `cargo horizon-mod` | The mod API lives in Ari's fork **arismoko/horizon**. Its owner runs that repo by their own rules; the council governs only what is Horizon's: horizon-rust/horizon-mods, horizon-mixins, horizon-native-mod-template, the registry. Label `mods` | The 🧩 MODDING category (#modding, #horizon-modding-team); roles 🧩 Modder (picked) and 🧩 Horizon Modding Team (the lead gives it) |
| **Shaders and rendering** | The renderer, shaders, shader packs, OptiFine and Polytone pack formats | Label `rendering`; `crates/horizon-client-render` (gfx), `*.wgsl`, `horizon-shaders`, `horizon-shader-api`, `horizon-optifine`, `horizon-polytone` | #shaders-rendering |
| **Performance** | Frame rate, memory, server tick, benchmarks | Label `perf`; `horizon-bench`, the benchmark tools | #performance |
| **Launcher** | The launcher: installs, updates, instances, its pages | Label `launcher`; `launcher/` | #launcher |
| **Server and SMP** | Dedicated server, hosting, the anticheat, multiplayer, and the official SMP | Labels `server`, `multiplayer`; `horizon-server*`, `horizon-anticheat`, `lan/`, `dedicated*`; horizon-rust/horizon-server | #server-smp (plus #join-server, #server-chat) |
| **Translations** | The game's and the launcher's languages | Label `roadmap:languages`; `launcher/lang/`, `crates/horizon-client/assets/horizon/lang/` | #translations and the 🌍 language channels |
| **Website** | playhorizon.dev | horizon-rust/horizon-site; label `website`. The site's design stays as it is unless the council decides otherwise | #website |
| **Vanilla accuracy** | Parity with vanilla 26.3: harnesses, oracles, the port ledger | Labels `parity`, `parity-regression`, `full-parity`; `tools/test_*_parity.py`, the Java oracles | #vanilla-accuracy |
| **UI** | The game's menus, HUD, settings, controller navigation, the design system | Labels `ui`, `accessibility`; `title/`, `settings_shell.rs`, `hud*`, `ui.rs`, `docs/design-system.md`, `horizon-theme`. The launcher's look is shared with Launcher | #ui |
A pull request can belong to two groups. Both leads can comment, and the Team merges as usual. If the leads disagree,
the council decides.
### 6.2 Leads per group
- **One lead per group.** A group with 8 or more active members may also have **one co-lead**, chosen the same way.
- A person leads **at most two groups**. Council members may lead a group, though a lead from outside the council is
preferred: it spreads the work.
### 6.3 What a lead decides alone
Inside their group's topic, and within this document, the contributing guide, the vanilla-first rule, the layers and
the design system:
- the group's priorities and the order of its roadmap items;
- which issues are `good first issue`, and triage of the group's issues;
- 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 starting elections in the group.
### 6.4 What goes to the council
- the rules, money, accounts and secrets;
- who is on the council, the Team or the mods;
- anything that changes vanilla behaviour or turns a Horizon-only feature on by default;
- breaking a public promise: the mod API, server compatibility, save and file formats;
- disputes between groups;
- anything involving Mojang, Microsoft or the law;
- speaking for the whole project outside the group's topic.
### 6.5 A group without a lead
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 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 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 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
| Case | What happens |
| --- | --- |
| Two groups disagree | The leads talk first. If they still disagree, the council decides (simple majority) |
| A lead's decision is contested | Any two active group members or one council member can bring it to the council |
| A Team member merges against the rules | Any council member can revert the merge. The council can remove the member (2/3) |
| A council member breaks the rules | The council decides without them. Removal takes 2/3 (section 4.7) |
| Someone holds a role and is under 18 | Section 9 |
| The council can't reach quorum | Section 4.8 |
| The founder and the council disagree about one of his own accounts | Until a step of section 11 hands it over, the account stays his. He carries out the council's decisions on project matters, or he says publicly that he won't |
## 9. The age rule (18+)
**Who must be 18 or older:** members of the council and the Horizon Team, trusted reviewers, group leads and
co-leads, the Chief of Mods and the moderators, official server operators, and anyone with admin rights anywhere in
server panels, Railway, or Kira's admin side. The same goes for anyone who holds a project secret.
**Who doesn't have to be:** members, contributors (with any rank), group members and 🤝 Support. Contributors under
18 are welcome and valued. They just can't hold the roles above.
**How it's checked, without collecting ID:**
- **A declaration.** Before taking such a role, and once at the start for everyone who holds one, the person presses
a button in Kira: "I confirm that I am 18 or older. I understand that a false declaration removes all my roles of
trust." Kira keeps only the Discord account, the date and the wording's version. It never stores a birth date, an
ID, a photo, or any outside age-checking service's result.
- **No declaration, no role.** At the start, current role holders have 7 days to make it. Until they do, their role is
paused.
- **When there's doubt** (the person says they are younger, or there is a credible private report), one council
member asks the person privately. If the doubt remains, any council member pauses the role, and the council decides
within 72 hours in #council.
- **A false declaration** removes every role of trust at once, and the person can't hold one for 12 months.
- **Privacy.** Age is never discussed in public. A role that ends for this reason ends with the same neutral line as
any other step-down.
## 10. The agents
Horizon uses AI agents: the founder's orchestrator, which runs coding agents, and Kira, the Discord bot.
- **They obey council decisions only:** a passed poll recorded by Kira, a recorded emergency action of two council
members, and the standing rules in this document and the project's docs.
- **The only exception is the founder's own things:** his spending, his machines (they run CI and the vanilla tests),
his personal accounts and his time. On those, his word decides. This list should get shorter over time (section 11).
- **Role work isn't a decision.** A lead asking Kira to route issues, or a Team member asking an agent to prove a pull
request, is that person using their role. The agents help, within the role's powers.
- **They check the source.** An agent acts on a decision only when it finds it in Kira's decisions log
(section 13). A message saying "the council decided", or a screenshot, is not enough.
- **They post as agents.** Everything an agent posts says it comes from an agent, and states decisions as the
project's.
## 11. Accounts and the handover path
Today the founder holds every account. The aim is for him to hold fewer, in an order that never leaves the project
unable to act, and never puts an account that is tied to a person into anyone else's hands.
| 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 |
| **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 |
| **Domains** (playhorizon.dev and the others) | The founder's registrar account | Not without moving it | Auto-renewal on, registered for several years ahead. They move only to a legal entity, if one is created |
| **The Microsoft app approved by Mojang** (sign-in) | The founder's Azure app registration; Mojang approved it after his application | **No.** The approval was given on his application. Moving the app or adding owners would change who Mojang deals with, and we have found no documented way to transfer an approval | **Never moves** without Mojang's written agreement. The only change considered: a legal entity, if one is created, and only if Mojang agrees |
| **Signing** (Apple developer certificate, release signing keys) | The founder's | The Apple certificate is in his legal name: no | The Apple certificate never moves. Release keys stay in GitHub's protected environments, and the council team approves their use from step 2 |
| **Money** (sponsorship, donations) | Personal accounts of the founder | Through a fiscal host | Section 12 |
| **The founder's machines, AI accounts and password vault** | His | No | His own things: they never move. The project should depend on them less over time |
**Order:** first the safety nets that remove a single point of failure (a second organisation owner, the bot's
developer team, panel sub-users). Then the day-to-day powers (CODEOWNERS and release tools to the council team). Last,
whatever needs a legal entity: domains, money, and with Mojang's agreement the sign-in app. Each step is a 2/3 poll,
and the founder carries it out.
## 12. Money
*Placeholder: the options (Open Collective and others) are being researched.*
- The council approves every expense by a **2/3 poll**.
- **Every income and every expense is public.**
- Nobody votes on money paid to themselves.
## 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. 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
#polls-and-decisions.
- **Who holds which role:** the people page of playhorizon.dev's hub shows the council, the Team, the group leads,
the Chief of Mods, the moderators and Support.
- **#council** is private for discussion and for polls about people. Everything the council decides is published.
## 14. Rules that are never voted on
These are Horizon's conditions for existing, not choices. No poll can change them:
- Playing needs a **Microsoft account that owns Minecraft: Java Edition**. There is no play without one.
- **No Mojang files** in Horizon's repos or downloads. Players' own copies stay on their own machines.
- **No gambling**, in any form, including with in-game currency.
- **Mojang's and Microsoft's terms and Usage Guidelines** apply to everything Horizon does.
- **The law**, and the terms of service of Discord and GitHub.
## 15. Changing this document
A 2/3 council poll, then a pull request that a Team member merges. The council reviews the floors' numbers
(section 5.2) once a year, or sooner if they turn out wrong.
It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public-tree check and the vanilla harnesses your change affects. Never commit game files, decompiled source or prepared resources.
How Horizon is run (the council, theme groups and their leads, the Horizon Team, moderation): [GOVERNANCE.md](GOVERNANCE.md).
## Project status
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the table below says what the harnesses compare.
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`.
description="The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
# The settings stores of the crates above the engine (docs/layers.md, "Platform registries").
horizon-hooks={path="../horizon-hooks"}
horizon-cosmetics={path="../horizon-cosmetics"}
# The GUI atlas's pixels, kept compressed once uploaded (hud.rs `pack_pixels`).
"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."
Loaded 100 of 826 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.