ServerWatchdog's report only had the last 50 log lines, so a tick stuck
for 60 s on the official server (#15349) never said where. The watchdog now
interrupts the server thread with a signal (Linux, Android, Apple) whose
handler captures that thread's backtrace, and the report carries it as its
stack, as vanilla's report carries the server thread's stack. A thread that
does not answer in 5 s, or a system without the signal, is named in the
report's details instead.
Refs #15349
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AGENTS.md gets a short "Don't lose time" section from measured agent sessions and CI: check for duplicates first, find code with the map, cargo check while working, no polling loops, one test per changed behaviour, don't hunt unrelated red checks, finish when a rule is unclear. CONTRIBUTING asks for a harness only where vanilla has something to compare.
docs/cleanroom, docs/native-hooks-neoforge and docs/how-horizon-is-built linked to the removed section and repeated its rules. The history of code removed for its license stays in how-horizon-is-built.
CONTRIBUTING now says Horizon works in two stages: first it plays exactly like vanilla, then it improves on it. Performance work keeps compatibility with vanilla clients, servers and saves and the gameplay players rely on, but need not copy vanilla's internal order, random calls or slow paths. Pull requests say what they no longer reproduce exactly and how it was checked.
Allow the supported pinned Ubuntu 24.04 fallback in the runner-policy check while preserving the shared pool-routing and privilege guards.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Refs #15336. Pin the GitHub-hosted verify fallback for a controlled CI run; preserve the existing self-hosted routing and checks.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Report missing inline-feature fields against the complete flat feature map, matching the vanilla decoder.
Validation: 84/84 strict vanilla records, the feature-codec unit test, and all 5 Pong compatibility tests passed locally; required verify passed on f55a82aa12.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Keep the gate change unchanged and include main #15250, which carries the validated Guest.record test correction.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Match the already validated fix in #14219, #14088, #15250 and #15291: Guest.record holds the prediction record directly after main #14005.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
The requested bulk merge includes #15250 and #15291. Keep interaction_data and inline_feature_parse selected so those fixes retain automatic coverage.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Resolve the port-ledger conflict by retaining the explicit social_screen.rs::Entry description. The other path corrections are already on main.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
use_item_on (#15320), recipe_book_view (#15321), block_entity_render
(#15322) and fishing_hook_entities (#15323) passed at main's last green
parity-main (fdc87aecd, 2026-11-07) and fail now whatever a pull request
changes. Each has an issue naming the merge, or the range, that broke it.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Compatibility fix for main pull request #14005: the record moved out of anticheat onto Guest. Preserve the existing tick_end assertion.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
An issue was opened only for a harness that passed in the previous audit,
so one merged while its pull request's parity job failed or was skipped
never got one: nine such harnesses failed on main unreported (#15158).
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Seven runs between 2026-11-09 and 11-11 searched up to 1,500 merges until
the job's 240 minutes ended, named no merge, and the next run started again
from nothing. The search now has --budget minutes (90), then says between
which merges each harness broke. What it learnt is kept in
.local/parity-runner/bisect.json while the base is the same, so the next
run narrows it further, and a merge already named gets no second comment.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
They merged while their pull request's parity job failed or was skipped and
fail on main whatever a change touches (#15158), so every pull request that
selects them fails and parity-main cannot pass. They still run when named and
in the nightly audit, and come off the list with their fix.
Part of #15316.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Canonicalize root entity attribute-list order while preserving all values, styles and other record fields.
Validation: 9 focused comparison tests and 105/105 strict vanilla parity records pass; current-head required verify is green.
Co-Authored-By: Codex GPT-6.1 Sol <[email protected]>
In 26.3 Feature.DIRECT_CODEC dispatches on `type` straight to the type's
own map codec: there is no `config` wrapper, so `config` is just another
unread key and a missing field's DFU error names the whole map. The
command codec still fell back to a `config` sub-map, so
`{type:"minecraft:simple_block",config:{}}` reported `MapLike[{}]`. The
inline_feature_parse parity harness (#15158) now matches 84/84 records.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Replace only the two unseeded equality probes with deterministic built-in profession-domain checks. Keep saved VillagerData and all finalized reads exact, with regression tests for the corpus and strict feedback/callback comparison.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Keep quoted text, nested custom lists, modifier order and every styled
separator exact. Cover values, duplicate entries, modifier IDs and actual
state differences with regression tests; preserve the contributor commit.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Identifier.isValidNamespace returns false for the namespace "..", and
otherwise accepts a-z, 0-9, '_', '-' and '.'. from_parts only checked the
characters, so Identifier::parse("..:x") succeeded.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
RconClient.sendCmdResponse cuts the Java string with substring at 4096
UTF-16 units, so a character outside the BMP can straddle a cut, and
String.getBytes(UTF_8) writes each lone half as '?'. Horizon counted
characters instead. Split the output on UTF-16 units and write a lone
surrogate half as '?'.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`width` and `interaction` share a HashMap bucket at every table size, and
keys sharing a bucket iterate in insertion order. java_hash_order ranked
neither, so they fell back to key order and `interaction` came first,
while Interaction.addAdditionalSaveData writes width, height, attack,
interaction and response in that order. The interaction_data parity
harness (#15158) now matches 120/120 records.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official server's players.online (any u64 from a plain status ping) went into the history
as i64: above i64::MAX it wrapped negative, and near it SUM() overflowed, failing
/v1/live/history?source=official until the rows were pruned. A count above a million is now
treated as no answer: that minute stays empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- Host: the inbound handshake slot is an RAII guard, so a failed spawn can't leak one of
MAX_HANDSHAKES until the direct port takes nothing.
- Sessions authenticate at most 16 direct-race streams, each under one 6 s deadline instead of a
per-read timeout a peer could drip past.
- Invite decode takes at most MAX_ENDPOINTS (8), as encode writes: a crafted code can't make the
guest dial thousands of addresses.
- A STUN attempt has one deadline, so datagrams arriving faster than the read timeout can't keep
the query, and the host's setup, going forever.
- A UPnP description's controlURL, URLBase or serviceType with control characters or spaces is
ignored instead of reaching a request line or header.
- The guest's direct-race listener handles each inbound stream on its own thread with a deadline.
- Relay: the connection count is given back by a guard, and attach tokens compare in constant time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- UserList add/remove only log a failed save, so with a read-only or full disk the CLI and the
stopped server's API said "Banned X" or "Made X a server operator" though the file still said
otherwise. Each change is now saved and checked (saved()).
- ServerProperties::store wrote server.properties in place, default mode: a crash at start left a
short file read as defaults (a new management secret, settings dropped), readable by other
accounts. It now writes a new file (0600 when new, an existing file's mode kept) renamed over.
- system_message/kick translatable parameters are plain text, as vanilla's Message takes them,
not components tellraw would resolve with the console's rights.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- One address could hold all 16 RCON places idle and keep the operator's own client out; each
address now gets at most 4.
- Management notifications went to every client from one thread with blocking writes: a client
that stopped reading stalled them for everyone. Management sockets get a 10 s write timeout
and a client whose write fails is closed.
- The event stream (SSE) gets a write timeout too, so a client that stops reading doesn't pin
its thread forever.
- same_token compares SHA-256 digests, so its time doesn't show the token's length.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
The council's polls are applied: rdbtCVS is the second organisation owner,
the council team holds CODEOWNERS and the security-manager role. The pages
that said Ludovic alone held these powers now say who does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
serde_json's index panics on a Map missing the key and on a non-object Value: a density
function "spline" without its "spline" field, a template pool whose elements aren't objects, or
a structure whose horizon:piece_limited isn't an object panicked the port. In the launcher that
killed pack_job's thread before PackDone, so the Modpacks page stayed busy until a restart.
Those reads now use get/get_mut and skip what isn't there, and pack_job turns a panic in a step
into an error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
`sudo horizon-server service install --system`, as the help says to run it, wrote `User=root`:
sudo resets USER to root, so the network-facing server (game port, RCON, management API, panel)
ran as root and left root-owned files in the operator's folder. The unit now runs as SUDO_USER,
else USER, and refuses to be written for root (from a root shell).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- The relay-wide unknown-code budget now refuses only joins whose code turns out unknown:
a handful of /24s or /48s could keep it empty and every guest with a real code got
RateLimited. Per-address and per-site budgets are still checked before the lookup.
- Behind the PROXY protocol, a connection from outside RELAY_PROXY_FROM is closed before
anything is read, as the docs say, instead of holding a slot for first_line.
- Registrations get a per-site budget (IPv4 /24, IPv6 /48; 30 a minute, burst 10) on top of
the per-/64 one, and ended nameplates expire oldest first from a queue instead of a scan of
the whole map under the global lock on every registration.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Move the existing DTOs and response parsing into a private model module,
retaining public social paths and the existing parsing tests. Keep wire
and presence helpers visible only inside the social module tree.
Document the small pure-move preparation sequence, including a mandatory
manager module and a vanilla audit gate after all structural steps.
No cache, scheduling, action or UI behavior changes are included.
Fixes#15179
Co-Authored-By: ZCode <[email protected]>
Its two version lookups fell back to a literal "26.3" when the lock
file was missing or had no version, so a version bump that touched
only research-lock.json left the ledger looking in the wrong folder.
tools/lab.py already reads the pinned version from there; the
fallback now uses it.
Co-Authored-By: OpenCode <[email protected]>
🤖 Agent: CODEOWNERS moves from @ludovic111 to @horizon-rust/council, as the council decided (P35); the team exists with the six members and write access; reviewer tests pass. Paths guarded by /nightly are unchanged.
The council decided (P35, 2026-11-10) that code-owner reviews move from the
founder to a GitHub team of the six council members. The team
horizon-rust/council exists and has write access, which code owners need.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
container_screen: the oracle makes its screens with Unsafe.allocateInstance,
which skips AbstractContainerScreen's constructor and so its
`skipNextRelease = true`. #13723 made Horizon's Clicks start that way,
as the real screen does, and the oracle then disagreed on every case
whose first event is a release (373 cases). The oracle now sets the
field the constructor would.
stand_lodestone needs --work-dir, a frozen Java run (--reference-dir)
and a built game (--binary), which an automatic run never passes, so it
failed in 0 s on every runner. It joins the other capture-preserving
harnesses in parity_hosts.json's named_only: it runs when named, and
in the nightly audit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The #[path] mod wheel block sat right after pub use Opt, the anchor the
widgets PR uses. It now sits after mod shell_pages, and its test moved into
the module's own #[cfg(test)] mod tests instead of appending to
options_ui_graphics_tests.rs, so the three siblings touch disjoint lines. No
behaviour changes.
Options.mouseWheelSensitivity is an IntRange(-200, 100) through
logMouse/unlogMouse: the value is 10^(i/100), so the slider's handle sits at
(i + 200) / 300 and a drag picks the step by floor(map(v, 0, 1, -200, 101)).
Horizon used a straight line from 0.01 to 10.0, which put the default 1.0 at
0.099 of the track and made every drag pick a different sensitivity.
Every Commands packet becomes a dispatcher, and every resource-style
argument the vanilla tree doesn't match goes through intern(), which
Box::leaks its registry name for the life of the game. The name is the
server's, up to 32767 bytes and unchecked, so a server sending trees of
new names packet after packet leaked megabytes each time until the
client ran out of memory.
intern() now keeps only resource-location names of at most 256 bytes,
and at most 1024 of them; anything else is kept as "" (no suggestions
for that argument).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
SystemEntropy is splitmix64 seeded from 8 bytes. On a first start with
no secret and no level-seed (both defaults), the 40 characters of
management-server-secret were outputs 1-40 of that stream and the world
seed was output 41; splitmix64's mixer is a bijection, so the world
seed (which ops read with /seed, and seed crackers recover from
terrain) gave back the state and every secret character. The secret is
kept when the owner later enables the management server. On Windows the
seed fell back to the clock, so the secret depended only on the start
time.
Secrets (the management secret, the TLS keystore password) now come
from OsEntropy, every draw straight from getrandom with rejection
sampling, as vanilla's SecureRandom. SystemEntropy itself is seeded
from getrandom too, the clock only if that fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
PATCH /api/v1/config checked the raw JSON key with changeable_remotely,
which doesn't trim it, then applied "{key}={value}" through
parse_change, which does. A key " engine.path" (or "\tapi.allow_public",
" discord.webhook", " properties.enable-rcon") passed the deny-list and
was written to server.toml: at the next restart the API or panel opened
to the internet, chat went to another webhook, RCON came on, or the
supervisor ran another program.
changeable_remotely now refuses any key with whitespace or control
characters in it, so what it checks is what gets applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A backup turned autosave off, then flushed. If the flush failed (the
control connection's 30 s reply timeout on a big world or a slow disk),
the ? returned before `console save-on` was sent, and the game kept
running with autosave off until the next backup that succeeded;
scheduled backups take this path, so a crash in that window lost
everything played since.
Once save-off has been asked, saving is now always turned back on: over
the same connection after a good backup, and over a fresh one when the
flush failed (a reply that timed out leaves the old one out of step).
The flush gets its own 300 s reply timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A pack whose one terrain program (gbuffers_terrain) draws both solid and
cutout terrain ran it twice over the solid list, each pass moving the other
part's quads off-screen: every solid-terrain vertex was shaded twice (0.6 ms a frame measured with a path-traced pack on an RTX 2080).
Now the game also gives the loader terrain's inputs for one pass
(GameInputs::terrain_one_pass, GbuffersInputs::parts): the vertex stage
works out its quad's part from the sprite layer, as before, and hands it to
the fragment stage as a flat varying, which applies that part's alpha test.
alphaTestRef becomes a per-part value in both stages (0 for solid, 0.5 for
cutout, as Iris 1.11.7 did in #9307's probes), and alphaTest.gbuffers_terrain
still replaces both parts' tests. When both tests are the same, nothing
changes in the shader. Packs with gbuffers_terrain_solid or _cutout keep
their two passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Include the root model scale in the appendage margin so animated elder guardian vertices remain inside the precheck bounds. The existing prepared-model coverage test remains unchanged.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
#14603 gave new_rocket the rocket's own random as a fourth argument; the
tests added by 188817342 still called it with three, so the client's tests
stopped compiling on main.
Refs #15025
Co-Authored-By: Claude Fable 5.2 <[email protected]>
#14335 read Game::studio_replay, which the layer split had moved to
Game::replay before it merged, so main stopped compiling.
Fixes#15025
Co-Authored-By: Claude Fable 5.2 <[email protected]>
`SocialInteractionsScreen` declares no `mouseClicked` at all: its clicks come
from `ContainerEventHandler.mouseClicked`, the inherited child dispatch, which
reaches the search box's `EditBox.onClick` before the tabs, the rows' buttons
and Done. The body and the port map had attributed that to an override on the
screen.
`social_screen::click_kind` now answers which child the click belongs to -
`Click::Search` for the box, `Click::Action` for the list - and `Game::social_click`
performs it. The test drives `click_kind`: both corners of the box are the
box's, the pixels left of and just below it are nobody's, and Done's centre is
the list's `Action::Done`. Without the search branch the two inside corners
read `None`.
The caret move itself still needs the GUI atlas (`EditBox::click`), so it sits
in `Game::social_click` and is not asserted: every `Game` test in this crate
needs prepared resources and is `#[ignore]`d.
Count performance cores on Linux systems with asymmetric core frequencies
(such as Intel Alder Lake, Raptor Lake, Meteor Lake and ARM big.LITTLE)
via sysfs cpufreq, bringing parity with the Apple silicon performance core
heuristics from #11294.
Co-Authored-By: Antigravity <[email protected]>
`BannerPatternLayers.CODEC` is `Layer.CODEC.listOf()` and
`BannerBlockEntity.loadAdditional` resolves it with `ValueInput.read` plus
`Optional.orElse(BannerPatternLayers.EMPTY)`, so a single layer the codec
rejects leaves no layers at all. The branch kept the valid ones with a
`filter_map`, which is the partial behaviour the body says it fixed.
The list now reads all or nothing, and the test asserts the empty answer for a
mixed list, a missing field, and that a list of valid layers still reads back
(and round-trips), so an always-empty result cannot pass it.
The row pointed at passenger_attachment, which is the seat for riders on the
spider, and claimed the spider always uses its fixed seat. The rule is
vehicle_attachment: 0.3125 of the scale only when the vehicle's box is no wider
than the spider's, and a wider vehicle falls to the type's vehicle attachment,
EntityAttachment.VEHICLE's AT_FEET fallback, which is the feet.
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.
`ZombieVillager.defineSynchedData` seeds `DATA_VILLAGER_DATA` from
`initializeZombieVillagerData(this.random)`: one `nextInt(15)` on the entity's
own random, made inside `Entity`'s constructor. Horizon built a synthetic
`LegacyRandom` out of the mob's random state and id instead, so the profession
never matched vanilla's and the mob's own random never advanced.
The draw also has to sit in the right place in the stream. `Entity.<init>`
makes the UUID's two draws and then calls `defineSynchedData` last, while the
yaw `living_random` used to take belongs to `LivingEntity`'s constructor, after
it. So `living_random` now returns the random straight after the UUID, and the
two draws it left behind move to `zombie_villager::constructor_draws`, which
`Mob::with_random` calls in vanilla's order. Nothing moves for other kinds:
those draws are constants, so their streams are unchanged.
Two draws vanilla never makes are gone as well. `ZombieVillager.finalizeSpawn`
only picks the villager type and never rolls a profession, and neither does the
spawn path. `readAdditionalSaveData` redraws only for a finalized save with no
`VillagerData`, which `read_nbt` now matches.
Verified with the issue's repro (`parity_fuzz.py --replay`, seed 998): vanilla
`butcher` against Horizon `nitwit` before, both `butcher` after. The replay's
other difference, `Motion[1]` -0.0784 against 0.0, is pre-existing on
`origin/main` and untouched here. `seed_sync`, `zombie_cure_sound` and
`world_spawn#1`-`#4`/`world_spawn_sync` pass.
`mobs.rs` is past the 3000-line limit, so it gains no lines: the helper lives in
`zombie_villager.rs` and `living_random` loses the yaw line it used to take.
Co-Authored-By: DeepSeek V4.1 Flash (OpenCode) <[email protected]>
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
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.
Villager.readAdditionalSaveData reads it with getByteOr, which narrows
any numeric tag (IntTag.byteValue is (byte)(value & 255)): FoodLevel:300
loads as 44. The loader kept the raw number.
The cube's AI step grew every adult of size 1 to size 2 on its second
tick, and shrank every baby above size 1 back to size 1. Vanilla's
SulfurCube.ageBoundaryReached runs only where AgeableMob.setAge crosses
the baby boundary, which finalize::set_age already does. The tick check
is gone: a summoned or loaded adult of size 1 and a split baby of size 2
keep their size.
command_exec gets a "sulfur cube size ticks" group with the cubes' AI on.
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]>
Adds a fourth NoAI pig (self) summoned with last_hurt_by_mob set to its
own UUID, read at the same points as echo, so vanilla 26.3 shows that
EntityReference.getEntity resolves the holder too and baseTick ends the
record after 100 ticks.
Part of #12830
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings the branch up to date and lays out the new code and tests as
rustfmt wants them, which the verify job checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Players are entities of the level from placeNewPlayer/respawn on, so a
guest whose client is still loading is equipped too, and guests and the
local player sort with the mobs and stands: entity sections first
(EntitySectionStorage.forEachAccessibleNonEmptySection, SectionPos.asLong
order), then join order by entity id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EquipmentDispenseItemBehavior picks the first living entity in front that
canEquipWithDispenser, players included; ArmorStand.canUseSlot rejects locked
slots and hands without arms. Fixes#12937
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Replace the open-ended UI scope with eight reviewable work packages and
separate backend, toast, face and host-adapter steps. Record vanilla
entry/lifecycle requirements, old-port defects, evidence limits and the
maintainer decisions required before implementation.
Keep the account overlay distinct from the approved host/share migration;
leave the old implementation PR and isolated merge branch untouched.
Co-Authored-By: ZCode <[email protected]>
`a_crafters_grid_counts_for_the_recipe_book` opens a crafter menu, puts
four oak planks in its first slot and asks the stacked contents whether
an oak-plank ingredient can be crafted. Without the crafter arm the same
call fails.
`CrafterBlockEntity.fillStackedContents` hands the crafter's nine slots
to the recipe book's stacked contents (`accountSimpleStack` each), so an
open crafter's grid counts towards the craftable check.
Horizon's `Menu::stacked_contents` read the player's inventory, the mod
sources and the `GRID` container, but a crafter's slots are its `MAIN`
container (`CrafterMenu.addSlots`): with a crafter open the book saw an
empty grid and marked nothing craftable.
Differential test over 2000 seeded random inputs: entity sets of up to 40
states (all four kinds, own and other ids), guest positions and slacks,
with entities placed on or next to every edge of the close box, checked
against a verbatim copy of the old scan. Plus an inclusive-edge test and
the own/item/orb exclusions. The inputs are multiples of 1/8 so the box
edges are exact in f64. No world or game resources needed; cargo not run.
Refs #14920
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
excused() ran a string compare on each entity's kind before the box test,
for every guest every tick. Move the box test first (it is a pure float
check) and move the entity scan into entity_near, so the excuse can be
checked against the old scan. The boolean result is unchanged: the
predicates are pure and any() is order-independent.
Refs #14920
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
game.rs is over the 3000-line limit, so the gate and its test move into
native_criteria.rs, which already builds the player's advancement views,
instead of a new module declared in game.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`a_disc_change_posts_block_change` places a jukebox, records the posted
game events and calls `set_has_record`: the box posts
`minecraft:block_change` at its centre with no source. Without the post
the recorded list is empty.
A 0.2 pack can't show a colour outside sRGB today: the contract says every
colour is in BT.709 primaries, forever. This adds the key that will let a
pack ask for more, before 0.2 is on by default and packs are written against
that contract.
- `[frame] primaries = "bt709"` (the default) or `"bt2020"`. The check takes
`"bt2020"` only with `blend = "linear"`, since a change of primaries is only
right in linear light. The game refuses a `"bt2020"` pack for now, as it
refuses `blend = "linear"`.
- `Frame` gains `primaries` and `luminance` (that space's luminance weights)
after `black_level`. The renderer writes 0 and BT.709's weights. The
uploaded buffer grows by 16 bytes; 0.1 declares the same prefix as before.
- The reference's "Room to grow" says a later release adds a name to a
stage's prelude only under a manifest key the pack sets or in a new stage.
`tests/names.rs` holds each 0.2 prelude to its list of names
(`tests/names-0.2/`), and shows why: a pack's own `fn depth_at` in a post
stage is refused as a redefinition, its own `fn to_bt709` passes.
No pack's picture changes: a pack without the key, a `"bt709"` pack and 0.1
packs draw as before.
Part of #14584 (step 1) and #12989.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
`JukeboxBlockEntity.notifyItemChangedInJukebox` sets the `has_record`
block state with update flag 2 and then calls
`level.gameEvent(..., GameEvent.BLOCK_CHANGE, pos)`: sculk sensors hear a
disc enter or leave a jukebox. Horizon's `set_has_record` changed the
state but posted no game event.
SlotSources.CODEC admits a registered holder (`ns:id`), a holder list and a
`#tag` holder set wherever a slot source is accepted, and validates the
`group`, `limit_slots`, `filtered`, `contents` and `slot_range` shapes
(including a `slot_range` source context) against the loaded registry.
DataStore loads `tags/slot_source` in the same pass as the definitions, in
pack order with `replace`. `slot_source_tag` resolves a tag as TagLoader
does: first-seen member order, nested tags, required members and cycles
rejected. The command registry publishes the tags that resolve.
Part 1 of 3 for #13091 (PR #13951 is the last part).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Share only isolated-group/filter metadata; each PR keeps its own corpus and gameplay change. Preserve hopper-pot exact selection and all existing groups.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Share only isolated-group/filter metadata; each PR keeps its own corpus and gameplay change. Preserve hopper-pot exact selection and all existing groups.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The fix and its Rust tests stay here (about 350 lines). The harness,
its transcript and the VanillaProtocolClient mode follow in a stacked
pull request, as asked in review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With LOD Distance on, the Render Distance row is relabelled "Full Detail
Distance", and its value came from Horizon's own "%s chunks" key instead
of vanilla's options.chunks, so "12 Chunks" turned into "12 chunks".
Format the value with options.chunks, as the vanilla row does.
Fixes#14880
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Both wrapped the command_exec driver without --work-dir, so they shared
.local/command-exec-parity with each other and with command_exec, and two
running at once read each other's scripts and worlds.
Fixes#14855
Co-Authored-By: Claude Fable 5.2 <[email protected]>
ActiveTextCollector.ClickableStyleFinder tests each glyph's own area
(BakedSheetGlyph.GlyphInstance: its top and bottom, the shadow, bold
thickness and italic shear), so a glyph a font's ascent moves off its
line is clicked where it is drawn; only empty glyphs cover the line's 9
pixels. The last area with a click event wins, not the first character
under the cursor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The End's own dragon flies above the cleanup's floor at times, and killing it started vanilla's
death sequence. bird flap sensors ran on item honey bounce's seed sync, 36 groups earlier, and
failed in more than half of the runs once the cleanup ran between them (0 of 9 without it): each
bird now gets its own sync, with entity ids clear of the world's loaded entities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assets::load leaked a new BlockRegistry (about 11 MB for vanilla) on every
call. The game calls it once, but tests call it once per fixture, and none
of it is freed. Keep the &'static contract and intern by content: equal
registries (same states in order, same defaults) share one leaked copy.
Closes#14503
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityReference.getEntity looks the UUID up in the level and returns any
matching LivingEntity, the holder included, so LivingEntity.baseTick ends
such a record after 100 ticks. resolve_last_hurt_by_refs dropped a
reference equal to the mob's own UUID, leaving it unresolved and saved for
ever. Remove that exclusion and cover it with a test.
Part of #12830
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
CEM names a humanoid's armour with two models, `<mob>_outer_armor` and
`<mob>_inner_armor`, but 26.3 draws it on four layers of the model: the helmet,
the chestplate, the boots and the thinner leggings. A CEM name gave one layer
key, so no armour file reached any of them, and the 24 armour layers of the
zombie family's baby models were unreachable.
An armour model now reshapes every layer it covers: the outer one the helmet,
the chestplate and the boots, the inner one the leggings. Layers a model does
not have are dropped by the existing check against vanilla's layers, so no list
of mobs has to track the game, and a baby keeps its own file through the suffix
rule.
The rabbit's `left_foot`/`right_foot` mapped to `left_hind_foot`/
`right_hind_foot`, which 26.3 no longer has: the haunch carries the box under an
empty hind leg, so the foot is the haunch and the thigh the leg.
`mobs::cem::tests::names_reach_horizons_models` (ignored: needs the prepared
resources) passes with no gaps.
Fixes#8426🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
This reverts merge 5248e83f28 (#13953).
Since it merged (2026-11-09 15:51Z), entities in the command_exec
fixture world stop ticking: they no longer fall, die, despawn or pick
items up, and command_exec fails 159/184 (it passes at the merge's
parent, and fails at the merge, both run here). Every PR whose change
selects command_exec has failed parity since.
Vanilla's gate is right, but Horizon does not keep the ring around a
forceloaded or ticketed chunk resident the way vanilla's ticket levels
do (a FORCED ticket gives its neighbours FULL), so the residency check
holds back chunks vanilla ticks. #13852 needs that ring modelled first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Eight offers put the thumb at the track's top; a hand over it, the vertical
resize while dragged, nothing left of the thumb or past its bottom edge, and
the disabled scroller of seven offers asks for nothing.
MerchantScreen.extractScroller requests CursorTypes.POINTING_HAND over the
scroller's thumb, or RESIZE_NS while it is dragged. Horizon drew the thumb
but asked for no cursor, so the pointer stayed an arrow over it.
The Windows-only test still built `horizon_prepare::java::Jdk`, but since
92886cd964 the launcher reaches that crate only through horizon-platform's
`prepare` feature, so `cargo clippy --all-targets` in launcher/ failed on
Windows (Linux CI never compiles this file). Use
`horizon_platform::prepare::java::Jdk`, as backend.rs does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: founder-imposed governance amendment of 2026-11-10 (Ludovic's go, ~09:55Z): three kinds of action, elections by tier, the permissions table checked against the live Discord, panel, ops list and GitHub, and P41 with a 24 h veto; docs only, verify green on this head. The council can overturn it by poll.
AgeableMob.canAgeUp to ai.rs::can_age_up, and Animal.mobInteract's food
branches to ai.rs::takes_food: an adult at age 0 that canFallInLove, else
canAgeUp.
A frog and a cow, both at Age -24000, through takes_food and feed_animal: the
frog refuses the food and keeps the age, the cow takes it and grows a tenth.
Without the fix the frog takes it: took_food=true ate=true age=-21600, where
the test wants false, false and -24000. The cow reads the same either way
(age -21600).
Animal.mobInteract's growing branch is `canAgeUp()`, which is
`isBaby() && !isAgeLocked()`, not `getAge() < 0`. ai::takes_food still
decided that branch from the raw age, so a frog spawned with Age at
getBabyStartAge (AgeableMob.finalizeSpawn's group baby is not gated on
canBeABaby), a camel husk (AbstractHorse's group spawn is 20 percent), or a
slime, magma cube, parrot or zombie nautilus with a negative age took the
food: it ate the item, played the eating sound and grew a tenth towards 0,
where vanilla refuses the click.
takes_food now asks can_age_up(m), the port of canAgeUp, the same predicate
the rest of the feeding code already used.
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]>
ChestBlock.isChestBlockedAt is a redstone conductor above the chest or a
cat sitting in the block above it (ChestBlock.java:318-319, the cat test
at :327-338). getAnalogOutputSignal (:346-348) reads getContainer(..,
false), whose combine(.., false) (:279) uses that predicate, so a blocked
chest combines to nothing and the comparator reads 0.
The comparator's container view (machines::Containers::view) only checked
the conductor; the cat test lived on the menu path alone. Hand the level's
mobs to the view and share one isCatSittingOnChest helper with the menu
path (chest_blocked_at). A hopper's getContainer(.., true) still ignores
the block, so hoppers and menus behave as before.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
`falling_model_vertices` works out the model seed from the block's starting
position itself, so its call in the falling-block loop fits on one line and
game.rs ends up shorter than on main (docs/layers.md#file-size).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new get_the_game hook (horizon-hooks, re-exported by horizon-platform)
answers with the page to open and the screen's texts as translation keys.
horizon-official-servers answers it with Horizon's launcher text and
horizon_addresses::WEBSITE. With no hook, the engine screen and
missing_resources say how to prepare the files and offer only Quit.
The screen's strings are now translation keys. The no_resources.rs entry
leaves tools/addresses.json's allowed list.
Closes#14479
Co-Authored-By: Claude <[email protected]>
The float frame is smooth, but the last pass rounded it to the HDR10
swapchain's 10 bits as it was. Around 90 nits one PQ code is a 1% step
in light, and a clear noon sky is two or three codes tall, so it showed
as wide flat bands with hard edges (#12989's audit, F3).
The pass into the HDR10 swapchain now adds triangular noise of one code
either way to the PQ signal, each channel's own, as the last thing
before the write: over the world and the HUD alike, after the black
level lift. A gradient shows as grain one code deep and its mean is the
light meant.
- The noise is a fixed function of the pixel (PCG's 3D integer hash):
no frame counter, state or texture, so a still picture stays still
and HORIZON_STILL needs nothing.
- Black stays black: within a code of 0 or of the top the noise shrinks
to what fits, so a signal of 0 is written as 0, one of 1 as 1023,
nothing leaves the range, and near-black isn't lifted.
In that last code the rounded mean leans to the end (a third of a
code from it shows as an eighth): the accepted price of a true 0.
- A pipeline override constant (`pq_dither`) turns it on for the HDR10
swapchain's pipeline alone. scRGB (a float swapchain), SDR, the SDR
screenshot, recordings and the screenshot's HDR10 copy (`-hdr.png`)
are as before, byte for byte; calibration's test pictures keep their
exact codes.
Refs #12989
Co-Authored-By: Claude Fable 5.2 <[email protected]>
AbstractHorse.onClimbable (horses, donkeys, mules, the undead horses, llamas,
trader llamas, camels, camel husks), Ghast.onClimbable, Phantom.onClimbable
and HappyGhast.onClimbable all return false, so LivingEntity never takes its
climbable branch for them: no 0.15 clamp and fall reset in handleOnClimbable,
no 0.2 climb against a wall in handleRelativeFrictionAndCalculateMovement, no
fall reset for a ridden mount in handleMoveVehicle, and isPushable keeps them
pushable in vines. Mob::on_climbable now answers that false, which every
caller (travel, pushEntities, the vehicle fall reset) already reads.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
PhantomSpawner.tick asks Level.canSeeSky, which is "sky light 15 here"
(BlockAndLightGetter.canSeeSky), not a scan for a full collision cube:
glass and stained glass keep the sky light and spawn phantoms, while
stone, water and a slab roof do not. The advancement location predicate
read the same hand-rolled scan and now reads the sky light too.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Rule 3 told every pass that keeps a history to blend tone-weighted. Measured on Horizon's
own TAA, which accumulates the float frame's gamma, that raised the sun scene's largest
per-pixel standard deviation from 11 to 27 PQ code values. The rule now goes by the encoding
a pass accumulates: tone-weighted for linear light (a 0.2 pack's post buffers, or the frame
under linear storage), as stored for vanilla's gamma.
Rule 2 named the sharpening's limit above 1 as a fault to fix. Measured, sharpening the sun's
edge as the rest raises its flicker under upscaling by 55 to 69% for 1 to 6% more detail, so
the limit stays and the rule says why.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
DefaultDispenseItemBehavior.spawnItem builds the item with the
five-argument ItemEntity constructor, which draws a default motion (two
nextDouble) from the item's own random before spawnItem sets the thrown
one. Horizon passed the thrown motion straight in and skipped those draws,
so every later draw from the item's random, starting with
moveTowardsClosestSpace's escape speed, came two doubles early.
spawn_item_thrown makes the draws; the dispenser, the vault and the trial
spawner use it.
Fixes#14537
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Turtle.TurtleLayEggGoal sets the egg then posts GameEvent.BLOCK_PLACE, and its lay-egg pitch comes from level.getRandom(); TryLaySpawnOnFluidNearLand posts the same event for frogspawn. Turtle.TurtleBreedGoal.breed overrides Animal.breed, so Animal.finalizeSpawnChildFromBreeding's entity event 18 never runs for a bred turtle (Frog.spawnChildFromBreeding still calls it).
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Since the radial menu, Pad::holds_action reads the actions pad_button
records for a pressed button; the helper called pad_action directly, so
four tests read a held action as not held.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayerGameMode.useItemOn skips the block's useItemOn while sneaking
with something in either hand, so CampfireBlock.useItemOn never places the
food; the item's own use (eating) runs instead. The block use it does
consume triggers ITEM_USED_ON_BLOCK, which the campfire placement now fires
too. CampfireBlock.douse and AbstractCandleBlock.extinguish post
BLOCK_CHANGE after the extinguish sound, so water on a lit campfire or a
lit candle is heard by sculk sensors, and CandleCakeBlock.getCloneItemStack
answers a cake for the plain candle cake as well as the dyed ones.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
living_death_message is a free function in horizon_client_lib::living_death,
so the test needs a set for that module like hunger, landing and chat.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Six rows still described behaviour the code has, and two of them cited
`Game::use_spawn_egg` in `creatures.rs`, where it no longer lives (it is
in `creatures/spawn_eggs.rs`):
- `SpawnEggItem.spawnMob`: the spawn path posts
`minecraft:entity_place` at the spawn position (the dispenser path does
too), so sculk sensors hear the egg; the path is now cited.
- `PotionContents.getAllEffects`: `brewing::contents_effects` is the
potion's effects plus the stack's `custom_effects`, and
`finish_drinking` applies them.
- `PiercingWeapon.makeHitSound`: the jab's sound plays at the target
through `audio.play`, shared with the LAN guests.
- `TooltipDisplay.withHidden`: the loot table's
`minecraft:toggle_tooltips` is applied in `loot.rs::Context::run`, next
to the `/item modify` path.
- `SlotDisplay.Composite.resolve`: `Book::stacks` flattens a composite
into its members' stacks.
Fixes#14628: the five source_silence_game tests overflowed the 2 MiB
test thread in a Windows debug build (4 MiB is enough) and took the whole
horizon-client lib run down.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker.teleportSomewhere calls level.gameEvent(GameEvent.TELEPORT,
oldPos, Context.of(this)) after setPos and before level event 2016. Emit
the matching MobEvent at the old block's centre so sculk sensors and
wardens hear the teleport.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
apply_late_wind_pushes gave each entity only the first push held for it.
Harness: tools/test_wind_bursts_all_push_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.baseTick's updateFluidInteraction applies the fluids' currents to a
thrown or hurting projectile (Entity.isPushedByFluid); only arrows had it.
Harness: tools/test_projectile_fluid_current_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
moveEntityByPiston's applyEffectsFromBlocks reaches CactusBlock.entityInside,
even for a push that moves the vehicle nowhere. The pricked vehicles are
hurt once the tick's pushes are over, so the targets found first stay valid.
Harness: tools/test_piston_cactus_vehicles_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Primed TNT joins the boats and minecarts in the list of objects every
projectile, the trident included, can strike: a wind charge bursts on it, a
snowball breaks on it, an arrow or a trident turns back.
Harness: tools/test_projectile_hits_tnt_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Game::apply_spawn_egg_entity_data` moved from `creatures.rs` to
`creatures/spawn_eggs.rs`, so `EntityType.appendCustomEntityStackConfig`
and `EntityType.updateCustomEntityTag` cited a function that is not
there. The leash row's gap is gone too: the tick checks the leashed
mob's life and its player holder's life and spectator state, so a dead
or spectating holder drops the lead.
FireworkRocketItem.asProjectile builds the rocket from Entity.random, then
Projectile.spawnProjectileUsingShoot spreads the shot with that same random.
Horizon made a second random for the constructor's draws, so a dispensed or
crossbow rocket's LifeTime (and the shot's spread) were out of step.
new_rocket now takes the rocket's own random and draws the UUID, motion and
lifetime from it in vanilla's order; launch_angled_rocket makes it and hands it
to the shot. seed_sync gets a dispensed-rocket case (UUID, LifeTime, Motion).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`FireworkRocketEntity.explode` posts `GameEvent.EXPLODE` from `getOwner()` at
the rocket before the damage, and the entity event passes `!isSilent()` to
`createFireworks`, which gates only the blast and the twinkle of
`FireworkParticles.Starter`, so a silent rocket keeps its sparks and flash.
A rocket launched from a block face also counts as a use, as
`ItemStack.useOn` does for `FireworkRocketItem.useOn`'s SUCCESS.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
The falling-block loop rebuilt every block's model vertices each frame: the
model lookup, two sprite hash lookups per face, shading and uv mapping. They
depend only on the block state and the model variant picked, so they are now
kept in model space per state and variant; each frame copies them, applies the
same translation as before and sets the block's light. The output is the same,
bit for bit.
8,100 falling sand blocks (fast profile, Windows 11, Ryzen 5 7500F, RTX 3070 Ti):
151 -> 247 fps, CPU 6.6 -> 4.0 ms per frame.
Part of #13007
Co-Authored-By: Claude Opus 5.5 <[email protected]>
preedit's state is one static for the process, and three tests change it while
cargo runs them in parallel: one's reset or should_suppress_key could land in
another, which failed at random in CI. They now hold a test-only lock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.readAdditionalSaveData reads last_hurt_by_mob as a lazy
EntityReference (the UUID) and sets lastHurtByMobTimestamp to
tickCount - ticks_since_last_hurt_by_mob. getLastHurtByMob only answers once
the UUID resolves to a living entity in the level, and baseTick ends the
record when that entity is dead or more than 100 ticks old.
- Mob.ai keeps the bare UUID (last_hurt_by_ref) until the game resolves it
into last_hurt_by (a mob, or the player) before the mobs' baseTicks.
- baseTick (mobs/hurt_by.rs) ends a resolved record as vanilla does; a
reference naming nothing stays, keeps aging, and is saved as it is.
- setLastHurtByMob(who/null) clears an unresolved reference.
- The tests' world loads the 2-column neighbourhood entities need to tick.
- The corpus gains a pig naming an entity that arrives later.
Part of #12830
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The game, entity_nbt, stations, creatures, minecarts, particles and remote modules were split this week; twenty rows
still named the file a function left.
Fixes#14568: 62e62c21e6 put the switch test between the socket test's
attribute and the test, so the socket test (std::os::unix) compiled on
Windows and the switch test ran only on unix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ThrowableProjectile.tick and LlamaSpit.tick call Projectile.updateRotation,
which lerps pitch and yaw 20% from the tick's old rotation towards the
motion's direction (the spit with its motion before drag and gravity).
Horizon set the target angles at once.
Fixes#14544
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs is over the 3000-line limit (docs/layers.md#file-size): the
coat coin, the wolf pup's voice and the test of both move to
game/creatures/bred_draws.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Wolf.getBreedOffspring picks the pup's voice with
WolfSoundVariants.pickRandomSoundVariant from the breeding parent's random,
after the collar colour mix; Horizon left every bred pup on the classic
voice. The cow, pig, chicken, fox, cat and wolf getBreedOffspring also draw
the coat coin (which parent's variant the baby gets) from the parent's own
random, where Horizon spent a level draw for every species but the
mooshroom.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Asked by a maintainer to review open pull requests, an agent takes them
one at a time, oldest first, skipping drafts, its person's own and ones
already reviewed. It checks function, regressions, tests and scope,
reports only points with a file:line, each marked by severity, and
submits Approve or Request changes, never a plain comment. Based on the
review prompt rdbtCVS shared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SocialInteractionsScreen.mouseClicked gives super.mouseClicked the widgets
first, so the search EditBox's onClick runs on a click inside it and puts the
caret under the pointer. Horizon's social click only knew the tabs, the rows
and Done; clicking the search box moved nothing.
An agent asked for some reviews swept about 300 open pull requests, most
reviews saying it found nothing. Agents now review a pull request only
when its author or a maintainer asks for it, post only findings someone
can act on as a real review, and skip reviews that found nothing.
Maintainers' own agents may run wider passes when the maintainer asks.
Closes#14545.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MobBucketItem.spawn loads the bucket's entity data and sets fromBucket,
never PersistenceRequired. SulfurCube.loadFromBucketTag reads its own
lowercase age keys, and Tadpole.fromBucket is always true, so
requiresCustomPersistence keeps every tadpole.
Fixes#14538
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PotentSulfurBlockEntity reads its column with
CollisionContext.positionContext(the height under it); the shapes were read
with no context, so scaffolding kept its full top and stopped the geyser.
Harness: tools/test_geyser_scaffolding_parity.py.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Sniffer.emitDiggingParticles posts GameEvent.ENTITY_ACTION at the head block every ten ticks for the whole dig, outside the dust window. Only sounds that go through Entity.playSound are silenced: the state transitions and the seed drop, not playLocalSound's searching sound or the block's hit sound. A bred pair's egg takes its push from ItemEntity's own random.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
The generated Hooks trait takes the event only: entity, kind and message are
its fields, so the test impls take one argument.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
A runbook for the Horizon Team and the council: who can do what today, merging by hand, the Nightly
and how to stop or roll it back, what waits when the self-hosted runners are offline and what to do,
the Mojang republish issue, the mods registry (form pull requests, forks, takedowns), Kira, the
official server and security incidents. It points at release-flow.md, ci-runners.md, security.md
and GOVERNANCE.md for the details instead of repeating them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: main merged after #13958: the dial's colours now come from menu_style (page, backdrop_muted, and new lit/lit_edge that Horizon's look fills with the night accent), so the renderer keeps no horizon-theme dependency and the dial looks the same; its curves are cubic-bezier points pinned to horizon_theme by a test. split-controllers waits on this before moving controller files.
Jobs that need the maintainer's machines (bots on horizon-light, parity, parity-main, the Mac's
Nightly and launcher builds) wait up to a day when those machines are off, then fail, and nothing
says why. Every 30 minutes, on GitHub's runners, tools/ci/runner_watch.py lists the jobs of queued and
running runs that wait for a self-hosted runner; when one has waited over 2 hours in a pool where no
job runs at all, it opens one issue naming the pools, what stops and what to do, keeps it current,
and closes it once nothing waits. Only the workflow token (actions: read, issues: write).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: ANGRY_AT's UUID and long TTL load back (clamped, not wrapped, past i32), and the test loads a whole saved piglin; the new head only merges the test-fix commit the branch already held (same tree as da800dff).
PotentSulfurBlockEntity.LAUNCH_ENTITY_TICKER pushes every entity in the
column; only mobs, items and primed TNT were.
Harness: tools/test_geyser_launch_parity.py.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Railway's builds of both services fail since horizon-telemetry gained horizon-addresses (and the
platform crates) and horizon-mods gained horizon-release-sign: the Dockerfiles copy a fixed list of
crates, so cargo can't find the new path dependencies. The old deployments keep running, so nothing
looked broken. Copy every path crate of each service's own Cargo.lock, watch them in railway.json,
and test both lists against the lockfiles so the next new dependency fails verify instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#14493: since #13953 an entity ticks only once every chunk within 2
columns is loaded; with only chunk (0, 0) the pig never fell.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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]>
Vanilla ticks entities one after the other, so a charge never meets a boat or
minecart that an earlier charge already broke. The tick's hits are all found
first here, so such a charge ticks again without the broken vehicle.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
A piston's push is Entity.move(MoverType.PISTON): the drop adds to the
falling block's fall, and landing runs fallOn and posts HIT_GROUND in
that step. Review finding on #14440.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WallHangingSignBlock.shouldTryToChainAnotherHangingSign lets the hanging sign
in hand chain onto the sign only when the side the player faces cannot execute
its click commands (SignBlockEntity.canExecuteClickCommands), so a waxed sign
with a run_command hands the click to SignBlock.useWithoutItem, which runs it
and returns SUCCESS_SERVER.
The client now answers that for the wall case as it already did for the
ceiling one (chains_hanging_sign: isHittingEditableSide by the sign's FACING
axis), so the click runs the command, plays the waxed-sign failure and places
nothing; the world sim's unconditional Pass for the wall edge is gone.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
The horse armour layer took its texture from the item id
(golden_horse_armor -> horse_body/golden), but the golden armour's
equippable asset_id is minecraft:gold, whose horse_body texture is
entity/equipment/horse_body/gold. The layer found no texture and drew
nothing. horse_armor_asset maps each horse armour to its equipment
asset; a test checks every vanilla horse armour against items.json and
equipment/*.json.
Fixes#13826
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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]>
AbstractWindCharge.onHitEntity hurts what it hit by one point before the
burst; a vehicle takes it through VehicleEntity.hurtServer and breaks on the
fifth hit of a tick. wind_charge_damage only looked among the mobs.
Harness: tools/test_wind_charge_vehicle_parity.py.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
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]>
Part of #14440: PrimedTnt and FallingBlockEntity move with Entity.move and
keep its checkFallDamage, which posts HIT_GROUND after the block's fallOn
when they land after a fall (a piston's push too, for TNT). Extends the
`entity landing sensors` command-exec group.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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.
`ItemUtils.onContainerDestroyed` makes each stack a destroyed item held
with `new ItemEntity(level, x, y, z, stack)`, whose motion is 0.2 up and
0.2 less 0.1 sideways from the *item's* own random
(ItemEntity.java:61-66). The spill drew three doubles from the level
random instead, so the stacks barely rose and the level's stream
advanced three times per stack. Route it through `spawn_item_own`,
which already draws the four-argument constructor's motion (#13938).
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
piston_push.rs: main's version (#13655 made the same correction to the order test's expected push).
Co-Authored-By: Claude Fable 5.2 <[email protected]>
🤖 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.
Join a Friend opens from vanilla's Friends icon slot, unchanged in place
and size at every GUI scale; it is now drawn inactive, and takes no click,
key or controller press, when no Hooks::tunnel resolver is registered
(net::joins_friends), as the pause menu's icon is without a sharing page.
horizon-official registers horizon-friends' resolver, so players of the
official game see no change; the plain executable, which refuses every
friend: address, no longer opens a screen that cannot join.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client sent the server's recipes (horizon:recipe_content) numbered its
recipe book displays from its own files, while the server numbers them over
its recipe set, a world's data pack recipes included. Each pack recipe moved
the server's numbers past the client's, and the server's book entries, put
over the book by number, replaced other recipes with server:N entries.
The display numbers now come from the synced recipe set itself
(RecipeIndex::load_from_sources), for the book, its toasts and the
RecipeBookAdd toasts; without synced recipes nothing changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
SulfurCube.getEquipmentSlotForItem answers BODY for every
#sulfur_cube_swallowable item, so the body's SlotAccess takes it. Horizon
checked only the item's own equippable slot and refused every block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main.rs is over 3000 lines and may not grow (docs/layers.md#file-size): the menu's
open flag, scroll and the window height it was opened at become one PickerState in
glass_home.rs, replacing picker_open.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`resolveForItem`, `resolve`, `markResolved`, `resolvePage` and
`isPageTooLarge` cite `WrittenBook::resolve` and `resolve_held_book`.
The spawn egg rows cited `Game::use_spawn_egg` in `creatures.rs`, where
it no longer is: the function lives in `creatures/spawn_eggs.rs`.
🤖 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).
The resolve test rewrites a selector page and a score page (a plain page
is left alone), marks the book resolved and then leaves a resolved book
alone; the over-long test keeps the raw pages past 32767 characters and
still marks the book resolved; the codec test round-trips `resolved`.
`WrittenBookContent.resolveForItem` runs in `ServerPlayer.openItemGui`:
`resolve` replaces every page's selectors and scores with the text they
name and marks the book resolved, and a page whose resolved text passes
`resolvePage`'s 32767 characters cancels the resolution (the book is only
marked resolved).
Horizon kept the page components (`WrittenBook.page_tags`) but never
resolved them and had no resolved flag, so a page holding a selector
showed its raw component text and a book could not be told as resolved.
`WrittenBook` gains `resolved` (read and written by the component codec),
`WrittenBook::resolve` ports the resolution, and `Game::resolve_held_book`
resolves the book in the acting hand when `use_book` opens it (a LAN
guest's copy is the mirrored inventory, so its slot is sent back).
`set_written_book_pages` was already applied in `loot.rs`.
🤖 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.
Dropping a particle with physics at its first tick still drew it for one
frame, so a falling leaf flickered with the newest wide copy through it.
The check now runs when a pending particle joins the engine list, and
set_polytone moves unchanged to particles/polytone.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_hooks::lang::register(&'static [(code, json)]), re-exported as
horizon_platform::lang. The game's entry installs the registered files
into horizon_client::lang; Language::load reads them in the horizon
namespace right after the engine's bundled file for the same language,
before packs, and the engine's file wins on a key both have. Regional
variants share one alias table with the bundled files (fr_ca -> fr_fr,
de_ch -> de_de without ß). Nothing registered: languages load as before.
tools/lang_coverage.py reports crates/*/assets/lang beside the game's
folder; a crate's review record is tools/lang_review/<crate>/, and
--mark-reviewed puts each key in the folder that holds it. lang.rs's
file checks run on every crate folder, and a crate key the engine also
has fails them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only the rows on screen are widgets; the Multiplayer group is below the fold
of an offscreen window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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).
The row said the four lid-step calls were counted and dropped;
`Behaviours::shulker_neighbor_updates` now runs them, and `tick_shulkers`
calls it once per counted update.
`a_shulker_lid_updates_the_shapes_and_redstone_of_its_neighbours`
observes the level as `shulker_neighbor_updates` runs: each of the six
neighbours gets one `neighborChanged` from the shulker's block, with its
own state as the changed state. Without the call the level sees none.
`ShulkerBoxBlockEntity.tick` calls `doNeighborUpdates` when its lid
starts or finishes opening or closing: the box's collision shape moved,
so the neighbours' shapes are updated (flag 3) and their
`neighborChanged` runs. Horizon's render tick counted those four calls
(`ShulkerTick.neighbor_updates`) and the game dropped them, so redstone
beside a moving shulker box never saw the update.
`Behaviours::shulker_neighbor_updates` ports the two calls, and
`tick_shulkers` runs it once per counted update. The box's collision
shape for the same ticks already reaches the world through
`shulker_collision_state`.
🤖 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.
The loot data is one slot per process. A test that installed a world pack's
armor_stand table warmed that slot, and every other test's stand break rolled
it too. load_thread_loot_data keeps such a test's loot data on its own thread,
ahead of the slot; a game never sets it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
A custom particle with has_physics whose box starts inside a block's
collision is dropped before its first tick, so its emitters never run.
Falling leaves start 0.05 under their leaf block, and a pack's leaf with a
0.08 hitbox reaches into it: in Java such a leaf never falls, while here
every one fell, and a leaf that emits a resting copy of itself each tick
left trails of them under every tree until the frame rate collapsed.
Compared in game against Java 26.3 with Polytone, with particles of its
own: a box overlapping a block by 0.01 goes, 0.01 clear of it lives, and
a particle without physics lives either way; the emitters do run every
tick. A test covers the three cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round the suggestion list scroll offset instead of truncating f64->i32.
Fractional touchpad/PixelDelta wheel deltas were dropped in one
direction, so command hints scrolled down but not up.
Fixes#14338
Co-Authored-By: Claude <[email protected]>
AbstractSelectionList.extractRenderState draws its scrollbar at scrollBarX
(getRowRight + scrollbarWidth + 2) whenever the rows overflow. Horizon clipped
and scrolled the rows but drew no bar, so a long list gave no sign of its
position.
WorldOptionsScreen.updatePortControlsState makes the port box read-only
without LAN (setEditable) but leaves it focusable: clicking it selects and
copies the port. Horizon dropped the focus instead, so with LAN off the port
could not even be selected or copied.
CreativeModeInventoryScreen.checkTabHovering shows a tab's display name when
the cursor is on its icon (3 pixels in from the 26x32 slot). Horizon drew the
tabs without the tooltip, so a tab's name only appeared in a search result's
tooltip.
🤖 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]>
remove_entity moved to game/remote_removal.rs, Minecart::to_nbt/from_nbt to
game/minecarts/nbt.rs. A class row takes home, not port.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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).
Use the existing nightly and reusable parity runners instead of a second
manual inventory. Keep the JFR checks and describe their JDK fixtures.
All 84 removed harness commands remain in parity discovery.
Co-Authored-By: Codex (GPT-6) <[email protected]>
ForcedCover and the Ranges methods that read the forced chunks (is_forced,
forced_chunks, and in_main_range past the player's circle) go to
streaming/forced_cover.rs, so streaming.rs doesn't grow. A test checks that the
distance map answers every margin as the scan of every forced chunk does, as the
forced chunks change, and that past MOST no map is made.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
in_main_range went through every forced chunk for each column it was asked about, several times
a tick. While update runs (nothing can change the forced chunks then, as for the extra areas'
cover), a map of each column's distance to the nearest forced chunk answers instead; it is made
again when they change. The same answers. Streaming on the bench: 6.2 ms a tick to 4.7.
Co-Authored-By: Kacper <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 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.
AdvancementDisplay, Advancement, UpdateAdvancements and
SeenAdvancements, with their codecs, move byte for byte into
packets/play/advancements.rs. play.rs re-exports the four public items,
so every `packets::play::…` path still resolves.
The port map follows: the two rows that name UpdateAdvancements::read
and SeenAdvancements::write point at the new file.
Part of #13063.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Downloader` kept an entry in `states` for every texture URL asked for and
started a thread (with its own HTTP agent) for each, and an entry in
`resources` for every `texture` id of a profile, none ever dropped. A tab list
of 200 players started about 400 threads at once.
At most 16 downloads now run at once: past that `state` answers `Pending`
without recording the key, and the caller, which asks every frame, starts it
when one has finished. `states` and `resources` keep at most 4096 entries each,
dropping the oldest settled one first (a running download is never dropped); a
dropped texture loads again from the disk cache or the pack. A download thread
that panics before answering now answers "failed" through a drop guard, so its
place is given back and the texture is retried after the wait. The fetch
function and the bounds are fields so a test can set them.
Part of #9370.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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]>
A server started by a harness's build joined that harness's job (sccache
doesn't ask to break away) and ended with it, failing other harnesses'
in-flight compiles (os error 10054). The runner now starts it before any
build, in a job of its own, with no idle timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemFrame.readAdditionalSaveData compares the Item stack as read with
ItemStack.matches, count included, and removes the framed map on a
mismatch; only setItem then keeps one item. apply_entity_nbt compared
against item_frame_from_nbt's stack, already cut to one, so a count-only
change kept the old map's marker. It now compares the stack as read.
Fixes#14178
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Retain calls from each stage main and global declarations, including
referenced overloads and prototypes. Blank unreachable function spans
without moving diagnostic lines. Cover the public translation APIs and
post loader with independent positive and negative GLSL regressions.
Co-Authored-By: Codex (GPT-6) <[email protected]>
r.noise read a single simplex noise of seed 0, which is octave 0 of
Polytone's noise: the mod builds 1.18-1.21's PerlinSimplexNoise (gone
from 26.3) with octaves {1}, whose noise comes from a random seeded by
the first noise's value at its own offsets, read at twice the input with
the noise offsets. So grass and leaves a pack tints with the noise took
other shades than on Java.
Measured on the real mod with a probe pack whose grass colormap encodes
the noise (column the coarse value, row the fine part, calibrated with
ramps): r.noise(x/8, z/8) at 1527 blocks matches this construction to
the decoding's 0.016, and so do polytone/noises definitions with seed 0
or 1 and octaves {0}, {1} and {0, 1} (their weights a third and two
thirds). SimplexNoise gains get2_f64/get3_f64: the mod sums and seeds in
double.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
The registry's own workspace still resolved rusqlite 0.37 ->
libsqlite3-sys 0.35, which bundles a SQLite older than 3.54
(RUSTSEC-2026-0459: uninitialised page-cache memory from a crafted
database opened with a "-wal" file of another page size). The registry
only opens its own database, so this isn't reachable as deployed, but
it is the same upgrade the telemetry server and the root workspace
already made: rusqlite 0.41 resolves libsqlite3-sys 0.39.1, and
fallible_uint keeps the u64/usize conversions working as before (an
error past i64::MAX).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
The duplicate check in enqueue stops a double click from filing twice
only once the first issue is recorded. While the first request still
waited on GitHub (up to 20 s) the row's issue was empty, so a second
identical request found the row, got no issue back and filed a second
issue for it; the 10-minute upkeep (file_pending) could likewise file a
report a request was filing at that moment.
Store::claim moves a report from 'new' to 'filing' for exactly one
caller, and Store::release gives it back when GitHub refuses. report()
and file_pending() both claim first; a request that loses the claim is
answered 202 with no issue yet, as when GitHub fails. A report left
'filing' by a server that stopped mid-filing goes back to 'new' when it
starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
bind_icons gave every icon drawn in a frame a slot in drawing order and pushed
its 128 by 128 RGBA pixels (64 KiB) every frame, into an atlas that keeps what
it is given. The screen now keeps which slot holds which icon from one frame to
the next: an icon is sent when it takes a slot, the slot of the icon drawn
longest ago is taken when all are in use, changed pixels under the same key are
sent into the same slot, and a new atlas (another revision) empties the table.
Icons past the 64 slots in a single frame still keep unknown_pack, as before.
Part of #9370.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Fixes#3823, as decided there. Fullbright was already off by default; on a
connection that plays no save of the window's own (Remote::save_folder is
None: a multiplayer server, or another player's LAN world) the renderer now
draws the normal lightmap and the graphics screen hides the row. Single
player, a LAN host, a splitscreen seat in P1's world and Replay Studio keep
it, and the saved choice is never changed.
The flag is applied with the settings and again each tick, since joining a
server sets the connection after the settings were applied. The row reads
Settings::fullbright_held, which each game sets for itself (not saved), so a
splitscreen seat does not share it through a process-wide flag.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text::push (MutableComponent.append) cloned the whole JSON component
built so far on every append, then added one sibling, so N siblings
cost N^2. to_text_with pushes once per "extra" entry, and a server's
chat, title or server-list MOTD can carry hundreds of thousands: 20 000
siblings (80 KB) took 76 s on the packet-handling thread.
push now takes the tree out of its Arc and appends in place
(Arc::make_mut, so a Text cloned earlier keeps its own tree). A styled
text is still wrapped once first, including one built from a single
styled span with no saved tree, so its style doesn't reach what is
appended. The JSON produced is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
LivingEntity.updateFallFlying starts with checkFallDistanceAccumulation:
a glide whose vertical speed is above -0.5 keeps at most 1 of its fall.
Horizon's update_fall_flying left it out, so a gliding mob kept its whole
fall. The glide code moves to mobs/ai_glide.rs, keeping ai.rs from
growing.
Fixes#13868
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#14317. A local --base origin/main read the lists at main's tip, so entries main dropped after the branch left it counted as the branch's additions. check_file_size.py already compares with the merge base; CI's HEAD^1 is its own merge base.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A translatable component a server sends ({"translate", "fallback",
"with"}) falls back to the server's own template when the language has
no such key, and format_translation copied the argument's spans for
every %1$s in it. With the argument being another such component the
copies multiply: 300 references nested three deep is a 4 KB server-list
MOTD or chat message that becomes 27 million spans, and the client ran
out of memory. Vanilla's TranslatableContents.decompose keeps references
to its arguments, so its cost stays linear.
format_translation now shows the template verbatim, as it already does
for a malformed one, when the expansion would pass 32768 spans or 4 MiB
of text. Real translations are nowhere near either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
ScoreHolder.getFeedbackDisplayName replaces the display name's hover
with ShowText of the scoreboard name (an entity's UUID, a player's
name); /team and /scoreboard replies name holders with it. Horizon used
the plain display name. PlayerTeam's displayNameStyle also inserts the
team's name, which Horizon's formatted team name lacked.
test_mob_display_name_parity.py compares the /team and /scoreboard
replies too (10/10; without this commit 5 differ).
Fixes#14279
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::despawn_check on a cod, a salmon and an axolotl 200 blocks from the
player: each with a custom name stays and each without one is removed, and a
zombie's name saves it nothing. Without the name half of removeWhenFarAway the
named cod is discarded like the unnamed one.
AbstractFish.removeWhenFarAway and Axolotl.removeWhenFarAway answer
`!fromBucket() && !hasCustomName()`, so a cod a player has named stays put
however far it swims, and one let out of a bucket is persistent before it.
Only the bucket half was ported: from_bucket is in
requires_custom_persistence, but check_despawn's removable flag knew nothing
of the name, so a name tag bought a fish nothing and it was discarded past the
128-block despawn distance like any other water mob.
named_keeps_it answers the name half for the fish and the axolotl, and
removable consults it beside the feline rule: a named cod, salmon, tropical
fish, pufferfish, tadpole or axolotl is never removed for distance, and no
other mob's name is persistence.
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]>
vehicle fall clip (#13005) and boat float height (#13022) waited for
their own forced chunk only. Since #13953 an entity ticks once every chunk
within 2 columns is loaded (ChunkMap.prepareEntityTickingChunk), so their
boats and minecarts fell only when an earlier group had left the
neighbours loaded: boat float height failed all 59 reads on main, and
vehicle fall clip failed whenever a group was added before it. Both now
wait for the 5x5 ring, as the other ticking groups do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the read-only helpers native_vehicle_of(entity) and
native_passengers_of(entity) for server mods (vanilla Entity.getVehicle
and getPassengers, first = controlling passenger). Helper-only fragment
riding_getters.json; API number is assigned when the fragment is stamped.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Entity.getDisplayName adds a HoverEvent.ShowEntity (type, UUID, name)
and the UUID string as insertion to the team-formatted name. Horizon's
command replies named a mob with the team formatting only, and /summon
without NBT with a bare type name.
mob_command_name (commands/display_names.rs) builds it; target_name's
mob arm and /summon use it. New harness test_mob_display_name_parity.py
(4/4; main: 0/4).
Fixes#14220
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
- Reuse thread-local scratch queues (`RelightScratch`) across BFS light propagation passes, avoiding reallocation during incremental relighting.
- Pass borrowed slices `&[Pos]` to `relight_layer` to avoid redundant `Vec<Pos>` heap allocations.
- Cache consecutive vertical column lookups in `lt_sources` to avoid redundant map lookups.
- Preserve lazy loading of sections in `Region`, only loading and cloning sections that are actually read or written.
- Add `relight_single_block_bench` and update `relight_bench` to verify timings for both single-block and large batch edits.
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]>
Conflict: particles/tick_bench.rs. Main (c221aa9ec9) adds `cached_chunk` to the
`ParticleLevel` literal of the old benchmark body; this branch had rewritten
that body into `with_scene` and `measure`. Kept this branch's structure and
added the field to the one literal.
That commit also makes the merge fail to build, which no textual conflict
shows: `ParticleLevel::cached_chunk` is a `Cell`, so a `ParticleLevel` is no
longer `Sync`, and `tick_all` shared `&ParticleLevel` between its scoped
threads (E0277). Each worker thread now takes a view of the same world with an
empty one-chunk cache of its own (`thread_view`, in simple.rs, so particles.rs
does not grow), built on the engine thread and moved into the thread. The
thread that runs the first share keeps the caller's level and its cache. A
cache shared between threads could not have held "the last chunk looked up"
for any one of them anyway.
cargo test -p horizon-client-render --lib particles: 59 passed, including
parallel_tick_matches_serial_tick. fmt, clippy on the particle files and
tools/check_file_size.py --base origin/main are clean.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
Mods can read whether a dedicated server runs in online mode, the port it
listens on and whether it takes its players from a Velocity proxy
(forwarding.secret). The values come from the live network config and
listening socket, so they match what the server does. None off a dedicated
server and before it listens. The forwarding secret is never returned.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Identical still textures share one layer (d874278161), and connected-texture rules find their
faces by layer. A rule matching one of them by name (matchTiles, connectTiles) then also took the
faces of the others. Since chained tiles resolve (#13707), a resource pack's rule over its own
transparent tile 19 (`matchTiles=./19.png`, drawing wheat stages) reached every grass block's top
layer, a placeholder with the same pixels, and put plants on the grass.
The rules are read before the models insert their textures, and every name they match keeps a
layer of its own (Sprites::keep_apart). Identical textures no rule names still share.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep one acknowledgement per completed batch while optionally requesting
64 chunks per tick. Preserve vanilla estimation by default, record the
selected mode, and cover the framed receive path in both modes.
Co-Authored-By: Codex (GPT-6) <[email protected]>
`mob_from_nbt` needs the entity's `id` and `Pos`; the fixture only had the
brain, so the test unwrapped `None`. Add both to the compound and to the
never-expiring variant.
`ObserverBlock.updateShape` reacts only when the direction to the changed neighbour is its
FACING. A change behind it, beside it, above it or below it starts no pulse.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`ObserverBlock.updateShape` schedules a tick two out when the block it faces changes; that tick
turns it on and schedules another two out, which turns it off. A lamp behind it is lit by the
pulse (`getSignal` answers 15 only for its facing); lamps in front of it, beside it and above it
are not.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`TripWireHookBlock.calculateState` scans `for i in 1..42` along the hook's facing for a hook
facing back and ends with `attached &= receiverPos > 1`: hooks attach 2 to 41 blocks apart and
never side by side or 42 apart. Both hooks are placed the way a player places them, because
vanilla runs the calculation from `setPlacedBy`, not from `setBlock`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonBaseBlock.isPushable` ends in `!state.hasBlockEntity()`, so a chest, furnace, barrel or
hopper ahead of a powered piston keeps it retracted and stays where it is.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonBaseBlock.isPushable` answers false for a piston with EXTENDED set, so a push that meets
it fails and the pusher stays retracted, while a retracted one is pushed along. Both kinds.
The extended piston is held extended by a power source of its own, out of the pusher's reach:
unpowered it retracts on the tick, and that retraction updates the pusher, which then pushes it
(vanilla does the same; the first draft of this test asserted the wrong end state).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
26.3 gives `PushReaction.PUSH` to glazed terracotta alone, and `PistonBaseBlock.isPushable`
answers `direction == connectionDirection` for it: a slime block beside it cannot drag it, where
it drags an ordinary block, and in line with the push it moves like any other.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonStructureResolver.canStickToEachOther`: a slime block drags slime and a honey block drags
honey, but a slime block and a honey block never stick to each other. Four pairings, each with
the block beside the one the piston faces either dragged or left where it was.
Built in open air: a slime block on stone drags the floor along too (vanilla's as well), which
would make the check about something else.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
`PistonStructureResolver.addBlockLine` refuses once `toPush` would hold
more than twelve blocks, and then the whole push fails: the piston stays
retracted, it does not move twelve of them. Pins 11 and 12 moving, with
the head where the first block stood, and 13 moving none.
The port matched vanilla; the ledger lists `PistonStructureResolver.resolve`
among the ported methods no parity harness covers.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
The five tests from `pistons_pop_fragile_blocks_and_refuse_obsidian` to
`replacing_a_moving_piston_leaves_its_extended_base` move, unchanged, to
level/tests/pistons.rs, beside the barrel and trigger suites that were split
out the same way. They are the piston tests that stood in the middle of a
4176-line file; the piston rules the next commits pin go in the same place,
so one file holds them. This takes 191 lines out of tests.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
TagValueInput.read returns the codec error's partial value, and DFU's list
codec keeps the valid elements as that partial (checked against
datafixerupper 10.0.21): vanilla keeps the other layers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test loaded a compound with no id or Pos, so mob_from_nbt returned None
and it could not pass; it now saves the id, a position and the brain, and
checks a TTL past the counter's range too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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.
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]>
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]>
A dying cube mob is out of every selector's reach and splits 20 ticks later, inside the next group, so
the three groups that end by killing their size-2+ sulfur cubes shrink them first. Horizon resolves an
@s of another level only from that level, so each level's cleanup runs in it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
The tracer read each command from the subprocess.Popen audit event, which on Windows carries the command line
Popen built, not the list: no harness's cargo build was recognised, so the probe prebuild built nothing and probes'
Rust sources never became dependencies. It now splits the line back (CommandLineToArgvW), and dep_info finds an
.exe's .d file.
Cargo on Windows names a program in deps/ without the hash of its features, so a harness building horizon-client
with another selection (-p horizon-client-lib --bins) relinked the file a running harness's game was using:
"Access is denied", and every later build of the common selection failed too. On Windows a harness now waits while
a running harness built one of its programs differently.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A harness that exited or crashed left the rest of its process group running, and on Windows a parity.py or
pr_ready.py that died left its harnesses and their games running too (taskkill /T can't follow a dead parent).
tools/process_tree.py starts each harness in its own Job Object on Windows (KILL_ON_JOB_CLOSE, started suspended
so nothing escapes before it joins) and, on POSIX, kills the rest of its session's group once it exits, before
it is reaped. pr_ready.py runs its Windows checks the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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
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]>
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]>
`tools/port_map/game2-001.tsv` recorded `EnderDragon.readAdditionalSaveData`
with a gap: `sitting_damage_received` was not read. The row now says
`Dragon::from_nbt` reads it with `getFloatOr`, 0 when absent.
`Dragon::to_nbt`/`Dragon::from_nbt` are the pair the dragon's chunk entity
uses. A perched dragon with `sitting_damage = 12.5` saves
`sitting_damage_received: 12.5f` and `from_nbt` gives 12.5 back. A compound
with no such key loads 0.0.
Without the read the reloaded dragon has 0.0: `left: 0.0`, `right: 12.5`.
`EnderDragon.addAdditionalSaveData` writes `sitting_damage_received` (the
damage a perched dragon has taken since it sat down, which makes it take off
at a quarter of its health), and `readAdditionalSaveData` reads it back with
`getFloatOr("sitting_damage_received", 0)`.
Horizon wrote the field in `Dragon::to_nbt` but `Dragon::from_nbt` ignored it:
a perched dragon saved after some damage but before taking off reloaded with
`sitting_damage` 0 and had to be hit for the whole quarter again.
`Dragon::from_nbt` now reads the float, 0 when absent, as vanilla does. The
health read is folded into one line so the over-3000-line file does not grow.
`tools/port_map/game2-004.tsv` recorded `Piglin.readAdditionalSaveData` as
reading the baby flag and inventory; it now also names the brain's
`minecraft:angry_at` UUID and TTL read by `mobs/piglin.rs::read_guarded_anger`.
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A compound whose `Brain.memories."minecraft:angry_at"`
has the UUID `[90, 91, 92, 93]` and `ttl: 99L` loads a piglin with
`extra.anger == 99` and `guarded_angry_at == Some(Some([90, 91, 92, 93]))`.
A memory without a TTL (the codec's never-expires form) loads the longest
counter, `i32::MAX`.
Without the read both fields stay at their defaults: `left: 0`, `right: 99`
for the counter and `left: None`, `right: Some(Some([90, 91, 92, 93]))` for
the UUID.
`PiglinAi` sets `ANGRY_AT` with an expiry when a player breaks a guarded block
near a piglin, and `Brain.Packed` saves every memory with a codec under
`Brain.memories`: the value is the player's UUID and the TTL is the ticks left.
`LivingEntity.readAdditionalSaveData` reads that brain back.
Horizon wrote `minecraft:angry_at` in `mob_to_nbt` but `mob_from_nbt` never read
it: a piglin that a player had angered unloaded with its chunk and came back
with `extra.anger` 0 and no actor UUID, so it stopped attacking and the guarded
event was forgotten.
The load now reads the memory back: a four-int UUID into
`extra.piglin.guarded_angry_at` and the `ttl` into the existing `extra.anger`
counter, with no TTL read as the longest counter there is. The writer moves to
`mobs/piglin.rs` beside it, which also keeps `entity_nbt.rs` from growing.
`guest_dig_feedback` sends `(int)(progress * 10)`, the stage
`incrementDestroyProgress` computes, and `tick_daylight_detector` gates on
the dimension type's sky light, so the End is excluded like the Nether.
Both rows still described the old gaps.
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]>
Groups share one world, so an entity a group left stayed visible to later groups' unbounded selectors.
The bell group's persistent zombie burned in daylight and died at a random tick in each engine, which
made the entities group's PersistenceRequired check flaky. The harness now kills, between two groups,
the non-player entities above the groups' floor (y 100 in the overworld, 128 elsewhere), shrinking
cube mobs first and killing twice so drops go too. World-generated entities below stay out of it.
The wolf anger clock diagnostic continues the group before it and is named in CONTINUES_PREVIOUS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
run_command checks InventoryChangeTrigger after every command; vanilla's
/loot give calls no broadcastChanges, so its Diamonds! comes on the
player's next tick, outside the command's record (inline_loot_reference).
/item replace on a player marks what its own check announced
(cmds.broadcast_announced); the harness records those and leaves the
rest for the tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
ItemStack.postHurtEnemy uses Weapon.itemDamagePerAttack from the stack's
component. Horizon read the item prototype's weapon damage, so a patched
weapon wore 0 or the prototype's amount instead of its own.
Test: a diamond sword with item_damage_per_attack 3 wears 3 per hit.
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]>
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]>
The radial menu's overlay_holds_view gate moves with debug_crosshair_drawn into
game/hud_render.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BannerPatternLayers.CODEC reads its layer list all or nothing: one layer the
codec rejects leaves the empty layer list. Horizon skipped only the bad layer,
so a banner kept the layers before it.
A run- folder per invocation under .local/<harness>/ is never removed by runner-gc (it only
looks one level down), and these four harnesses aren't this fix's. Main's harness fixes own them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The same fix (compare with the main the pull request lands on) is open as #13882. A change to
.github/ also keeps a fork's runs from being approved automatically, so this branch got no parity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layer plan M6, step toward dropping horizon-client-lib -> horizon-anticheat. The record
(horizon_anticheat::prediction) is what the engine's movement replay (lan::prediction) plays again:
it moves to lan/prediction/record.rs unchanged. The guest holds it and the client's sprint
(was Player.record / Player.sprinting). Behaviour unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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
HoglinAi.updateActivity runs after the brain, so a hoglin given an attack
target enters the fight activity at the end of that tick and its
MeleeAttack hits from the next one: the goal now acts only in the fight
activity (brain_step runs after the goals). MeleeAttack.create(40) sets
ATTACK_COOLING_DOWN for 40 brain ticks and MemorySlot.tick erases it on
the 41st, so hits come 41 ticks apart (16 for a baby).
A hit one mob's tick deals another is handled after the mob loop, after
the target's own tick; vanilla deals it inside the attacker's tick, so a
target later in the list counts HurtTime and its damage cooldown down
once more that tick. The pig hit by a hoglin read HurtTime 10 on the tick
of the hit; vanilla's reads 9.
hoglin_repeat_attack reports its rand- rows instead of comparing them:
each hit is attack / 2 + level.getRandom().nextInt(attack), and
Level.random is RandomSource.create(); two vanilla runs disagree on the
pigs' Health.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CountDownCooldownTicks keeps ITEM_PICKUP_COOLDOWN_TICKS while it counts
down and erases it the tick after it reached 0 (stop), and
Allay.isOnPickupCooldown is the memory's presence: an allay loaded with
{value:30} saves {value:0} on its 31st tick, then nothing. ints[3] now
holds the value plus one (allay::pickup_cooldown).
EntityItemAccessor calls containerMenu.broadcastChanges for a player
whose slots /item replace changed, so InventoryChangeTrigger (here the
Diamonds! announcement) comes before the command's message. The command
harness now announces what a command earned before its feedback, as a
guest's command does (announce_guest_advancements); it left the
announcement for the next tick, outside the command's record.
allay_pickup_cooldown reports its rand- rows instead of comparing them:
the natural allay's flight starts at its first item scan
(Sensor.randomlyDelayStart with the allay's own random, which is
RandomSource.create()), and two vanilla runs of the script disagree on
those 30 rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ZombieVillager.defineSynchedData picks the profession with
VILLAGER_PROFESSION.getRandom(this.random), and Entity.random is
RandomSource.create(): the first two subjects' profession:none check
passed 1 time in 15 on either side, so 'zombie villager summoned data'
matched 9/10 from run to run. The third subject's profession comes from
its save and stays checked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its name matches no harness pattern; test_command_exec_parity.py --only
"item count predicate" already compares the group with vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It needs --work-dir and --reference-dir, which an automatic parity run
doesn't pass, so without the named_only entry it fails at startup on
every pull request that selects it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AttributeMap.pack lists the instances in the iteration order of an
Object2ObjectOpenHashMap keyed by Holder.Reference, which keeps
Object.hashCode: an identity hash. The same script run under another
main class saves the pig's list in another order, so the oracle's
order is the JVM's, not the game's. All 11 differences on main were
that order. The driver gets unordered_attributes(), which sorts the
entries of every attributes list in a record and keeps their text and
styles; the harness compares through it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
The file-size check (docs/layers.md#file-size) does not let a file over
3000 lines grow. Breaking a block entity and dropping its resources both
read the custom name and lock from the world, so a helper does it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The file-size check (docs/layers.md#file-size) does not let a file over
3000 lines grow. The count bounds matcher moves to commands/item_count.rs
and the ignored test module to game/item_count_predicate_tests.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
When drawing held items for non-local entities in held_item_geometry,
set fishing_cast_with_this_item from the remote hooks' active owner
state and the holding hand (with the main hand taking precedence when
both hold fishing rods), matching FishingRodCast and FishingHook's
client owner tracking.
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.
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]>
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]>
Screen::choose, Hosted::clicks_step, Shell::focus_soon and
changed_elsewhere, the binding row's chosen text, Capture::Pad on any
shell screen, and where a shell screen is hosted (options_ui/shell_pages.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What vanilla draws in the middle of the view (the crosshairs, the chat's
lines, the toasts, the hands) gives way to an overlay through one query,
so the next overlay adds its condition there rather than to each piece.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved hand_geometry and first_person_arm to game/frame_geometry.rs:
the hands still come from hands_shown there, so an overlay that holds
the view hides them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Which shell each page opens, what a shell's outputs do, and the radial
menu's name entry and rows move out of options_ui.rs as they are, so the
file doesn't grow. pad_clicks_step now also sets Shaders' shell back,
which main put on the shell since.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move: the tests module leaves settings_shell.rs as it is, so the
file is under 3000 lines (docs/layers.md#file-size) and the radial
editor's additions to the shell don't grow a file over the limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main put Shaders on the settings shell and made Screen::undo return
outputs: the radial screens' sheets use Sheet::new and their undo returns
none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/hud_room.rs answers whether an overlay holds the view (the radial
menu, once it shows), and the 2D and F3 crosshairs, the chat, the toasts
and the hands ask it. The four toast draws go into draw_every_toast and
the hands into hands_shown there; draw_pad_over_hud draws the menu and
then the pad's cursor. The respawn no longer closes the menu itself: the
new player's first position does, through the one pad_player_replaced
call in remote.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RadialMode, RadialStick, the opener's thumb rule, each stick's session
opener, can_bind, set_radial_stick and their tests move out of
gamepad.rs as they are, so gamepad.rs stays under 3000 lines. The paths
under horizon_client_input::gamepad stay the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/pad_capture.rs moves to game/pad/capture.rs as it is, its items
visible to the gameplay module as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Controller Settings > Bindings > Radial Menu... opens the pages on
main's settings shell (radial_editor.rs): a sheet per page with its
eight slots by direction, then Rename Page, New Page and Delete Page,
then Reset All Pages and the opener's row.
- A press on a slot opens the list of every action and Empty for it,
which names the slot, marks what it holds and opens on it; Right and
Left still step it (`Screen::choose`, and a controller's Left and
Right count as steps, `pad_clicks_step`). Back from the list, the
focus is on the slot again (`Shell::focus_soon`).
- Rename opens a small page with a name field, Done and Cancel.
- Delete and Reset act at once; Undo puts the pages and the opener back
as the screen found them. At most 6 pages, at least one.
- The opener's row is a `Value::Binding` on `Capture::Pad`: it waits in
place for a button, refuses the ones under the selecting thumb and B,
and its strip says when no button opens the menu or another action
shares it.
- Controller Settings' own Undo leaves the pages to this screen.
e2e: radial_editor, and Settings Pages reaches the new page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/pad_menu_e2e.py gains 28 radial_* cases, each a real offscreen game
driven through horizon-ctl's virtual pad: commits, retention, pages,
cancels (B, Start, a disconnect, focus loss, death with immediate
respawn), the camera and the walking stick, menus and the on-screen
keyboard keeping the opener, held sneak and drop, D-pad rolls after a
commit or a cancel, RT/keys/clicks fighting back, Toggle mode, the left
stick and Either, a bumper opener, haptics, narration, unavailable and
missing actions, a file from before the menu, Nintendo glyphs and the
status rows. A case can seed the controller file and pass extra launch
arguments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Controller Settings > General gains a Radial Menu group on main's
settings shell: Radial Menu (Hold, the default, or Toggle) and Radial
Menu Selects With (Right Stick, Left Stick or Either), each describing
its value in the strip, and stepping with Left and Right as every shell
choice does. Stepping the stick goes through `set_radial_stick`, so an
opener under the thumb that now selects is let go, and stepping back
gives it again. e2e: radial_stick_back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PadSettings gains the radial pages, `radialMode` and `radialStick`, and
the opener's binding, `pad.radial` on D-pad Right; nothing in play reads
the opener yet (it routes as a held button until the menu does).
- `bindLayout:2`: once, for a file from before the opener, a button the
player bound by hand keeps it, and the opener goes unbound rather than
sharing it. A file from before the binds list's defaults binds none.
- An opener under the thumb that selects, or on B, can't be bound
(`can_bind`), and a file with one reads as never saved.
- `set_radial_stick`: a stick that takes the opener's thumb unbinds it;
stepping back gives each stick the opener it had this session, else
D-pad Right where it's free.
- Lines this game doesn't read (a newer Horizon's settings, an action it
doesn't have, a button it doesn't know) are kept as they were, and a
newer `bindLayout` is kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The radial menu in play (game/pad/radial_play.rs): hold the opener and
the selecting stick points at one of eight slots; letting go runs the
action once, through the same path as its button. Bumpers turn the
pages. `pad.radial` now routes to it, and the dial draws over play with
the game's icon painter and the frame's motion clock.
- Hold or Toggle, and the right, left or either stick, from the file.
- B, Start, a disconnect, focus loss, a new player (death with
immediate respawn, the End's exit) or a screen cancels; RT/LT, any key
or mouse click cancels and goes on. A flick between polls counts (the
pads' peaks, splitscreen's router included). Holds from before the
open keep working; a held drop restarts its wait.
- A D-pad roll off the opener reaches nothing for 100 ms after the later
of the close and the opener's release.
- What can't run is dimmed: Emote without cosmetics, Quick Actions
without a dialog; Social in single player shows vanilla's "not
available" message, as its key now does.
- While it shows: the chat lines, toasts, crosshair (F3's too), guide
and first-person hand give way; the status rows are kept clear.
- Haptics on reaching a runnable slot, a page turn and a release that
ran nothing; narration of the page and a settled slot.
- horizon-ctl: `pad focus true|false` sends a real focus event.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
radial.rs gains what the menu's pages hold: `RadialAction` (actions that
do everything on their press; Horizon's own namespaced, vanilla's by
their key mapping, old ids read through an alias table), `Slot` (an
action, Empty, or a Missing id kept as it was, which never runs), named
`Page`s of a kind (`Fixed` slots, or `Hotbar`, read and kept), and the
default General and Camera presets, which hold only actions with no
button of their own.
`Pages::parse` and `serialize`: the `radial_<n>_*` lines of
horizon-controller.txt. A page the player hasn't changed is saved as its
preset, so it follows the defaults; `radialLayout` is read and a newer
one kept; unknown fields, kinds and slot ids past the eighth survive a
save; up to 16 pages are read and the menu turns through 6. Edits:
set_slot, rename, add, remove.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub names the selection on a dark disc: the action, and for
Perspective, Hide HUD and Cinematic Camera its state and what a press
turns it to; for a slot that can't run, why; for a missing action, its
id; while a stick must be let go first, "Let go of the stick". The large
icon goes above the name when the hub has room, and its text stays off
every tile (`fit::fit_in_circle`). Toggle tiles get state chips where
there's room for them.
Along the top, the pages are a `tabs::tab_strip` between the bumpers'
glyphs; collapsed, the bumpers name the pages they turn to. With a
bumper for the opener only the other shows. B's Cancel sits on a plate
at the lower right when the view is roomy, in the player's glyphs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client-input's radial.rs: `RadialMenu`, what the opener, the
selecting stick and the bumpers do while the menu is open, knowing pages
only by their count and slots by their index. Eight slots clockwise from
the top; a release commits once; 250 ms of neutral retention; a stick
leant since before the press waits to be let go (`StickHistory`), while a
flick that starts with the press selects; tilts that follow the
deadzone (`Tilt`); toggle mode's latch, with A running the slot settled
on (`A_SETTLE`); `DpadSettle` for a thumb rolling off the opener; and
`Settle` for narration. Every tuning value is at the top of the file.
gamepad.rs: `RadialMode` and `RadialStick`, `opener_refused` (the opener
is never under a thumb that selects, nor on B) and the radial haptics.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
radial_overlay.rs draws the radial menu over play, in the look the
mockups settled on (`docs/ui-kit.md`'s pieces): a dark vignette, the
ring's track with the selected 45° sector lit, and eight tiles on true
45° rays, each vanilla's game mode switcher slot with its action's icon
from the player's own resources. An empty slot is a pip; one that can't
run is dimmed and slashed; a missing action shows its mark. The sizes
follow the view's height (`Layout`, the scaling rule in its docs), and
the ring keeps below the boss bars and above the status rows over the
hotbar (`Keep::StatusRows`, which the HUD's room gains). Its motion
(`Look`) is quiet and never selects: a fade and scale in, the sector
easing to a new slot, the tiles sliding in on a page turn, a commit's
flash and a fade out.
Nothing draws it yet. The hub, the page tabs and the Cancel hint come
next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/pad_capture.rs: `Game::pad_capture` says which overlay over play
owns the controller now, in one priority order (the on-screen keyboard,
then a prompt over the world). The keyboard checks in pad.rs,
pad_actions.rs, pad_menu.rs and the gameplay guide ask
`pad_keyboard_captures()`. Behaviour is unchanged; the radial menu adds
its place in the order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A game answers its control socket before it has drawn a frame: `ui`, `click`, `set`,
`screen` and `click-button`, which read the last drawn frame, now wait for the first one
(UiWait), as `ui`, `click` and `set` already wait for a frame after a click.
- The ui scenario expects Create New World's Game tab, the manage scenario a widget the
Horizon options page still has (ai-agent), and the options scenario takes GUI scale 2 as
half the GUI of scale 1, so display scaling other than 100 % passes.
- The no-ai check honours HORIZON_E2E_HEADLESS like the other scenarios.
Fixes#12972
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pausing the integrated server for the copy ran its pause save (IntegratedServer.tickServer's
saveEverything) during the copy, often after the call that saved had returned. That save
renames level.dat and the player files into place and hands region files to the region
writer, so the copy failed with "The system cannot find the file specified", or could read
a region file mid-write. Now the server thread saves, then waits on its own thread until the
copy is done; the pause flag stays as it was.
Fixes#12977
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ParticleEngine::tick ran every live particle one after another. Most of them (flames,
smoke, poofs, dust, crits and a firework spark's own motion) read and write only their
own state, the world and the sprite sets, so tick_simple now ticks them on up to four
scoped threads once there are 2048 particles. A spark's trail child, which takes the next
engine seed, and every other behaviour still run on the engine thread afterwards, in the
order they had, so the particles, the engine's random and the sounds come out as before.
On the stress_particles mix (isolated benchmark, particles/tick_bench.rs): whole-scene
engine tick 1.81 ms on one thread, 1.40 / 1.27 / 1.18 / 1.15 ms on 2 / 3 / 4 / 6 threads.
parallel_tick_matches_serial_tick runs both over 90 ticks with rockets and compares every
particle, the engine's random and the sounds.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
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]>
Post BLOCK_CHANGE at the frame from ItemFrame.interact, hurtServer and
dropItem, after the changes and drops vanilla performs first. Preserve
BlockAttachedEntity.kill ordering and discard unsupported frames without
a spurious ENTITY_DIE.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
/damage: keeps this branch's villager reputation flush after a hurt and
main's enderman teleport settle (they act on different mobs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main split projectile_hit into a wrapper and projectile_hit_inner: the
reputation flush now runs in the wrapper after the inner hit. Trident and
wind charge hits keep both the reputation flush and main's absorbed-damage
and post-attack calls.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep both sides of the potions.rs test modules; the contents fixture passes
the impact motion that main's splash now takes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
entity_portal_delay walks every entity list; it ran for every item, mob,
boat, cart and pearl in the End each tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review: Entity.applyImplicitComponent applies two components, the custom
name and minecraft:custom_data (saved as `data`), and BoatItem.use /
BoatDispenseItemBehavior turn the boat to the player's or dispenser's yaw
after the stack's config, so entity_data's Rotation does not keep it.
Also moves place_minecart's doc line back onto it.
Harness group "vehicle item stack config": the three new cases 24/27
before, 27/27 now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecartItem.useOn, BoatItem.use and the dispenser's
MinecartDispenseItemBehavior / BoatDispenseItemBehavior apply
EntityType.createDefaultStackConfig to the new vehicle: the item's custom
name (applyComponentsFromItemStack), then minecraft:entity_data loaded
over it (updateCustomEntityTag) when it names the vehicle's type, an
op-only type only from an op player and never from a dispenser. Horizon
placed default vehicles. Game::apply_vehicle_stack_config does both,
through the /data merge loader, with the spawn egg's guard.
Command harness group "vehicle item stack config": 12/23 -> 23/23.
Fixes#11195
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:36:09 +03:00
656 changed files with 43189 additions and 8327 deletions
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
@@ -47,6 +55,29 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- **Render passes keep HDR's light.** A pass that reads, filters or redraws the world's picture (upscaling,
anti-aliasing, sharpening, frame generation, post effects, captures) follows [docs/hdr-passes.md](docs/hdr-passes.md).
## Don't lose time
Where agent time went, measured on sessions and CI in November 2026: reading code, compiling `horizon-client`,
waiting in loops, and work someone else had already done. Tests were a small part.
- **Check nobody else is on it.** One in three pull requests closed unmerged was a duplicate. Before you write
code, look at the issue's linked pull requests and run
`gh pr list --state open --search "<issue number, function or file name>"`; then `/claim`.
- **Find code with the map, not by reading.** Start from the tables below and
`python3 tools/port_ledger.py where Class.method`. Read the function you change and its callers, not whole
files.
- **Compile cheaply while you work.** `cargo check -p <crate>` after an edit. Clippy, fmt and tests run once, at
the end, through `pr_ready.py`. `cargo clippy -p horizon-client` and `cargo build -p horizon-client` are the
slowest commands here: never after every edit, and build the game only when you need to run it.
- **Don't wait in loops.** Start a long command in the background and carry on, or wait for it once. No
`sleep` or `until grep` polling of builds, harnesses or CI.
- **One test per changed behaviour.** None for a refactor or a file move, none that reads source text, and no
harness where vanilla has nothing to compare (Horizon's own features, performance work).
- **A red check isn't always yours.** When `verify` fails in a test your change can't reach, say so in the pull
request and move on; don't hunt it.
- **Unsure about a rule? Finish the work.** Take the reading that lets you finish, and say in the pull request
which one you took. Don't stop, and don't change direction halfway.
## Crates
Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-server` are separate workspaces.
@@ -96,6 +127,7 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
- Use independent expected values or a vanilla oracle; do not derive the answer from the code under test.
- Test outcomes, not only calls to stubs. Use mocks to isolate behavior, not as the behavior itself.
- Skip tests of trivial constants, derives and getters, or behavior already covered by another test.
- One test per changed behaviour is enough. A refactor or a move that changes no behaviour gets no new test.
- Avoid pinning implementation details; a harmless refactor should not break a behavior test.
- Make slow tests faster without removing cases or weakening their coverage.
- Never delete parity harnesses or tests that pin vanilla behaviour: they are the spec.
@@ -152,7 +184,8 @@ python3 tools/lang_coverage.py --summary # the keys each language still misses
- A harness runs the oracle against the real 26.3 jars and the Rust probe on the same inputs, then compares
their JSON. Oracle runs are cached by `tools/parity_shim.py`; `--fresh` reruns them.
- Mirror vanilla's `Math` calls with `horizon_core::jmath`, and vanilla's RNG with `horizon_core::random`:
random draws must happen in vanilla's order.
random draws must happen in vanilla's order. Performance work may break that order where no player can rely
on it; the pull request says so ([CONTRIBUTING.md](CONTRIBUTING.md#when-it-doesnt-have-to-be-11)).
## horizon-client: where things live
@@ -215,7 +248,8 @@ player does (#1514). A window's `Game` never ticks a world itself: the old in-fr
Open `[server-thread]` issues list the gaps left.
Single player, LAN guests and dedicated servers all run the server code, so a fix there serves all three.
Test the server thread when you touch player-facing gameplay:
When you change how the server and its clients talk (packets, tracking, guests, the server thread itself), check
it there. Other gameplay fixes are covered by their own harness and CI's parity job:
```sh
cargo run --profile fast -p horizon-client -- --resources .local/horizon-client/26.3 --world T # server thread
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) | Two owners since 2026-11-11: the founder and rdbtCVS (P28); a `council` team holds CODEOWNERS (P35) and is the security managers (P36) | Yes: an organisation can have several owners | **Step 1:** a second owner from the council, chosen by a 2/3 poll in the council's first week, with two-factor authentication on. **Step 2:** a `council` team; CODEOWNERS reviews and the release tools' owner powers (`paused`, `/nightly remove`) move from the founder to that team |
| **Discord server** | Owned by the founder | Partly: a server has exactly one owner; powers can be given through roles | **Step 1:** the 🏛️ Council role gets moderation and ban permissions; Kira applies the outcomes of passed polls, so nobody needs Manage Roles; two-factor authentication becomes required for moderation. Ownership itself stays with the founder until a legal entity exists |
| **Kira** (bot application, Railway service) | The founder's Discord developer account and Railway | The bot: yes (a Discord developer team). Railway: adding members may need a paid plan | **Step 1:** a Discord developer team with two council members. Railway members once the council decides on money |
| **Official server** (hosting panel) | The founder's account | Yes: panel sub-users | **Step 1:** sub-users for the Server and SMP lead and for one council member |
| **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.
Needs Rust 1.90+, Python 3.10+, JDK 25, and on Linux `libasound2-dev libudev-dev`. `horizon-prepare` builds the game's resources from **your own copy** of Java Edition 26.3 (checked against `research-lock.json`) and never downloads it:
Needs Rust 1.90+, Python 3.11+, JDK 25, and on Linux `libasound2-dev libudev-dev`. `horizon-prepare` builds the game's resources from **your own copy** of Java Edition 26.3 (checked against `research-lock.json`) and never downloads it:
1.**Client:** play 26.3 once in the official launcher. Horizon reads `~/.minecraft` (or `--minecraft-dir` / `HORIZON_MINECRAFT_DIR`).
2.**Server jar:** the 26.3 server jar (`downloads.server.url` in `versions/26.3/26.3.json`), passed once with `--server-jar` or `HORIZON_SERVER_JAR`.
@@ -135,6 +135,8 @@ python3 tools/pr_ready.py
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.
"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."
// This thread takes the first share, instead of waiting for the others.
ifletSome((part,done))=own{
run(part,done,level);
}
});
for(p,done)inparticles.iter_mut().zip(handled){
matchdone{
Simple::No=>self.tick_particle(p,level),
simple=>self.finish_simple(p,simple),
}
}
}
}
Loaded 100 of 656 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.