Agents had to open issues one by one to see if they were taken, and the rule
did not mention Discord claims (Kira's `in progress` label). Give them the
ready-made search and name both claim signals.
Fixes#11367
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.getDamageAfterMagicAbsorb applies Resistance, returns 0 when
nothing is left, returns the damage as is for #bypasses_enchantments (the
warden's sonic_boom), and only then the enchantment protection. Horizon's
Mob::hurt_server ran the protection first and never read the tag, so a
zombie in a Protection IV chestplate kept 1.6 of a 10-point sonic boom.
DamageKind::bypasses_enchantments (SonicBoom, or a NamedDamage with the
new BYPASSES_ENCHANTMENTS tag) gates the protection, which now follows
Resistance as vanilla orders them.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
`ServerExplosion.hurtEntities` damages every entity in reach, and
`ItemEntity.hurtServer` writes the result back:
this.health = (int)((float)this.health - amount);
if (this.health <= 0) { this.getItem().onDestroyed(this); this.discard(); }
Horizon's item branch only asked whether the damage reached 5 and destroyed the
item, so an item that lived through a blast kept all 5 points of health: a
power-5 bed blast that leaves vanilla's item at 1 left Horizon's at 5.
The damage is now subtracted in f32 and truncated after the subtraction, as the
cast does, and the item is destroyed at zero health as before. The nether star
stays immune through `ItemStack.canBeHurtBy`'s explosion exception, and a blast
whose source is an entity with `mobGriefing` off never reaches the branch
(`items_ignore`).
A dropped item starts with 5 health and `ItemEntity.hurtServer` takes the
blast's damage off it, so a survivor's `Health` says how much of the blast it
took. The pinned compound the ring already reads carries it, so the compared
line grows one fact:
data push -0.03 -0.00 -0.05 health 1 keep 0.9800
Red on main: Horizon reports `health 5` where vanilla reports `1`.
BoneMealItem.addGrowthParticles places a NEIGHBOR_SPREADER's happy
villager sparks at BonemealableBlock.getParticlePos, which is the block
above for grass, nylium, netherrack and moss; a GROWER keeps the block
itself, rooted dirt and mangrove leaves the one below. Horizon used the
bone-mealed block for spreaders too, a block lower than vanilla.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Player.actuallyHurt awards DAMAGE_TAKEN as Math.round(dmg * 10.0F) only
while dmg < 3.4028235E37F. Horizon awarded every hurt with Rust's round,
so /damage @s 3.4e38 added a saturated i32::MAX where vanilla adds
nothing. A damage_taken_award helper applies the threshold and
jmath::round_f32 (Math.round(float): ties up, saturating).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A modded 1.21.11 server's own entity type (Sit's sit:entity_sit is 157) kept its number, which in 26.3 is
the zombie villager: every sitting player showed one under them. Chunks already dropped such block entities;
their updates now go too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Creeper.readAdditionalSaveData reads ExplosionRadius as a byte (default
3), addAdditionalSaveData writes it back, and explodeCreeper blows with
it, twice it when powered. Horizon used a fixed 3 or 6 and never read the
key, so a summoned creeper with ExplosionRadius:12b blew with 3.
Extra.explosion_radius holds it (3 from Mob::new), entity_nbt reads any
numeric tag narrowed to a byte and writes the byte, and creeper_tick
scales it by the powered multiplier.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
The survivor the harness gained at the edge of the blast is read through the
item's own tick counter. A push on an item decays every tick
(`Entity.getAirDrag`, 0.98), and the two servers do not advance the same number
of game ticks between the same commands: at three script points of one case the
vanilla survivor was 31, 33 and 48 ticks old and Horizon's 38, 41 and 75, so a
`Motion` read there measures the pacing rather than the blast.
The edge item's `Age` is now read before the blast, and its whole NBT (`Age`,
`Pos` and `Motion` from one snapshot) at two pinned ages after it. The case
compares the push carried back over the ticks since the blast, to two decimals
because a tick of slack between the blast and the read is worth 2%, and the
decay the two snapshots show per tick, to four, which is `getAirDrag`'s 0.98.
Both agree with vanilla's in all twelve cases.
SnbtGrammar accepts one trailing comma before the closing brace or
bracket; a comma alone and two in a row stay errors. Both readers take it:
horizon-world's parse_snbt (NBT arguments, item components) and the text
component reader (tellraw and other component arguments).
Fixes#11285
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Record the actual block entity or its existing pure save projection in a local Pending, then attach existing component copies to the unchanged block drops. Keep source blocks and loot replacement policy unchanged.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A mooshroom's mushrooms, an iron golem's poppy, a snow golem's pumpkin and a
copper golem's antenna block are submitted through BlockModelRenderState.submit,
whose block-item sheets (ITEM_CUTOUT, ITEM_TRANSLUCENT) cull back faces. Only the
enderman's carried block culled, so the coplanar faces of the other layers' cross
models z-fought as the mob moved (#5301).
The cull now goes by each quad's winding, as the GPU's does, rather than the baked
facing, which is the nearest axis and 45 degrees off for a cross model's planes.
Fixes#5301
Root cause: #9335 follow-ups from #8733 left passenger rotation after /tp covered only by parity corpora (tools/passenger_teleport_corpus.py), which cannot run without vanilla data. The Game-level rider test exercised offset carry (sub-item 1) but not the rider keeping its angle to the vehicle through absolute and relative turns (sub-item 2).
What changed: extend teleport_rider_tests::a_vehicle_carries_its_riders_and_a_rider_leaves_its_vehicle with a second horse/villager pair: absolute turn 0->40 leaves rider at 70 (30-degree offset kept), relative turn to 50 shifts rider 70->80, and facing (turn_riders=false) leaves rider at 80. No runtime change; the teleport/passenger writers already implement Entity.teleportSameDimension/calculatePassengerTransition via vehicle_teleport::passenger_rotation.
Vanilla reference: 26.3 Entity.calculatePassengerTransition, PositionMoveRotation.calculateAbsolute, Entity.setXRot (pitch clamp).
Refs #9335
tools/test_enchanting_parity.py always built a fresh ItemStack for its offer
corpus, so EnchantmentMenu.slotsChanged's ItemStack.isEnchantable gate (the
enchantable component and an empty minecraft:enchantments) was never
exercised, and the oracle skipped the gate entirely. Fixture lines now carry
damage and a pre-existing enchantments entry; the oracle applies the gate and
the probe builds the same stack. The default corpus is unchanged and
--no-components reproduces it.
68992/68992 enchanting cases match.
A crystal's spin and bob start from a random draw. Which crystal gets which
draw, and a server's entity ids that seed a client's copies, follow the order
chunks load in, so crystals stood at different angles in each launch of a
frozen scene. Under HORIZON_STILL the start now comes from the position; the
draw is still made, and normal play is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hand's quads from a translucent sheet (stained glass, ice...) go after the opaque ones in
its buffer and draw after deferred with gbuffers_hand_water (or its fallbacks: gbuffers_hand,
textured_lit, textured, basic), blended, as translucent terrain does; through gbuffers_hand
they came out solid.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Gui.setScreen runs KeyMapping.releaseAll for every screen and
Minecraft.tick skips handleKeybinds while one is up, books, the sign
editor, dialogs and prompts included. Horizon keeps Screen::Playing under
those, and its release-all (track_toggle_screens) and the client input
gate (remote_handle_input) only knew the server dialog, while the book's
and sign's mouse arms eat both edges of a click: a dig held into a book
went on after the book closed, until the next click.
gameplay_has_keyboard now takes "a screen is over the world"
(screen_over_playing) in place of the dialog alone, and remote_handle_input
is not ready while one is up.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
LocalPlayer.getRopeHoldPosition eases the lead's hand end from the view
to yRotO/xRotO by half the partial tick. Entity.turn moves yRotO and xRotO
by the mouse's delta too, and AbstractBoat.clampRotation moves yRotO with
the clamp, so only what the tick turned (a boat's deltaRotation) eases in.
Horizon's rope_rot_old was a once-a-tick snapshot the turn never moved:
while the mouse moved, the hand end lagged by half the turn since the
tick began and snapped back at the next tick, 20 times a second.
turn_player now moves rope_rot_old through the same Rotation::turn as the
view, and both boat clamps (the turn's and the tick's) move it with the
view. A test pins that a mouse turn leaves no gap, a boat turn eases, and
xRotO clamps at 90.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the timer
`still_tick` keeps in `counter`; `readAdditionalSaveData` reads it back and
a save without it is 0.
0, 7 and 10 through `mob_to_nbt` and `mob_from_nbt`: the Int vanilla writes,
the timer back on the mob, and `on_still_timeout` following it. Without the
writer the saved tag has no `still_timeout` (the test fails on `left: None,
right: Some(0)`).
`HappyGhast.addAdditionalSaveData` writes `still_timeout`, the
`serverStillTimeout` behind `staysStill`/`isOnStillTimeout`, and
`readAdditionalSaveData` reads it back:
protected void addAdditionalSaveData(ValueOutput out) {
super.addAdditionalSaveData(out);
out.putInt("still_timeout", this.serverStillTimeout);
}
Horizon keeps that timer in `Mob.extra.counter` (`happy_ghast::still_tick`
counts it down after the first 60 ticks, `player_above` refreshes it to 10)
but neither `mob_to_nbt` nor `mob_from_nbt` touched it, so a ghast saved
while it was held still came back with 0. For the ticks vanilla restores, a
dropped item resting on its back had no box to rest on
(`EntityGetter.getEntityCollisions` takes a happy ghast only while
`isOnStillTimeout`), and a rider had no control (`getControllingPassenger`
asks `isOnStillTimeout`).
ByteBufCodecs.holder in vanilla 26.3 reads a VarInt and, when it isn't 0,
looks the entry up with `byIdOrThrow(id - 1)`, which no negative id passes.
skip.rs's `holder` accepted any nonzero id, so a slot whose kinetic_weapon
sound holder was negative was kept as a raw component, and hashing it for an
inventory click computed `id - 1` on i32::MIN: a panic with overflow checks
on, a wrapped id otherwise.
ByteBufCodecs.holderSet reads the size plus one and builds
`new ArrayList(size)`, which refuses a negative size. `holder_set` computed
`n - 1` before checking the sign, the same overflow for i32::MIN.
Both readers now refuse a negative value with a protocol error, so every
component that reads a holder or a holder set gets the same boundary.
Valid direct and registry holders read as before (their vanilla hashes in
hash/raw.rs's tests are unchanged).
Fixes#11222
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntityRenderer.isShaking is isFullyFrozen (DATA_TICKS_FROZEN at or
over 140), and setupRotations wobbles the body while it is. Mobs and
other players already shiver, but the window's own model, rebuilt each
tick by tick_player_model, never got the player's frozen ticks, and the
window ignored the DATA_TICKS_FROZEN its server sends it, so a player in
powder snow stood still in third person.
The own model takes ticks_frozen from the player's body, and own entity
data index 7 sets it on the window's player as LocalPlayer reads it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Guard against negative scale values producing excessive shift
distances in CheckerboardBiomeSource::noise_biome by clamping
scale to a non-negative lower bound before adding the +2 offset.
This keeps the shift distance within a sane range and avoids
surprising behavior for out-of-range runtime scale inputs.
ServerGamePacketListenerImpl handles a client's packets in order: the
RELEASE_USE_ITEM a client sends from handleKeybinds comes before the move
LocalPlayer.tick sends in the same client tick, so the server shoots from
the position of the move it handled the tick before, one tick of strafe
behind the camera. Hub::process_inbox applies moves at once and hands a
PlayerAction to the game for after the pass; it held a queued move behind
a dig, UseItem, UseItemOn and Interact, not behind the release, the drops
or the stab. The move ran first, and the arrow left a tick ahead of the
camera: strafing left, it seemed to come from the left instead of the
right.
Every PlayerAction now holds the queued move for the next tick. A test
drains a release followed by a move and checks the game gets the release
alone with the guest's position and known movement unchanged.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Entity.removePassenger sets the leaving passenger's boardingCooldown to
60, but Player.removeVehicle overrides Entity.removeVehicle and zeroes it
right after super.removeVehicle(), so a player never waits to board again;
only mobs do. #8733 (from #8586, which missed that override) gave the host
and every guest the 60 ticks, checked by may_board before boarding a boat,
cart, cushion, ghast, llama, mount or nautilus: the delay the issue
reports.
The player's cooldown goes: the boarding_cooldown and riding_at_tick
fields of Game and Guest, their swap for a guest's turn, the per-tick
count-down and may_board at its eight call sites. Each interaction still
refuses a sneaking player, the other half of Entity.canRide. Mobs keep
extra.boarding_cooldown.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A 60 fps clock steps 16.666667 ms a frame, which a whole number of
milliseconds can't give. FrameTiming::nth takes the step as f64.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackInputs::frame_timing gives the frame number, the seconds since frame 1
and the seconds since the frame before. Iris-format packs take it for
frameCounter, frameTimeCounter and frameTime and smooth wetness,
eyeBrightnessSmooth and centerDepthSmooth by its delta; custom uniforms
start over on its frame 1 and carry their state after. Horizon packs take
it for time, frame_index and delta_time, ease by it, and do not treat a
slow frame as a gap. Nothing sets it yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.addAdditionalSaveData writes FallFlying as isFallFlying(), and
readAdditionalSaveData sets shared flag 7 from it, so a player who saves
mid-glide resumes the glide on the next load. Horizon wrote a fixed
FallFlying of 0 and never read it back: the player came back standing in
mid-air.
Level now keeps fall_flying, read from any numeric FallFlying tag (as
getBooleanOr does) and written from the guest's glide; playerdata's store
and apply carry it between the Guest and the file. The server's next
sync_player_state sends the 0x80 shared flag to the client, and
updateFallFlying's port ends the glide if it can't go on.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Entity.shouldBeSaved is true for every projectile in 26.3, and
WitherSkull adds `dangerous` on top of the hurting projectile's
fields. Horizon listed WitherSkull as not saved, so a skull still in
the air when its chunk unloaded was gone when the chunk came back,
and a blue skull came back as nothing.
Write and read the skull the same way as the fireballs: `dangerous`
as a byte (absent means a black skull), and no `Fire` since
WitherSkull.shouldBurn is false. The fire rule now keys on the
fireballs proper (is_item_fireball) instead of "not blast-immune",
which would have lit a saved skull.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Post the existing damage event for accepted health-changing mob command hits so a same-tick teleport cannot move its origin. Preserve the existing duplicate marker and reinforcement handling; pin the real command/listener route against Java.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
tools/test_block_removal_parity.py only ever mined as a survival player:
every mine case ran ServerPlayerGameMode.destroyBlock in SURVIVAL, so the
creative branch (Player.preventsBlockDrops, no mined statistic, no tool
wear) was never compared even though game.rs::break_block_as_player splits
on it. Half the mine cases now run in creative, with a tool that can destroy
blocks in creative (a sword and a mace cannot, which the creative-mining
predicate corpus already owns).
VanillaDispenserOracle's Miner gains a creative flag (isCreative and
hasInfiniteMaterials) and the real CREATIVE game mode; the game's dispense
fixture sets the same mode for the case. --survival-only reproduces the
corpus as it was before this change.
4000/4000 cases match on the new corpus.
gbuffers_skybasic drew every part of the sky with the translucent blend.
Vanilla 26.3 draws the sky disc and the dark disc with RenderPipelines.SKY
(ColorTargetState.DEFAULT, no blending), the sunset fan translucent and
the stars with RenderPipelines.STARS (BlendFunction.OVERLAY: colour
SRC_ALPHA ONE, alpha ONE ZERO). Iris 1.11.7 drew them so with probe packs
of ours: the sky disc kept exactly what the program wrote, (0.3, 0.6,
0.9, 0.25) in buffers of four formats, and the stars added 0.25 x 0.5 to
the disc's 1.0 and left their own alpha 0.5. Without a
blend.gbuffers_skybasic of the pack's own, the disc and the void, and
the stars, now draw with pipelines of the same program that blend so,
keeping a blend.gbuffers_skybasic.<buffer> the pack sets (Iris blended
the disc into that buffer only).
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program writing a float vec4 into an integer colortex (RGBA32UI...) was
refused: wgpu has no float output into an integer target, so the whole
program, and every buffer it writes, was lost. Iris 1.11.7 drew such a
program with a probe pack of ours: its other buffers kept what it wrote,
and the integer buffer read the floats' bits back (GL leaves that value
undefined; these are NVIDIA 615.78.08's). The output now writes the
float's bits as the buffer's integer type.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon always made its default noise texture 256 wide. Iris 1.11.7
made it the size a pack's `const int noiseTextureResolution` gives, when
declared in a fragment shader (composite, deferred, gbuffers, final, or
shadow.fsh with no shadow pass), and ignored it in a vertex shader, in
probe packs of ours. The setting is now read like a colortex format.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`.length()` of a runtime-sized array in a named buffer block made naga's
validator refuse the program: its GLSL frontend loads the array and takes
the length of the loaded value. After parsing, the length is now taken of
the array's pointer. Iris 1.11.7 ran such a program in a probe pack of
ours and read the buffer's length (bufferObject size / stride).
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon decided whether to run the shadow pass from the samplers of the
programs' vertex and fragment stages only. A pack whose only reader of
shadowtex0 or shadowcolor0 was a compute stage (prepare.csh alone, or a
composite.csh beside a composite.fsh that reads none) got an empty map
(1.0), while Iris 1.11.7 drew it for probe packs of ours (0.3956 depth,
0.251 shadowcolor0 at our noon scene). Compute stages now count as the
other stages do.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon made an R11F_G11F_B10F custom image as RGBA16F, then refused
every program that declared it layout(r11f_g11f_b10f), since the shader's
storage format no longer matched the texture. Iris 1.11.7 stored into such
an image and read it back, with a probe pack of ours: (0.25, 1.5, 3.0)
read back as written, alpha 1, also filtered between texels. The image is
now Rg11b10Ufloat where the device stores into that format; elsewhere it
stays the RGBA16F stand-in, and the shader's image declares that format.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The default blend of gbuffers_skytextured added the sun's alpha over the
sky's (One, OneMinusSrcAlpha). Vanilla 26.3 draws the sun and moon with
RenderPipelines.CELESTIAL, BlendFunction.OVERLAY: colour (SRC_ALPHA, ONE),
alpha (ONE, ZERO). Iris 1.11.7 left the sun's own alpha (2.0) in an
RGBA32F buffer the sky had written 1.0 to, with a probe pack of ours;
Horizon left 1.0.
Part of #10490
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Thank you for adding Thai to the launcher in #8306. We were really
happy to see it. When we used it, though, many lines were hard to
understand for us as Thai players. The translation looks
machine-made, and many words have their everyday meaning instead of
their computer meaning. We rewrote it so that Thai players can use the
launcher comfortably. 731 of the 1225 strings changed. The others
were already good, so we kept them as they were.
Below is each word we changed, what it is now, and how a Thai reader
understands the old word. We hope this makes our reasons clear, even
if you don't read Thai:
- build: อาคาร / โครงสร้าง -> บิลด์
อาคาร means a building (a house, an office). โครงสร้าง means the
structure of something. "Native build" read as "the original
building". Thai developers and players say บิลด์, written as it
sounds.
- instance: ตัวอย่าง -> อินสแตนซ์
ตัวอย่าง means "example" or "sample", so "Instance" looked like a
demo copy of the game, not a separate game setup. อินสแตนซ์ is the
English word written in Thai, as Thai players say it.
- native (mod, build): ดั้งเดิม -> เนทีฟ
ดั้งเดิม means traditional or original. "Native mod" read as "the
original mod", which says nothing about compiled code. เนทีฟ is the
usual Thai programming word.
- launcher: ตัวเรียกใช้งาน / ตัวเปิด / (left in English) -> ลันเชอร์
The same thing had three names, so a player could not tell they
were one program. ลันเชอร์ is what Thai players call it.
- jar: ขวด / โหล -> ไฟล์ jar
ขวด is a bottle and โหล a glass jar for food. "Port gets the jars"
read as "Port gets the bottles". It is a file type, so ไฟล์ jar.
- Loader: รถตักดิน -> ตัวโหลด
รถตักดิน is a wheel loader, the construction vehicle.
- Client: ลูกค้า -> ไคลเอนต์
ลูกค้า is a shop customer. Here it means the game client.
- Running / run: วิ่ง -> กำลังทำงาน / รัน
วิ่ง is running on foot. "Running" read as "jogging". A program
that runs is ทำงาน (working) or รัน.
- Proxy: หนังสือมอบฉันทะ -> พร็อกซี
หนังสือมอบฉันทะ is a legal power-of-attorney letter.
- port: ท่าเรือ -> พอร์ต
ท่าเรือ is a harbour for ships. A network port is พอร์ต.
- cape: แหลม -> ผ้าคลุม
แหลม is a headland on a coast (or "sharp"). A Minecraft cape is a
cloak, ผ้าคลุม.
- cosmetics: เครื่องสำอาง -> คอสเมติก
เครื่องสำอาง is make-up (lipstick, powder). Thai gamers call
in-game items to wear คอสเมติก.
- seed: เมล็ดพันธุ์ -> seed
เมล็ดพันธุ์ is a plant seed for farming. Thai Minecraft players use
the English word for a world seed.
- Home (page): บ้าน -> หน้าแรก
บ้าน is a house. The Home page is the first page, หน้าแรก.
- Ban: บ้าน -> แบน
Ban was also translated as บ้าน ("house"), so the ban button said
"house". Thai players say แบน.
- founder (set): ผู้ก่อตั้ง -> ผู้เล่นรุ่นแรก
ผู้ก่อตั้ง is the person who founded a company, so "Free for
founders" read as "free for the people who started Horizon". The
shop page says the set is for "the players who were here first", so
it is ผู้เล่นรุ่นแรก, "early players". It is not "sponsors" either:
the set is free.
- player counts: คน -> ผู้เล่น
"{online} of {max} players" used คน (person). In a server list Thai
players expect ผู้เล่น (player), as the game itself says.
- Java Edition: รุ่นจาวา -> Java Edition
A product name, like Minecraft and Microsoft, which stay in
English. รุ่นจาวา ("the Java version") reads as a Java release.
- 78 strings still in English -> translated
They showed English in the middle of a Thai screen.
- Polite particle ครับ removed
ครับ is the spoken "sir" that ends a sentence when a man talks. In
UI text it sounds like a person talking, not a program.
We kept every {placeholder} and every middle dot of the English, and
we used one Thai word for each English term across the whole file.
The two of us, both native Thai speakers, read every changed line
(SamsVT and NotNiceKun). If anything still reads wrong,
please tell us and we will gladly fix it. Thank you for Horizon!
Narrate incompatible server list entries matching vanilla narration order: select -> incompatible -> version -> motd. Also remove the note about incompatible server status from docs/narrator.md known differences.
Closes#10317
Co-Authored-By: Antigravity <[email protected]>
macOS and Windows are supported platforms with dedicated documentation under docs/platform/. Replace the stale 'macOS and Windows are out of scope' sentence with a reference to docs/platform/.
Fixes#11145.
Co-Authored-By: Antigravity <[email protected]>
On Linux, Horizon asks ALSA for a 1024-frame fixed buffer to keep sound effects low-latency. When PipeWire is configured with min-quantum >= 1024 (common for Bluetooth audio or in VMs), the 1024-frame buffer repeatedly underruns with POLLERR and produces no sound.
Allow setting HORIZON_AUDIO_BUFFER=default or HORIZON_AUDIO_BUFFER=<frames> (e.g. 2048) to override the fixed buffer request and use a larger or system-default buffer. Document the environment variable in native-client.md.
Fixes#11082.
Co-Authored-By: Antigravity <[email protected]>
DEFAULT is a Path object that formats with backslashes on Windows, while harness_args.py appends str(default). Pinning against str(DEFAULT) instead of the hardcoded POSIX string lets the test pass across all platforms.
Co-Authored-By: Antigravity <[email protected]>
Keep removed mob entries out of command enumeration and UUID/ID lookup before
the normal sweep, and prevent a discarded passenger from saving inside its
vehicle. Preserve existing death-time and health rules.
Validate frozen Game commands/API37/save assembly against real26.3 discard
and entity-manager behavior with both client and server JARs.
Co-Authored-By: Codex <[email protected]>
Horizon split a dying cube at the moment of death. Vanilla
AbstractCubeMob.remove runs the split when the entity is removed, which
LivingEntity.tickDeath does after the death animation (deathTime 20),
so the smaller variants appear about one second later.
The oracle's stand-in player overrode LivingEntity.startUsingItem and only recorded the item, so
the real vanilla vibration never ran: ItemStack.causeUseVibration posts ITEM_INTERACT_START when
the stack's use effects ask for interaction vibrations, which every item's UseEffects.DEFAULT
does (DataComponents.COMMON_ITEM_COMPONENTS). Since #10272 Horizon posts it, so the harness
reported 284 false mismatches (4916/5200). It now calls causeUseVibration under the same guard
as vanilla: 5200/5200.
A first draft by DeepSeek V4.1 Flash, via pi, reviewed by Claude.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep entity actions and API 29 ahead of item fallback, reuse normal packet and crosshair selection, and let the first response consume with or without an acting-hand swing. Cover bounded native Mob entries with a pending fragment, generated dispatch and actual Game priority/filter tests; leave other living interaction tables explicit.
Co-Authored-By: Codex <[email protected]>
Keep ordinary wear, Unbreaking, durability criteria, stack removal and break handling in one path. Existing damage_held callers retain MainHand behavior; callers that model an offhand use may select OffHand without editing stack damage directly.
Validation: native prepared-resource Game test verifies offhand wear, break event48, unchanged main-hand stack, legacy event47 and creative immunity. Strict gameplay clippy, formatting and public-tree checks pass.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: foudretbr's rebase onto main (a1ff75a4a) drops the stray `);` that broke `tests/mod_api_hooks.rs`, and the file-wide import reorder. The hook change is the same 9 files: with no mod loaded, every translucent screen background is still the vanilla gradient. The 10 hook tests and tests.test_stamp_native_hooks pass.
🤖 Agent: merged main so this builds again (its old base still called `mob_to_nbt` without the UUID lookup that #10544 fixed). Burst notes are unchanged: one group per packet kind, each naming the player who spent the most time; the 7 burst and event-timing tests pass on the merged head.
Cache conservative envelopes of every baked model alternative, invalidating
on mutable model access. Reject rear-origin falling blocks only when their
complete padded model is outside the posed camera's depth clip planes.
Depth-only clipping preserves TAA jitter and screen-edge geometry; shader
packs keep their original path.
Include geometry/cache regression tests and separate ordinary/TAA FPS and
image comparisons, with front/empty controls and an oversized model fixture.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: README/docs-media only; keeps the Project status block and '### Java parity' table the site and tests read, so playhorizon.dev's parity number is unaffected.
The README's pictures were from mid-October (launcher 0.2.30, the old
menus), so this retakes them and tidies the page around them.
- Pictures: Nightly 2026-11-05 (66f3981) windowed on the Mac mini's M4
Pro (Metal) with seed 8675309: a river valley with a village and a long
view, a cherry-grove sunset, a crimson forest with a piglin, survival
with the HUD, and two players over LAN; the launcher 0.2.47's Home (with
those screenshots in its instance) and Mods pages. WebP, 712 KB in all,
under docs/media/readme/. The five README-only images they replace are
removed; village/nether/underwater.webp stay (the launcher embeds them).
- Text: a short pitch, what works in the current release, a download
table with each platform's file and requirements (Windows x64 and Arm,
macOS Apple Silicon and Intel from 12, Linux glibc), the Microsoft
sign-in requirement, mods and shader packs (Java mods don't run;
modpacks import), hosting, agent control, build from source,
contributing, and Mojang's "NOT AN OFFICIAL MINECRAFT PRODUCT" line.
- Fixed claims: macOS Intel and Windows on Arm are out (not "from the next
Nightly"); the known-issues line points to docs/known-issues.md.
- Two badges instead of seven: the parity and gap badges the tests and
tools/parity_gaps.py keep in sync. "Project status", its gap block and
the "### Java parity: N%" table are unchanged, so the website still
reads its number and #build-from-source/#project-status still resolve.
Builds on #11040 by Web-mc1, who first noticed the launcher pictures were
outdated and retook them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MobEffectInstance.Details.MAP_CODEC uses optionalFieldOf("ambient", false)
and ("show_particles", true), so like amplifier 0 those defaults are not
written (the mob writer already does this). The harness gets a default
work dir, so tools/parity.py can run it with no arguments; the run-log
doc is dropped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity.py runs each harness with no arguments, and this one exited
asking for --work-dir; it now uses a fresh folder under .local.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob::voice already plays at the feet (Entity.playSound), stays quiet for a
Silent entity, uses getSoundVolume and HappyGhast's pitch 1.0, so the
special arm is not needed and a Silent happy ghast no longer sounds. The
test checks the Silent case too. The 238-line capture harness ran a Java
server, a native server and a client for 1200 ticks on every ai.rs
change; the resource test covers the position.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merged #10312 (with main) in. The cache compares the whole Scoreboard twice per frame (displayed_ids and layout) and clones it on every change, so a server ticking a sidebar score every tick rebuilds and clones each frame; still cheaper than the old double sort, and tab_list parity passes on a union holding this head.
🤖 Agent: merged main in: #10328 (merged meanwhile) now caches the layouts of the tags your collect_name_tags keeps, and #10098's new hide_gui argument is passed in your early tab_list::visible check too, so F1 still hides the list on the skip path. Skipping a tag whose entity box (inflated 0.5) is off the frustum matches vanilla, where shouldRender gates the name tag with the entity. 194 selected harnesses run on a union holding this head: tab_list and hud pass, the 8 failures are identical without it.
Minecarts and primed TNT now seat their riders from the riders map;
the branch's seated box is kept on that path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: shelf mushroom stands in as brown_mushroom_block (solid, not the walk-through brown mushroom), explorer maps as filled_map; 26.2 refusal from #10477 still in place
The x64 macOS launcher (#1161) installed and ran the game, but native mod builds
stopped with "the launcher has none for macos x86_64": its managed toolchain
had no x86_64-apple-darwin host. Add Rust 1.99.1's official rustc, cargo and
rust-std for it, pinned by the SHA-256 static.rust-lang.org publishes; the
linker is the system's, as on Apple Silicon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Prevent vertical letter-by-letter text stacking and control clipping
when running in narrow tiling or compact displays:
- Top bar switches to compact tab padding and hides long subtitle
- Home actions and subtabs adaptively scroll and wrap without overflowing
- Mods and Versions headers stack title and filters vertically
- Settings navigates via top scrollable pill bar on narrow displays
- About tiles and Account cards stack cleanly
- Setting rows wrap controls instead of shrinking labels to 0 width
Upstream's UUID bridge (#10304) now writes a mob anger target's UUID
through mob_to_nbt's uuid_of. The branch keeps its two additions on top:
a target read from the save that is not in the world keeps the loaded
UUID, and the loaded angry_at goes once the anger is over.
A mob only gets its UUID when it is first saved, so an anger target
not saved yet came out with no angry_at (wolves too). mob_tree_nbt now
gives a target that is still in the world its UUID first; the new
anger_at_a_mob_not_saved_yet_names_it test failed without that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Mesa's hasvk Vulkan driver for Intel Haswell (Gen 7.5, vendor 0x8086) reports conformance version 0.0.0.0 and suffers from severe vertex buffer corruption and polygon tearing across 3D terrain (#9912).
- Check is_broken_haswell_vulkan in open_with_retry to skip non-conformant hasvk on Haswell and fall back cleanly to Mesa crocus OpenGL 4.6 Core Profile.
- Preserve Ivy Bridge (IVB) on Vulkan (#4059).
Horizon's update status (Up to date, Install) and the launcher's version with
Check now / Update sat under About, while the Updates section held only the
two auto-update switches. They now open Updates, in both looks: the Glass
tiles above the switches, the classic rows at the top of its Updates group.
About keeps the data folder (now a row like Logs), logs and the applications
menu.
Fixes#11035
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The integrated server matched the used item by id (minecraft:totem_of_undying)
and used it up before the client looked for it, so any other item with
death_protection showed a plain totem in the activation animation.
Vanilla LivingEntity.checkTotemDeathProtection takes the first hand whose
stack has DataComponents.DEATH_PROTECTION, keeps a copy before shrink(1), and
uses that copy for the stat, the used_totem trigger and the animation.
Part of #10587
ThrowableProjectile.tick runs on the client too: handleFirstTickBubbleColumn
before gravity, then applyEffectsFromBlocks' BubbleColumnBlock.entityInside
after the move, with Projectile's unclamped pushes (+0.06 inside, +0.1 at the
surface, -0.03 dragging down). The client's remote_projectile_tick had none of
it, so a pearl held in a stasis chamber on a server sank between the tracker's
updates and snapped back on each one.
Projectile::tick_remote adds both for ThrowableProjectiles, through a new
ProjectileWorld::bubble_column hook (bubble_column_at for the game's world).
The integrated and dedicated servers keep pushing their pearls in the
creature tick, so their projectiles are not pushed twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Their target goals call setUnseenMemoryTicks(300): the evoker's player
and villager goals, the illusioner's player, villager and iron golem
goals. Pillagers and vindicators keep the default 60.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shelf mushroom is a solid shelf players stand and bounce on (ShelfMushroomBlock's shape); the brown
mushroom it stood in as has no collision, so a 26.2 client would fall through what the server stands on and
be pulled back. Its block now shows as brown_mushroom_block, whose collision holds the shelf's; the item stays
brown_mushroom. The new explorer maps are filled maps (MapItem), so they show as filled_map, which draws their
map_id, not as an empty map. v26_2_ids.rs regenerated with --substitutes. TooltipDisplay's writer no longer
allocates a list on every write.
Review follow-up for #10557 (Detimes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: tooling only. When more than 20 harnesses newly fail at once, the audit now opens one issue instead of 421, and the open-issue lookup no longer stops at 200.
🤖 Agent: tooling only. ensure_work_dir now returns the folder argparse picks, so the --work-dir=PATH spelling no longer crashes four harnesses on sys.argv.index.
Four harnesses looked the directory up with sys.argv.index('--work-dir'), which raises
ValueError for the --work-dir=PATH spelling this PR keeps. ensure_work_dir now returns the
directory argparse will pick (the last one given) and they use that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 2026-11-06 audit tested a main commit that did not compile (mob_to_nbt arity in raids.rs) and opened
421 "Parity regression" issues, one per harness. Above 20 new failures the audit now opens one issue with
the list and the first compiler error, and keeps those harnesses' previous results so the next audit
compares them again. The open-issue lookup no longer stops at 200.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One tick of a server player's own movement from a bot's input, played on a
copy of the body with the movement prediction's replay code, and the block
collision boxes in a box (Level.getBlockCollisions). Read-only; nothing runs
unless a mod calls them. Helper fragment player_bots, docs, unit tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Game helper (Host::refresh_commands for trait mods): at the start of the
next server tick every mod's native_register_commands is asked again,
LanHost::native_commands and the hub's mod tree are rebuilt, and every
placed player is sent their tree again after native_player_commands_sent,
as PlayerList.sendPlayerPermissionLevel / Commands.sendCommands after
/reload. Hub::store_mod_commands keeps the new tree without sending it, so
each player gets one filtered tree. Servers whose mods never call it are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`tooltip_render_pre` (#10302) is labelled `RenderTooltipEvent.Pre`, so
`horizon-port-pack events` counts the 10 ATM10 mods that listen to it.
The "Next hooks" table in docs/native-hooks-neoforge.md listed
`player_tick_pre` and the client tick pair, which have now landed: the
section lists them with the rest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Five command-oracle harnesses looked for a bare `--work-dir` and appended
their default anyway when the caller wrote `--work-dir=PATH`. argparse takes
the last value, so the caller's directory was silently replaced and the run
wrote its scratch output somewhere they did not ask for. They now share the
guard test_boat_leasher_parity.py already used, and tests/test_harness_args.py
pins both spellings.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
ServerPlayer.swingAndResetAttackStrength: the guest's block, item, leash
knot and entity uses reset the attack ticker when their swing starts.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- FishingRodItem.use posts cast and retrieval vibrations using the stack's use effects.
- EnderEyeItem.use posts a projectile-shoot vibration.
Part of #9776.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShowTradesToPlayer sets the main hand's drop chance to 0 when it shows
an offer (displayAsHeldItem) and back to 0.085 when it clears the hand
(clearHeldItem).
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add the native hook `entity_interact_specific` (NeoForge's `PlayerInteractEvent.EntityInteractSpecific`): a right-click on an entity hears the point clicked, measured from the entity's position, before `native_interact_entity`; a claim swings the hand and ends the click. Server-side.
Co-Authored-By: DeepSeek Pro via OpenRouter <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The install side of the Launch API, experimental: what a build loads (the
new `content`), the folder and the on/off file of each kind under
--game-dir, when a change applies, native mods needing a rebuild, what to do
with a .hpack, and the checks before installing as mod-index.md specifies
them (where it comes from, size and SHA-256, the minisign key, revoked[] and
installed copies, the manifest). Listed in docs/api/README.md. Part of #10562.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Piglin.holdInOffHand equips the item with setItemSlotAndDropWhenKilled,
a guaranteed drop even without a player's kill, and makes the piglin
persistent unless the item is a gold ingot.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.push(Entity) does nothing while the entity sleeps, and
doPush reaches the other entity through it. The mob snapshot carries
sleeping (a villager in bed, Fox.isSleeping) and push_entities skips the
push. The sleeper's own tick still pushes the others.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`content` in horizon-client's reply, from the constants the build checks
content against: content mods' manifest api lines (APIS without the removed
WebAssembly 0.1), native mods' hooks level and that they need a rebuild, the
shader schema and the line it draws, the resource and data pack formats, the
modpack format and the index schema. A launcher offers only what works. The
generated reference shows the hooks level as a placeholder, since it rises
with every hook. Part of #10562.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the pre-tick hook swapped each guest's whole state into Game every tick even with no mod loaded; I pushed a guard so `as_guest` runs only when `mod_api::mods()` isn't empty. The fragment passes tests.test_stamp_native_hooks.
🤖 Agent: verify failed because bake_bricolage.py imports fontTools before it reads its arguments, and CI has no fontTools. I pushed a change that checks the arguments first, so the new usage test passes without fontTools installed.
🤖 Agent: #10398 asked that a local build in a folder with a space still builds, so I pushed a fallback for GNU windres when the volume has no 8.3 names: outside CI it warns and builds without the icon instead of failing. With the taskbar-icon commit, `cargo check --target x86_64-pc-windows-gnu` passes on the desktop for horizon-client and horizon-client-input.
🤖 Agent: a failed region already restores its originals, so blocking the server start on any prune error took a server down for one corrupt chunk record. I pushed a change that starts it when no journal is left, with a supervisor test for it. Note that delete mode now writes regions one at a time with fsync, so a large prune takes longer than the old parallel rewrite. 169 horizon-server tests pass on Linux.
🤖 Agent: raids.rs now passes the same `persist.uuid_of` lookup persist.rs gives `mob_to_nbt`, and the four entity_nbt.rs test calls get `|_| None`, so main builds again after #10304 (verify green on this head).
🤖 Agent: translations only; %s placeholders kept and every file parses. The languages you flagged as uncertain (ba_ru, cv_cu, sah_sah...) stay worth a native speaker's look.
🤖 Agent: removes the old pack pages, their two slider kinds and four lang keys in all 87 files; nothing else read option_label_within, checked with clippy -D warnings on the stack over current main.
🤖 Agent: The non-constant texel offset becomes offset / textureSize(level 0) added to the coordinate: right for the base level, but at a sampled lower mip the shift is in base-level texels, not that level's (GL moves by texels of the level read). Fine for Arc's FXAA at level 0; worth a note if a pack hits it with textureLodOffset.
🤖 Agent: CAT_* macros come from biomes::CATEGORIES, so they pick up #10499's CAT_MOUNTAIN 17 and CAT_UNDERGROUND 18 with no second table to keep in sync; your test comparing every macro with constants() covers that.
🤖 Agent: The constant evaluator now refuses a non-finite F32 fold with Overflow, and only GLSL function bodies turn that into a run-time expression, so WGSL input still rejects 1.0/0.0 at compile time. Vendored naga's own 133 lib tests pass with the chain.
🤖 Agent: The integer-sampler helpers texelFetch the texel under the point with the coordinate clamped to the edge, which is what GL's nearest read of an integer texture gives; a bias argument is accepted and ignored, as GL does for these. Stack 3/6, checked with the rest of the chain on current main (clippy -D warnings, horizon-shaders and render tests).
LivingEntity.playBlockFallSound: the block 0.2 below the feet, at half
its volume and three quarters of its pitch, not for a silent mob or air.
Most mobs play it after their own fall sound, horses and llamas after
the damage, and a llama's damaging fall under six blocks plays it alone.
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
as_guest swaps a player's whole state into Game: running it for every
player every tick with no mod to call cost the server for nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every frame each text display built a batch, emitted its styled glyphs and
transformed every vertex, and a display of width or height 0 (what plugins
summon for holograms) is noCulling, kept to the distance rule because its
transformation can put the text anywhere: 200 of them cost ~0.3 ms a frame
with none on screen (#4664).
Each display's local-space text quads are now kept, rebuilt when the text,
its style, opacity, background, shadow, see-through, the font epoch or the
GUI atlas revision change (obfuscated text every frame), and only
transformed per frame. A noCulling display is skipped when the box of its
quads, through this frame's full matrix, misses the frustum: it can't draw a
pixel. Displays with a size keep vanilla's box test; the view range stays.
#10398 asked that a local build in a folder with a space still builds: a
volume without 8.3 names gives windres no usable path, so outside CI
such a build warns and goes on without the icon, as before. CI and
every other case still fail without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Peaceful, vanilla never lets a player be a mob's target
(`LivingEntity.canAttack` refuses a player there), so a mob that already
held one gives it up. A neutral mob also stops being angry and forgets
who hurt it (`NeutralMob.stopBeingAngry` calls `setLastHurtByMob(null)`).
Horizon dropped a player target only for creative and spectator players,
so an iron golem that had been angered kept attacking after the
difficulty was set to Peaceful.
Fixes#9759.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
Co-Authored-By: Codex gpt-6.1-terra <[email protected]>
CI has no fontTools: the import failed before main() could print the
usage, so the new test saw a traceback (verify failed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the entity position for the special ambient caller and cover adult and baby natural events through the real Mob tick and recording route.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
parity-main: natural_spawn first fails at f6a9444de (#10416), of 182 merges
since 66f3981f5. The spawn start heights it produces differ from vanilla 26.3
by up to tens of blocks (chunk/10 monster: vanilla y 99, Horizon 62), so
players would see mobs spawn at the wrong heights. Reverted until the change
passes the natural_spawn harness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10304 gave mob_to_nbt a fifth argument after the raid observation's call to it
was written, and the two merged without a conflict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
names.rs: #10328 landed on main; its NameTagLayouts frame now wraps this
PR's frustum-culled collect_name_tags, both test sets kept.
scoreboard.rs: #10098 added the hide_gui argument to tab_list::visible;
the early visibility check here passes !self.ui.hide_gui too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Team review (kacpherek) on #10382: groups filled in arrival order
under a 32 cap hid a late UseItemOn burst behind other players' moves;
count > 1 dropped 69 players sending one 6-9 ms use each; and one slow
event hid the rest of its group. Groups are now one per packet kind (a
bounded set) with the time of their short events only (the slow ones
are noted on their own), and each names the player who spent the most
of it: "213 x UseItemOn under 10 ms each; most from Steve: 140 of
them, 700.0 ms".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read Git's NUL-delimited UTF-8 output without newline conversion, and
include deleted source paths when files move between check scopes.
Keep pr_ready's line accounting consistent with parity selection.
Fixes#10535
Co-Authored-By: Codex (GPT-6) <[email protected]>
The crossbow hit, trident hit and trident hit_ground sounds come from the same
Entity.playSound as the arrow hit (AbstractArrow.onHitBlock / onHitEntity,
ThrownTrident.onHitEntity), so they take getSoundSource(), NEUTRAL, too; the
name-based guess put them on PLAYERS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A failed region restores its originals before the error comes back, so
the world is whole: blocking the start then took a server down for a
corrupt chunk record. Only a recovery that cannot finish stops it now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DragonSittingScanningPhase.doServerTick computes Math.acos(dot) * 180.0F / (float) Math.PI
and Mth.atan2(..) * 180.0F / (float) Math.PI in double, not a multiply by the float
RAD_TO_DEG constant. Also moves the Silent test doc comment back onto its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Loading a shaders/ pack made its pipelines on the main thread, and the driver
compiles each program there: the game froze for 5 to 50 s on a big pack
(Complementary Reimagined, Photon), with no frame drawn.
The renderer now builds the pack on a "Shader pack" thread (load_shader_pack)
with its own handles to the device and the inputs, and the game keeps drawing
without a pack meanwhile; each frame poll_shader_pack checks the thread and
runs the pack once it is built, logging its programs and notes as before. A
pack the thread refuses or panics on is turned off as before. Picking another
pack, or none, drops one that is still building.
A boat's or minecart's box query (boat_contacts, boat_obstacles,
cart_entity_collisions) asked every mob of the level for its box, and
Mob::bounds reads the mob's whole size (dims: sleeping, baby, species, scale)
across several cache lines. While boats and carts tick, the widest half width
and tallest height of the mobs (BoxReach) are worked out once, and a mob whose
position is farther than that from the query is skipped without its box. The
reach is worked out again when a mob joins or leaves, and nothing a vehicle
does resizes a mob, so the same mobs are found.
What a Horizon server sends a 26.2 client (accept_older_clients) now carries 26.2's
registry ids: tags, block states, entity, block entity, block, attribute, sound and
particle ids, and items and components through the existing Way::Down writers, which
now leave out components the client lacks. A creative slot or container click from the
client is read with its ids up.
generate_registry_id_adapter.py --substitutes writes, from both versions' data
reports, a stand-in for each 26.3 entry 26.2 lacks (poplar as birch, wool and concrete
slabs and stairs as stone ones of the same shape, ...); what has none (the cushion
entity) isn't sent. v26_2_ids.rs is regenerated with it.
Part of #10404.
🤖 Agent: a 26.2 client is now refused in configuration before any untranslated registry data, tags or play packets (bebf670); 26.3 path byte-for-byte unchanged with the option off
A proxy that balances players between servers has to know, before it sends
one, how many players a server has and whether a login would be refused as
full. transfer-status answers with counts only: the players placed, held and
expected, the hand-overs under way, the places the player limit counts, the
limit, and whether the server is full. It names no player, changes nothing
and writes nothing to the log, since a proxy asks it often. Like the player-*
requests, it is refused while [transfer] is off.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both tools above used to crash, or write into a flag-named path, instead of
asking for what they need. These tests run them in an empty folder and check
that they exit non-zero with a usage line and write nothing.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
Both arguments were read unguarded, so running it with none, or with one, was an
IndexError rather than the command its docstring shows. Print the usage line.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
The script read sys.argv[1] straight away, so a missing argument ended in an
IndexError traceback, and a mistyped flag created a directory named --help and
filled it with 66 MB of fixtures. Print the usage instead: a flag is not a path.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
GameRenderer.renderLevel gives LevelRenderer.render shouldRenderSky false
while a boss bar makes world fog (the Ender Dragon's), so the frame is only
cleared to the fog colour. Horizon drew the End's sky box behind the island
during the fight. fog::sky_pass_skipped now holds vanilla's conditions:
LevelRenderer.addSkyPass's (lava, powder snow, blindness or darkness) and
the boss fog.
Part of #10491
Co-Authored-By: Claude Opus 5.5 <[email protected]>
position_riders finds every vehicle's riders in one pass over the mobs instead
of one per boat, minecart and primed TNT. A boat tests a mob's box before
anything else, and works out the scoreboard teams only when something touches
it, instead of once per boat a tick.
HORIZON_TRACE=FILE, horizon-ctl trace FILE [SECONDS] and horizon-trace on a
dedicated server's console write each span of every thread as a Chrome trace
event, so what ran beside what, and what a thread waited for, can be read off
in ui.perfetto.dev. A thread that records spans but is never taken from (a far
terrain or lighting worker while spans are on) now drops them past 16k instead
of growing for as long as spans stay on.
Load and save Evoker and Illusioner SpellTicks using the counter already consumed by casting goals and server AI. Cover the real codec and actual Game tick path.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: is_prepared now answers from a 5 s per-version cache (21.4 µs to 0.21 µs per call); need_prepared and the CLI still run the full per-file check, so an empty JSON is caught before launch.
Only packet ids are translated for an older client so far (#10404): the
registry data, tags (registry ids) and play packets (chunks, light, login,
player_action numbering...) would reach a real 26.2 client in 26.3 ids and
layouts. With accept_older_clients on, such a client now logs in and is
told so in configuration, right after the known packs and before any of
them. NetConfig::older_clients_play (tests only, not in server.toml) keeps
the play path reachable for the id renumbering test until the later parts
translate the contents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10389 made is_prepared parse research-lock.json and the manifest, list the
prepared folder and stat every table on each call. Pages call it while drawing
(instance rows, the Play menu, can_start), so every frame did file I/O.
is_prepared now keeps its answer per version folder for 5 s and drops it when
the launcher installs, removes, reuses resources or a job exits.
check_prepared runs the full check and refreshes the answer; starting the game
or a server (need_prepared), the CLI and the post-prepare auto-play use it, so
an empty JSON is still caught before a launch.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: Entity.checkFallDamage subtracts (float)ya into the double, which the f64::from(.. as f32) step reproduces, and Entity.load reads fall_distance back. Main merged in again after #10422 landed (GitHub read the criss-cross bases as a conflict); the diff to main is this PR's own 6 files.
🤖 Agent: `camera_list` is now collected into a Vec on every frame, even with no pack, where the old iterator was lazy; it's one small allocation per frame. The shadow-only set and the spawner's `shadow_pass_drew` scan (linear in shadow_visible per spawner) run only under a pack whose shadow draws entities.
`biome_category` read 0 (CAT_NONE) in lush caves and the other cave biomes,
where Iris 1.11.7 reads 5 (CAT_PLAINS), and differed in 12 more vanilla
biomes. A probe pack of ours read Iris's value in each of vanilla 26.3's 67
biomes (`fillbiome` around the camera): they fit vanilla's `is_*` biome tags
(BiomeTags; `is_savanna` unused), then desert, swamps, mushroom fields and
snowy plains by name, NONE for the void, and PLAINS for the rest. Mountain
biomes read 17, which Iris names CAT_MOUNTAIN; CAT_UNDERGROUND is 18.
Biomes vanilla doesn't have keep the old guess from words of their name.
Fixes#10453
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack (or the dimension folder it uses) with no gbuffers program for terrain
had the game draw terrain itself, into colortex0 and depth. Iris 1.11.7 draws
none: with probe packs of ours having no gbuffers programs, at the root or in
world-1 below a root that has them, colortex0 held only the sky (or fog
colour), entities and particles, depthtex0 had no terrain, and colortex1 to 7
none either; water and lava were missing too.
When no program of gbuffers_terrain's chain (nor _solid or _cutout) is in the
folder, the pack takes solid and cutout terrain from the game and draws
nothing; the same for translucent terrain and gbuffers_water's chain. A
program that is there but fails to load still leaves terrain to the game.
Fixes#10455
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the existing species maximum after shared Mob tick work, preserving the normal underwater countdown. Cover the real full-tick path and actual Game commands against fresh Java.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Mob.addAdditionalSaveData always writes canPickUpLoot(), so moving it out of the put-if-absent defaults is the fix (Allay and sulfur cube overrides now win over a loaded byte). Main merged in after #10393 landed; clippy and the entity_nbt tests pass on the merge.
🤖 Agent: piglin_replace follows Piglin.canReplaceCurrentItem in order (PREVENT_ARMOR_CHANGE first, then wanted/unwanted, then Mob.canReplaceCurrentItem), with getPreferredWeaponType null for babies.
🤖 Agent: `is_prepared` now reads the manifest and stats every top-level JSON on each call, and the launcher calls it from render paths (glass_home, glass_palette, can_start): a few hundred µs per frame at most, but a cache keyed on the folder would remove it. I dropped the unused `is_json_file`; launcher clippy and backend tests pass.
🤖 Agent: checked on Linux with mingw: the build script now compiles `main_res.o` for x86_64-pc-windows-gnu and passes it to the link, which the host-gated `#[cfg(windows)]` skipped. I kept the warn-only fallback outside CI for windres paths with a space (#10398).
🤖 Agent: causeExtraKnockback's non-living branch is Entity.push (no knockback resistance, no halving), as strike_boat does; the guest path still checks the attack range before skipping look_at_entity. We merged #10434 into this branch: both extend the boat fixture and harness.
🤖 Agent: for one-digit mantissas {:.1e} picks the closest two-digit decimal, which is what Java's Float/Double.toString does (Float.MIN_VALUE prints 1.4E-45). We switched the harness to the shared release world_probe: debug at CARGO_BUILD_JOBS=1 would have compiled horizon-world again in CI.
🤖 Agent: RealmsPdpScreen shows the trial description (and a Try for free button) only when trialAvailable; Horizon does not ask for it, so we dropped the description and kept the title with Buy, vanilla's no-trial layout. Titles are drawn plain where vanilla underlines/bolds them: small follow-up.
🤖 Agent: start() matches NeutralMob.stopBeingAngry plus IronGolem.startPersistentAngerTimer (nextInt(381) + 400 = rangeOfSeconds(20, 39)) at target priority 4. We moved the creeper test's doc comment back onto its own test: the new test had been inserted between them.
🤖 Agent: the tier fetch and both PUTs hit the same paths as RealmsClient.getRealmTierConfiguration/updateRenderDistance/updateSimulationDistance, and a reply missing either range is an error as in vanilla; nothing beyond what the vanilla settings tab offers.
🤖 Agent: only Tags move to the new id, as ConversionType.convertCommon copies them; the old raw compound (UUID, species data) is left behind, which the save/reload test checks.
🤖 Agent: hook_block_hit now runs on the client before moving, as FishingHook.checkCollision does on both sides, and gravity is f64::from(0.03f32) (getDefaultGravity is a float).
🤖 Agent: matches Pig/Strider.getControllingPassenger (saddled, first passenger a player holding the right stick in either hand); the server side reads the same rule through moved_by_client, so it no longer simulates the mount twice.
🤖 Agent: component_bool matches NbtOps.getBooleanValue (doubleValue != 0, so 0.5f and NaN are true, Short 256 too), not NumericTag.asBoolean's byteValue; that is the Hypixel glint_override report.
🤖 Agent: LivingEntity.aiStep resets fall distance for slow_falling or levitation before travel, NoAI included; dropping the later travelInAir-style reset is right, it erased the current tick's descent.
🤖 Agent: packet_utf8 differs from from_utf8_lossy only on ED A0..BF (encoded surrogates), which Java consumes as one replacement; the length limits still use the encoded bytes first and UTF-16 units after, as Utf8String.read does.
🤖 Agent: reading the bytes once and parsing them removes the exists()-then-load race too; only NotFound starts an empty list, so a directory or a locked ops.json now stops the stopped-server command instead of being overwritten.
🤖 Agent: fangs_allied follows Entity.isAlliedTo both ways (AbstractIllager tag only when both are teamless, Evoker's vex root-owner rule); guests on the evoker's team are still bitten, as before, which is a follow-up rather than a regression.
🤖 Agent: follows LivingEntity.onEquipItem (same item+components or firstTick: nothing; EQUIPPABLE on the new stack picks equip over unequip) and data merge keeps first_tick. We removed the docs/parity run-log file.
🤖 Agent: all nine families are Monster subclasses (entity.hostile.* is Monster's shared hurt/death/fall set), so HOSTILE is right; the list scan is 34 short strings per sound, negligible. We removed the docs/parity run-log file.
🤖 Agent: DataComponents.DAMAGE goes through Codec.INT (Number.intValue: truncation, Long keeps its low 32 bits), and dynamic::as_int already does that. We removed the docs/parity run-log file.
🤖 Agent: LootCommand.dropFishingLoot's no-tool endpoint passes ItemStack.EMPTY, and the code only reads tool_stack when a tool, mainhand or offhand was given, so an omitted tool stays empty. We removed the docs/parity run-log file.
🤖 Agent: Pig BABY_DIMENSIONS has PASSENGER (0, 0.5, 0), so the one match arm is right. We removed docs/parity/baby-pig-passenger-attachment.md: run logs and hashes belong in the PR description, not in docs/.
head_skin decoded the textures property (base64, JSON, URL checks) and hashed
the skin location (SHA-1) for every head on every frame, before asking the
atlas whether the skin was resident; an armor stand's worn head also turned
its profile NBT into JSON each frame. With 60 stands wearing textured heads
and 36 heads in the inventory that was ~9 us a head a frame (#4664: a server
full of menu heads, CPU-bound with the inventory open).
The pure part (profile -> textures -> skin location, failures included) is
now kept in a bounded cache beside the resolver; worn heads reuse the JSON of
an unchanged profile. The download state, atlas residency, drawn tracking and
eviction re-uploads still run per draw.
`Game::shoot_crossbow` set the arrow's weapon and Piercing count but never
ran the weapon's `projectile_spawned` ignite, which only the bow path
applied. In vanilla, `AbstractArrow`'s constructor runs
`EnchantmentHelper.onProjectileSpawned` for every arrow built with a
weapon, bow or crossbow, so a Flame crossbow shoots burning arrows that
prime TNT and set entities on fire.
The bow's `bow_projectile_ignite` becomes `ignite_spawned_arrow(weapon,
arrow)`, shared by both weapons; the crossbow ignites each arrow of its
volley after Piercing, as the constructor does. The port map rows follow
the rename, and a resource-backed regression checks no Flame, Flame and
Flame with Multishot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
🤖 Agent: confirmed in Blocks.<clinit>: water, lava and bubble_column are the only three blocks with .liquid(), so the name list is complete for Block.isPossibleToRespawnInThis.
🤖 Agent: `tick_blocks` starts a fresh `laps` after the scheduled ticks, so the stretch between `tb_entity_inside` and `tb_broadcast` shows only through `random_ticks`, `tb_scheduled` and `tb_fluids`. The counters and laps cost one relaxed load each when spans are off.
🤖 Agent: HappyGhast.getHurtSound/getDeathSound pick GHASTLING_* by isBaby, and happy_ghast::sounds already does exactly that, so the new voice_sounds arm is the whole fix.
🤖 Agent: checked against 26.3: `ChunkAccess.getHeight` returns `getFirstAvailable - 1` and `getRandomPosWithin` adds the 1 back, so topEmptyY is Horizon's `world_surface` itself. The old +1 made the bound 130 for a column solid to y 63. run-parity's natural_spawn harness checks the draws end to end.
🤖 Agent: rowing_time now gates on paddle_controller like AbstractBoat.getPaddleState (flag AND a controlling passenger) and clamps a as Mth.clampedLerp does; #10386 carries this branch merged, queued after it.
🤖 Agent: the `[false; 256]` table relies on a local palette never holding more than 256 entries (global palettes keep the per-block path). The test covers 1, 2, 256 and 300 states, a stale palette entry, and the LCG draws against an independent oracle.
🤖 Agent: matches Entity.saveWithoutId: the orb's command NBT now reads its registered uuid_for(id) instead of a fresh random UUID per read; queued first of the 10393/10422/10458 stack (the other two carry this branch merged).
🤖 Agent: `native_bake_layer` re-reads entity-models.json on every call for a vanilla layer no mob has (the doc says so), so a mod that bakes per frame would hitch; caching that document is the natural next step. The humanoid-against-zombie#main bake test is the right oracle, and the fragment stamps cleanly (tests.test_stamp_native_hooks).
🤖 Agent: the early return for an unchanged size stays ahead of the new `device.poll(wait)`, so the GPU sync happens once per real size change. A drag-resize pays it per new size, a fair price for not holding both sets of buffers.
🤖 Agent: `world_seen` never resets inside a `Custom`, so the restart on entering the Nether relies on the pack being rebuilt for each dimension. If `PackPost` is ever kept across dimensions, it will need resetting there.
🤖 Agent: only `Source::Color` gets the linear-mip sampler; shadowcolor, custom textures and the lightmap keep `Linear` (nearest level), which matches what was measured. Buffers without mipmaps read the same as before.
🤖 Agent: the 256ths weights are a fit to one NVIDIA GPU but now apply on every GPU, so an AMD or Intel reading of Iris may move them later. The cost is two floors and a multiply per filtered shadow read.
🤖 Agent: the spectator rule keys on `pack_ids.is_some()`, so a native pack's shadow keeps the first-person model, as the test pins. The other game modes stay as before, since only spectator was measured.
🤖 Agent: the +1/32 bias moves only places within one float step under a 32768th line, so #9839's water tops (29094.34) and 13/15 offsets (28398.93) still floor as before, and the new test case pins the 1, 0.5 and 9.5 edges.
🤖 Agent: with the near and far planes dropped, the shadow pass now draws every section inside the box's sides out to its reach, so a pack with tight planes draws more shadow terrain than before. Worth watching in the cave perf numbers. #10471 conflicts with this in gfx.rs; I'll merge main into it once this lands.
Both extend the boat authority fixture and harness: keep the paddle and
attack cases, both Case fields and both docstring paragraphs. The harness
keeps main's fast-profile build with #10434's --resources option.
Queued after #10434, so either landing first leaves the other mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
All three add a command_exec fixture group at the same lines and a test
module at the end of entity_nbt.rs; keep every group, flag, dispatch block
and module. Queued in order so whichever lands first leaves the others
mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both add a command_exec fixture group at the same lines; keep both groups,
their isolation sets, flags and dispatch blocks. Queued in order so either
landing first leaves the other mergeable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A debug world_probe at one job compiles horizon-world from scratch beside
the release probe every other horizon-world harness uses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon does not ask trialAvailabilityTask. Vanilla shows the trial
description (and offers the trial in the footer) only when one is
available; otherwise the header is the title alone and the footer offers
Buy, which is what this screen shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The GNU windres the launcher's native mod builds use can't take a path with a
space, so a local build there must go on without the icon (#10398 asked to keep
it); CI still fails without it. One Windows block for the stack and the icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_player_tick_pre() runs once a tick for every living player in the
world, at the top of their ServerPlayer.tick, before their digging, item
use and cooldowns. native_player_tick (API 31) already ends the same tick.
It fires in tick_guests after the checks that skip a dead player and one on
a server trip, so the pair brackets the same players. Single player's own
player is a guest of its internal server.
The fragment player_tick_pre.json labels it PlayerTickEvent.Pre (side
server, not cancellable) for horizon-port-pack events. 23 of ATM10's mods
listen to it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SDL rows match a firmware version exactly, and gilrs' copy has no row for
045e:0b13 at 0x0520. The pad then falls back to gilrs' default mapping: its
right stick is on ABS_Z/ABS_RZ, which nothing read, its triggers on
ABS_GAS/ABS_BRAKE, which that mapping doesn't name, and X and Y stay crossed
because it lists C and Z.
Recognise that layout from what the pad reports, so a new firmware works
without a row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Terrain's `mc_Entity.y` was -1 on every block but water and lava, and -1 on
those too when the pack has no block.properties. Iris 1.11.7, with probe packs
of ours, reads 1 on water and lava and 0 on every other block, whether
block.properties maps it, leaves it out, or the pack has no such file.
The sprite layers' table now starts each layer at (-1, 0), with y 1 on the
fluid sprites, before block.properties is read.
Fixes#10452
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's terrain gl_Vertex floors each vertex's place in its
section to a 32768th. Plants whose random offset is 9/15 of a block came
out a 32768th under a half block, where our probe packs read Iris
1.11.7's exactly on it: we guess, untested, that our float sums put them
a hair under the line. A place up to 1/32 of a 32768th under a grid line
is now on it.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack's texture() on shadowtex0/1 through a plain sampler blended the
four texels with the exact fraction. Iris 1.11.7's filtered reads on our
NVIDIA GPU fit weights in whole 256ths: the fraction rounded half up on
each axis, the far corner's weight their product over 256 rounded half
up, the others the rest of each axis. #8464's lit probe then differs
from Iris's at no pixel of ow_ground_noon.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's shadow pass drew the block entities the camera's view
had built: a chest beside the view, in a section whose terrain the
shadow pass draws, was left out, and a spawner's mob out of the view
and out of those sections went in. Our probe packs' Iris 1.11.7 shadow
maps hold a chest wherever the shadow pass draws its section's terrain
and nothing of the one spawner in our scenes, which lies outside them.
The block entities of the sections the shadow pass drew last frame are
now added hidden from the camera, and a spawner's mob that is out of
the view and out of those sections is left out.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In first person, a shader pack's shadow pass drew the spectator's own
model (its head) at the camera. Our probe packs' readings of Iris
1.11.7's shadow map in spectator mode hold the terrain below the camera
there.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
37 of ATM10's mods listen to the parent ModConfigEvent, which hears the
three events NeoForge's config page documents under it: Loading, Reloading
and Unloading. config_loading, config_reloading and config_unloading answer
them, so the parent now counts as hooked rather than as another phase's:
93 of 326 names, up from 92.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server.toml option, [server] accept_older_clients (horizon-accept-older-clients), off
by default. On, a login handshake stating 776 opens an adapter::ClientSession on the
connection instead of being refused, and a 26.2 status ping is told its own protocol.
The session follows the connection's states as Session does, mirrored, and renumbers
packet ids both ways with the inverse of the generated 26.2 tables, in the blocking
login path and in the io_pool play path. Off, nothing changes.
Part of #10404.
Adopt and project the existing fall counter, including Java's per-movement float narrowing before double accumulation. Preserve travel and landing-call order, and cover the real tick/save route plus decoded saves without raw data.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: staged files now get the current time, so a Nightly's source unpacked with its commit's older mtimes can no longer pass for already-compiled crates (Windows/macOS fs::copy kept them); one-time stage rewrite for old stages.
A custom uniform's smooth() restarted on every frame whose camera biome was
unknown, so after each teleport it started again, and it started from the
unknown frame's values (biome -1, biome_category 0, temperature 0): the
frame that restarted it also stored them, and the first frame that knew the
biome went on from there.
Now smooth() restarts only until the first frame that knows the camera's
biome, that frame included, and then keeps going for as long as the pack
runs. With a probe pack of ours, Iris 1.11.7 kept smoothing across
teleports within a dimension and started again when the player entered
the Nether.
Fixes#10450
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shader pack's colortex read between two mipmap levels (textureLod at a
fractional level) took the nearest level: the buffers' sampler had the
default, nearest, mipmap filter. In Iris 1.11.7 our probe packs read every
colortex, in composite and in final, as the two nearest levels mixed. The
buffers now get a sampler of their own that blends them; shadowcolor and
the other textures keep theirs (not measured).
Fixes#10451
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's documentation describes one event under two names. 1.21.1's
"Registering Additional Models" (ModelEvent.RegisterAdditional, one
ModelResourceLocation per model) is "Registering Standalone Models"
(ModelEvent.RegisterStandalone, a key and a baker per model) from 1.21.5 on.
Both versions use the same sentence, and the example method is still
registerAdditional. standalone_models (API 151) answers it, so the 47 ATM10
mods that listen to it now count as hooked: 92 of 326 names, up from 91.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Nightly's source keeps its commit's mtimes when unpacked, and fs::copy
keeps them on Windows and macOS, so a changed engine file reached
native/stage/ older than the previous build and Cargo reused the old
horizon-core. sync now sets each written file's mtime to now; a stage
written before this is rebuilt once.
Fixes#10421
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The screen behind Add Realm is 26.3's `RealmsPdpScreen`: the header's
logo, the trial header, then the friends, minigames and private-server
entries with their 80x45 pictures and 182-wide text, scrolling between
the header and the footer, whose buttons are the PDP's own title (the
buy link) and Back.
Its old strings were dead: `mco.title` never existed, and 26.3's
deprecated.json removes `mco.selectServer.popup` and
`mco.selectServer.buy`, so the screen drew raw keys.
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: options.txt is vanilla's key list again; the emote key is saved as emoteKey in horizon-extras.txt and an old key_key.emote line is migrated, options parity passed on the desktop.
Our probe pack's Iris 1.11.7 shadow maps hold the terrain at every depth we
put it along the light (from 535 blocks beyond the shadow camera to about 120
before it), whatever shadowNearPlane and shadowFarPlane the pack sets, or
none. Horizon dropped the sections outside the shadow projection's near and
far planes; since #9768 those are the pack's own planes with no offset, so
in our probe's cave scene with the planes at 0.05 and 256 the surface toward
the light was missing from the shadow map. A shaders/ pack's shadow pass now
culls by the box's sides only; Horizon's own packs keep their planes.
Fixes#10403
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A NeoForge mod builds its entity models in code (createBodyLayer: a
MeshDefinition of CubeListBuilder boxes at PartPoses, a LayerDefinition),
registers them in EntityRenderersEvent.RegisterLayerDefinitions, bakes them
with EntityModelSet.bakeLayer and draws the ModelPart tree it posed in
setupAnim with ModelPart.render. Native mods get the same:
- the builders and ModelPart, ported from vanilla (mobs/model_layers.rs):
box UV from texOffs, mirror, deformation, texture scale and visible faces
as ModelPart.Cube bakes them; poses, reset_pose, get_child;
- native_layer_definitions, a collect hook asked once at start, keyed by
ModelLayerLocation ids in the mod's own namespace;
- native_bake_layer: a registered layer, or vanilla's by its
entity-models.json name (minecraft:zombie#main);
- native_draw_model_part: ModelPart.render under a mob's or block entity's
frame, textured from an atlas sprite.
The humanoid mesh built with these builders bakes to exactly vanilla's
minecraft:zombie#main (an ignored test against the prepared resources).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the kept buffer is cleared at the end of every tick, so no villager Arc<villager::Seen> outlives the tick, and #[inline] on Mob::seen only changes where each Seen is written (in its slot instead of a stack copy): same values, same order. creature_prepare 1.2 → 0.8 ms in both A/B rounds.
Options parity regression from #10092: key.emote was in the vanilla key list, so options.txt
gained a key_key.emote line and every key after it shifted. It is now a Horizon-only mapping
(HORIZON_MAPPINGS): bound in Controls and translated like the others, saved as emoteKey in
horizon-extras.txt. A key_key.emote line written by the broken build is read once and dropped.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings the branch up to date and re-runs CI: verify failed once in
horizon-relay's slow_first_lines_and_too_many_connections_are_closed (a
timing test, 5.6 s against its 3 s on a loaded runner), which this
change doesn't touch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
🤖 Agent: a mob that did not tick still gets its last position refreshed (no step), so one moved by a command while out of range doesn't count a walk or a landing once it ticks again; damage and mount events stay for every mob since vanilla raises them from hurt/startRiding. Proven on vanilla 26.3 with ore_probe2.py (settled pig/item stop lighting redstone ore once the player is 6 chunks away).
Rebuilding a changed or new section's random-tick candidate mask asked
whether the block state ticks randomly for each of its 4,096 blocks. A local
palette holds a handful of states: each entry is now classified once and the
mask built from the decoded palette indices. Single-value and global
palettes keep the previous path. Same mask, same draws.
Write CanPickUpLoot on every save through the existing virtual getter, preserving the Allay and sulfur-cube overrides instead of retaining stale raw bytes. Cover decoded and mutated models plus frozen actual command reads.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
NaturalSpawner.getRandomPosWithin draws Y in [minY, getHeight(WORLD_SURFACE)
+ 1], and ChunkAccess.getHeight is getFirstAvailable - 1: the top is the
first empty block. Horizon's world_surface already returns that block and
the spawner still added one, so every start could land one block higher
than vanilla's, in the air above the surface, where vanilla's draw never
goes; nextInt's bound was one larger too, so the draws differed from
vanilla's from there on.
🤖 Agent: idle entities (outside the simulation-level entity-ticking range) no longer run sendChanges, as in vanilla's ChunkMap.tick, so their 60-tick MoveEntityPos stops; a bare state (no data) is never paired, and the idle check follows the tracker's viewers (placed or held). tick_lan −1.66 ms (−24 %) in 3 A/B pairs.
🤖 Agent: queued. Every tracked entity now records what it rides, so any passenger (item/block display included) hides the player's tag as vanilla's isVehicle does; one risk checked: a removed vehicle leaves a stale id in passengers' vehicle, harmless since tags look up live entities.
🤖 Agent: the gate is the set of mobs the loop actually ticked (Creatures::ticked), not a second ticking-area test, so a mob that walks out of range during its own tick still finishes its pickups as in vanilla; proven against a vanilla 26.3 server (pickup_probe.py: only the ticking villager and zombie take their item).
Vanilla's LivingEntityRenderer.shouldShowName hides a name tag for any
entity with a passenger (`!entity.isVehicle()`). The client only counted
mobs, text displays, armor stands and interactions as passengers, so a
server that hangs its own name over a player with an item display, a
block display or any other entity still got the player's tag drawn
under it: two names (#5666).
Every tracked entity now records the vehicle SetPassengers puts it on,
and the name tags read that, whatever the passenger's type. The typed
lists stay for placing the riders.
The rest of #5666 was fixed already on main: the sword drawn in the
off hand by #5111 (each remote hand renders its own stack), the double
tag of custom-named players by #8457, and text display and armor stand
riders by #9754.
Fixes#5666
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack with large buffers (SEUS PTGI at 3440x1440) held both sets at once while resizing and ran
out of video memory going back to fullscreen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A slow tick named spawn_natural, tick_blocks and tick_creature_entities but
nothing inside them. With 100 bots they take ~11, ~21 and ~10 ms a tick, and
an index for spawn_selection's nearest-player shortlist saved nothing: the
time was elsewhere. Their steps now have laps (sn_*, tb_*, tc_*), and the
natural-spawn pass counts its attempts and spawns (sa_attempts/sa_spawns,
one note a tick). They show the attempt loop itself: ~18,850 attempts a tick
for ~3 spawns (#3910). Only when HORIZON_SPIKES is set.
Steps, landings, splashes, flaps and stepping on redstone ore come from
an entity's own tick in vanilla (Entity.move, applyEffectsFromBlocks
from LivingEntity.aiStep and ItemEntity.tick), and ServerLevel.tick
ticks only the entities in entity-ticking chunks. Here every loaded mob
and dropped item on the ground stepped on the block under it each tick:
a pig or an item that had settled on redstone ore kept lighting it after
the player went six chunks away.
Those events are now for the mobs tick_creature_entities ticked (and
those standing in the ticking area, as the sculk fixture posts before
the mobs tick) and the items in the ticking area. A mob that did not
tick keeps where it is as its last position, without a step. Damage and
mounting are raised by hurt and startRiding, so they stay for every mob.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse the existing identity getter rather than generating a UUID per read. Cover repeated saved reads, absent raw input and stale raw UUIDs through the actual Game getter and command fixture.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
RealmsSettingsTab's two distance sliders, fed by
getRealmTierConfiguration (asked until it answers, as
tierConfigurationFetched does) and saved with updateRenderDistance /
updateSimulationDistance only when a value changed, alongside
saveSettings' existing name, description and region update.
Co-Authored-By: ZCode <[email protected]>
tick_creature_entities snapshots every loaded mob (`Seen`, ~400 bytes)
before the loop and again after each mob's tick. Mob::seen lives in the
client lib and was not inlined into the game crate, so each one was
built on the stack and copied into its slot, and the snapshot's
megabytes were allocated and freed every tick.
Mob::seen is #[inline] now, and the emptied buffer stays in Creatures
for the next tick. The snapshot holds the same values.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #9998.
Startup parsed entity-models.json (2.4 MB) into a serde_json::Value six
times: for the handoff's GameCpuResources, MobTypes, the player type, both
player arm models and the spin-attack model. The rest worker did five of
those one after another. Nothing in the document changes during a loader
generation, and a pack change starts a new one.
load_resources now parses it once on its own step beside load_core and
shares it as an Arc<EntityModelDocument> with every model loader. A
missing or malformed document leaves each caller on its old path, so
errors and fallbacks are unchanged; a dedicated server draws no models and
skips the parse.
Callgrind, the whole resource load in the game (to "resources loaded"):
21.576 G -> 20.631 G instructions (-4.38%); the entity-model path alone
1.594 G -> 0.349 G (-78.1%). A new test builds MobTypes from a shared
document and from its own parse and compares every type, model and layer.
A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
light_chunk filled its 48x48xheight region from every non-air section by
unpacking 4,096 state ids into a scratch array, then reading each one
back, looking up LightTables::info and storing 2 of its bytes. That round
trip was 35% of a light job, and the flood fill only ever reads those 2
bytes.
PalettedContainer::unpack_ids_mapped decodes the packed storage through a
lookup table in one pass (out[i] = map[id of entry i]), with a fast path
for 4 bits per entry, a linear palette's usual width. light.rs's new
states_of maps a section's palette to the packed light info once (at most
256 entries), then copies the region's 16-wide rows from the staged
values. A section whose palette emits light stages full infos and seeds
block light and the queue per cell, in the same order as before. Air now
writes info[0], which is 0, the value the region already holds (asserted
in debug builds).
Callgrind, gen_light_bench --seed 1234 --radius 4 --threads 1 (81 lit
columns): the light job 1.991 G -> 1.260 G instructions (-36.7%), the
whole run -1.15%. The light job is 5-7% of the workers' CPU, so this is
about 2% of the chunk pipeline.
Exact: gen_light_bench's block and light checksum is identical for three
seeds and centres at 1 and 4 threads, light_chunk_matches_old_algorithm
(an independent light implementation over random 3x3 regions) passes
unchanged, and the new section_states_match_the_unpacked_ids compares
states_of with a walk over unpacked state ids for one value, 4 to 8 bits
and the global palette, with and without light sources.
A first draft by DeepSeek V4.1 Flash (pi coding agent), reviewed by Claude.
Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
A killed exporter left an empty gameplay-facts.json (or another table) behind.
The preparer only checked that the path exists, so "prepare again" kept the
empty file, and the launcher called the folder prepared and started the game
on it. Trust an earlier export only when it still parses, and treat a folder
with an empty top-level JSON table as not prepared so Play prepares it again.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The not-installed Zoom row pushed Page::Mods from Extras, which
every_setting_and_page_is_reached_from_one_place refuses: Options > Mods
already has the one way in. It is now a note that says where to install
Zoom; once the mod is built in the row is still its switch and Settings
button.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: Checked on the desktop against main 9f8f6842d (merges cleanly, lock passes --locked). All 19 new lock packages are permissive (MIT/Apache/BSD-3/ISC-style webpki/Zlib/BSL option on ryu); the only build scripts (mysql, mysql_common) just pass target env vars, no downloads; TLS is rustls+ring only, flate2 uses miniz_oxide, no openssl/native-tls/-sys/cmake. cargo tree for horizon-client (default, --no-default-features, --target all) is identical to main, so players' game and native-mod compile are unchanged; the launcher's cold cargo fetch gains 21 crates / +2.5 MB once (noise in wall time). Note for later: webpki 0.22 and rustls-pemfile are unmaintained upstream; worth moving to mysql 28 when it no longer rewrites the lock.
serde's "expected value at line 1 column 1" says nothing about which of the
prepared folder's tables is damaged, so a player with a killed export only saw
a crash report. Read the core tables through horizon_world::json_file, which
prefixes the path, and have the startup check name a top-level table left
empty before the window opens.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
While Tab is held every frame sorted every entry twice (once for the faces,
once for the rows), allocated team keys inside the comparator, rebuilt and
measured every row's text, split the header and footer, and pruned the
health states with a scan per row (#10108). TabList now keeps the sorted
order and the laid-out rows, rebuilt only when their inputs differ (players,
scoreboard, header/footer, width, font and language); the faces reuse the
order; sort keys are borrowed; the health states go by a uuid set. Hearts
still animate every frame.
Mob.aiStep's looting, the villagers' item sensor and the fox, dolphin,
panda, piglin, allay and sulfur cube item passes run after the mob loop
over every loaded mob. In vanilla they are part of the mob's own tick,
and ServerLevel.tick ticks only the entities in inEntityTickingRange: a
villager six chunks from the nearest player, loaded but outside the
simulation distance, took the bread at its feet here and not there.
The loop records the mobs whose tick ran (Creatures::ticked), and those
passes leave the others alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In adventure mode, vanilla allows ItemStack.useOn if the item's can_place_on component matches the clicked block. Previously, use_map_on_banner checked only !self.may_build() and returned early without checking can_place_on.
This honours can_place_on_in_adventure on the banner's block position, allowing adventure mode players with matching maps to toggle banner markers.
Co-Authored-By: Antigravity <[email protected]>
🤖 Agent: each mob footstep scanned every mob three times (two id lookups and the warden/allay listener loop); now an event with no sculk block in reach and no warden or allay within 24 blocks skips the context and the listeners. Listeners give up past 16 blocks before any side effect, so vibrations are unchanged.
Override only ArrowSound::HitGround through Audio::play_on. Add an actual Game/Java block-hit capture using the existing headless client, Studio recorder and official sound codec.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: launcher-only fix. Maps the service's 403 use_checkout / not_offered_here to their own messages (86 tables), and drops one_click after use_checkout so the Shop opens the free checkout. Risk: none on the hot path; only the claim's error branch changes. Launcher tests 719 passed on the desktop.
🤖 Agent: villager_pickups tested every loaded villager against every dropped item, looking up tags before the reach: that was 12.8 of 23 ms of the mob phase in the load test (22.7 → 8.1 ms after this). The new ItemScan keeps the full scan's order and falls back to it if the item list changes under it.
🤖 Agent: merging. Every knock_back caller outside the legacy rules passes legacy = false, so the 26.3 path is unchanged. 1.8's air lift and the unhalved sprint push only run under the hosted-world switch.
The cosmetics service answers POST /v1/claim with 403 use_checkout where
Paddle sells (the set goes through its free checkout there) and 403
not_offered_here where the claim is left out. The launcher mapped neither,
so a player saw "Horizon's cosmetics service had a problem" (Discord bug
thread, 2026-11-05).
- Both codes are the Shop's own refusals now, each with its own words, in
all 86 lang tables.
- After use_checkout the saved answer drops one_click, so the Wardrobe stops
offering "Claim for free" and the Shop's button opens the free checkout on
playhorizon.dev.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. [combat] is only written when legacy is on, and unknown keys are rejected. The game reads it in dedicated.rs, and a bad section logs and keeps 26.3's rules. The test now looks for the section line, not the template's '# [combat]' comment, which made verify fail on the first head.
🤖 Agent: merging. legacy_rules() is remote.is_none() && combatRules, so a window on any server keeps 26.3's cooldown and damage. Measured over LAN: 1.8 rules 7 and 7 (diamond axe, charged then right after a swap), 26.3 9 and 3.6.
Every frame built, styled and measured the name tag of every player within
64 blocks, drawn or not: with 150 players and the camera facing the sky that
was ~0.74 ms a frame (#4664). A tag is now built only for an entity vanilla
would render, its culling box grown by 0.5 in the frame's frustum
(EntityRenderer.shouldRender), checked before any text work; armor stands
likewise. Status and native snapshot queries still see every tag.
other_players finds each player's entity through one id->index map instead
of scanning every entity per player (the first match, as iter().find).
🤖 Agent: merging. The 1.9 hit sounds and particles are filtered on receipt (remote.rs, lan_particles.rs), so nothing sent changes. Hurt sounds and crit particles stay, and 1.8 hit sounds are off by default.
Remove the blanket trait-mod presence bypass while preserving shader-pack guards. Verify the projected shadow in a game with an active native camera hook.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: merging. Legacy only moves the hand's matrices, and only for items with vanilla's flat display (pack-moved and post-1.8 items keep 26.3's). It's off by default, and the renderer is the only reader of the equip state it changes. On scripted fights against a vanilla server, #10117's Modern-vs-Legacy packet comparison matched on every scenario that is stable between two identical clients.
Zoom moved from the built-in extras to Horizon's official native mod
(#2030), so the Extras page lost its Zoom setting. Players looking for it
there, including the macOS report in #8713, found nothing.
The Extras page now keeps a Zoom row: with the mod built in it is the mod's
own on/off switch plus a Settings button that opens its page; without the
mod it opens the Mods page where it is installed. The cause is not
macOS-specific: every platform lost the row when Zoom moved.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The shader screen (shader_ui) draws the pack list and every pack's settings,
so the options screen's own rows for them go: the Shader Packs and Shader
Options button lists, a Horizon pack's option pages, their actions and the two
pack slider kinds, with the label fitter only they used and the four lang keys
only they read.
A button that pushes the Shader Packs page (as one on the Options screen would) listed the packs but left the picked pack's settings unread until a pack was picked again. Every way into the screen now reads them with the list.
A mob near some dropped item tested every item on the level (2,000 in
the load test: 1.5 ms a tick). ItemAreas now keeps the items of each
cell, and ItemScan walks the list in its order from one item in a met
cell to the next, testing what the scan tested; it keeps up with the
items the walk removes, and tests every item from there on if the list
changes any other way. The villagers' pickups walk the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shader Packs... opens a screen in the frame of the graphics and controller
settings (settings_shell) instead of two vanilla button lists. Its first page
lists the installed packs (a tick box on the one in use; picking it again
turns shaders off), the pack folder, and Horizon's own Shadow Cache under
"Built-in settings". Under the picked pack's name, the page list holds the
pack's own screens (a shaders/ pack's screen.NAMEs, a Horizon pack's
[[pages]]), as "Pack settings": switches, sliders, choices, profiles and links
to deeper screens, with the pack's own descriptions in the strip. The search
finds an option on any of the pack's screens. A shaders/ pack's changes apply
with Apply or when the screen closes; picking another pack keeps the last
one's changes first.
MinecartHopper.tick has no transfer cooldown in 26.3: it calls tryConsumeItems every tick, and consumedItemThisFrame caps a tick at one item even when makeStepAlongTrack tries too. The reported pre-#2250 whole-stack pickup is gone; add the regression test that catches it coming back, and cite the per-frame cap next to hopper_cart_suck.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The launcher traced a compile error to a mod only inside the helper function itself. horizon-mixins
wraps each helper in a block that calls it, `{ fn helper(…) { … } helper(…) }`. When Horizon
changes a type the helper's signature mentions (flair 1.0.0: `extra` became five-element tuples),
the error lands on that generated call, so no mod was named and the whole build failed instead of
leaving that mod out. A block holding only the helper and its call now counts as the helper's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cull conservative bounds of flame billboards before light lookup and vertex expansion. Keep shader/mod/TAA paths and unsupported dimensions, with emitted-geometry and edge-image coverage plus paired FPS samples.
Co-Authored-By: Codex GPT-6 <[email protected]>
HORIZON_SPIKES named almost nothing inside bench's prepare phase: with 150
players on a server and the camera facing the sky, prepare took ~0.43 ms a
frame and its spans accounted for ~0.005 ms. Each step between the fixed
ticks and the renderer now has a lap (pr_hud, pr_entities, pr_lights,
pr_frame, ...), dynamic lights two finer ones. They showed the cost: name
tags (eg_name_tags) built every frame for every player, drawn or not (#4664).
A resident column the server sends again was always relit here, for a
Horizon server's bulk edits whose packet carries the light from before
its relight. A proxy's next server resends its whole world at the same
coordinates, so every column queued light jobs for itself and its eight
neighbours, which a small CPU could not work through.
Relight a resent column only when the server announced Horizon's quiet
commands, the same feature flag Horizon servers send in configuration;
any other server's packet light is final, as vanilla applies it. A
reconfiguration resets the announcement until the new server sends its
own.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
From review, each measured against vanilla:
- DragonHoverPhase.getFlySpeed is 1, not the default 0.6: a hovering dragon thrown upwards
climbs back at vanilla's rate (new harness case, 12 commands off before).
- wrap / wrap64 are Mth.wrapDegrees: Java's % then one step of 360. rem_euclid lost the low
bits of a negative angle (-10.123456 became -10.123444) every tick (unit test).
- The parts' placement, the strafe aim and the flap timing use Mth.sin / Mth.cos and vanilla's
float casts.
- DragonSittingScanningPhase turns only while the player is more than 10 degrees off the
heading, with Mth.atan2 and vanilla's degree constant; dist is clamped in both divisions.
- A dragon's parts start at the origin, as vanilla's part entities do until the first aiStep,
so its first scanning turn aims from there.
The command execution harness gets "dragon scanning turn" (a dragon facing a survival player and
one turned away): 6/12 before, 12/12 now. All dragon and End harnesses match; the whole command
execution harness: 15832/15832.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging the mod hot-load design note, docs only. The point to check is the native measurement it rests on: a one-line gameplay change rebuilds in 570 s against 812 s cold, and mold doesn't help, which is why the run-time switch came before background builds.
🤖 Agent: merging live switching of content mods. The point to check is the server's world tables: with no data pack they used to stay on the startup resources, so a switched mod's data never applied. After a live switch they are now built from the new root (data_switched), checked in game by a mod function that loads, unloads and loads again.
GLSL asks a texel offset to be a constant expression; GL drivers also take one a
function receives as a `const in ivec2` parameter (Arc's FXAA samples its neighbours
that way, so its final pass was skipped in every dimension). Such an offset now moves
the coordinate by that many texels of the base level; a constant one stays an offset.
The biome categories and precipitations custom uniforms compare with are defined for the
GLSL as well, with the same values: Sildur's Enhanced Default reads
`biome_category == CAT_THE_END` in gbuffers_skytextured, which failed on the unknown
name. BIOME_* still needs the world's registry and stays in custom uniforms.
Three things GL drivers take that naga's GLSL front end refused:
- an empty declaration (`;`) at global scope in any version, as a macro that writes a
function leaves after it (DrDestens: 56 programs skipped);
- a precision qualifier on a function parameter (`float rand(highp vec2 p)`,
TrailerShaders: final and composite);
- a constant expression in a function whose value is infinite or NaN
(`1.0 / toeX` with toeX 0, Epoch's tonemap at its defaults): it is computed at run
time, as GL does, instead of folded into a literal that can't hold it. WGSL keeps
refusing it.
GLSL lets a pack sample a usampler2D or isampler2D with texture(); GL drivers read an
integer texture with nearest filtering, so it is the texel the point falls in. naga
rejects sampling an integer image, and Mello, Voyager, Exposa and Reverie lost the
programs that read their integer colortex buffers that way (Mello 20, Voyager 42, Exposa
48). Those calls now go to helpers that texelFetch the texel under the point, clamped to
the edge, at level 0 (textureLod: the level it names).
`const vec4 s = vec4(scale[0].x, ...)` over a constant array read the index and the
array from the function's arena while folding in the module's: naga panicked (index out
of bounds) and Ebin Resurrected failed to load. Both now come from the global arena.
A panic in one pack no longer stops the run: it is caught, and each pack ends with a
SUMMARY line of the programs skipped in each dimension. A pack without shaders.properties
keys is read as one (every key at its default), not a failure.
`mat2(v)` with a vec4 `v` is the matrix with columns v.xy and v.zw in GLSL, as when the
components come as several arguments. naga's GLSL front end made each column the whole
vector, which failed validation ("Composing 0's component type is not expected"):
Bloop's water normals (prepare1) were skipped.
Packs compare block ids with Distant Horizons' block materials outside any
DISTANT_HORIZONS guard (Exposa: `blockID == DH_BLOCK_LEAVES`), so Iris defines them in
its standard macros whether the mod is there or not. Exposa's composite and composite1
failed on the unknown names; DH_BLOCK_UNKNOWN to DH_BLOCK_ILLUMINATED are now 0 to 15,
the mod's EDhApiBlockMaterial numbers.
GLSL asks a fragment input of integer type to be `flat`, but since 4.30 only the
fragment stage's interpolation qualifier counts: a vertex stage may write
`out uint frag_worldTN;` and GL drivers link it to `flat in uint`. naga left such an
output without interpolation and refused it (Alpha Piscium: 111 programs). An integer
vertex output or fragment input with no qualifier is now flat.
`colors2_colorspaces_ ## a ## _to_ ## b ##(x)` (Alpha Piscium) pastes the name's last
part to its `(`. The preprocessor made one token of `..._6_to_8(`, which named no macro,
so the call to the function-like macro `colors2_colorspaces_6_to_8` was left for the
compiler as an unknown function. As GL drivers' preprocessors do, the paste now stays the
tokens it is made of, and the name is rescanned with its `(`.
Retain bounded local text quads while updating camera transforms, lighting and cosmetic marks every frame. Invalidate on text/font/atlas/bidi changes and bypass animated or inline content.
Co-Authored-By: Codex GPT-6 <[email protected]>
🤖 Agent: the request log reads the player's name before the request runs, so a release or drop that removes the guest still logs who it was. The e2e this PR adds ran on this exact stack against current main: 31/31 over real control sockets with horizon-bots.
🤖 Agent: pushed 4020e2537 for martipops' review. It refuses a rotation that overflows f32 (1e39 became inf/NaN after the cast), refuses a named position when the record still has a RootVehicle to restore (the vehicle would undo it), keeps recentered on retries, and documents that velocity is the server's body only. Tests added for each; union-tested on the desktop.
🤖 Agent: the commit point holds. place_arrival writes the .dat with write_dat_durable plus the advancements and stats with write_durable, each a Result, before ok, and the player stays held on any error. Retries after the commit answer through placed_arrival. martipops' two reviews are answered point by point in the comment above. server_transfer_e2e passed on the final stack (31/31).
🤖 Agent: with [transfer] off, no login is expected, so held stays false and watches() equals here(): every here→watches swap in hub.rs and the loading_chunks refactor are no-ops for ordinary servers. With it on, a held player is a viewer but not tracked, and gets list changes through broadcast_watching. Union-tested with main and the stack on the desktop, plus server_transfer_e2e (31/31).
tick_mob_sculk_events posts every loaded mob's footsteps, landings and
splashes. Each event looked its mob up by a scan of every mob (for the
context and the supporting block) and asked every mob whether it was a
warden or an allay, so a server with thousands of mobs paid three scans
of them for each footstep near no listener.
Now, as GameEventDispatcher only visits the listeners around the event:
while the mobs' events are posted, mobs are found by id through a slot
map, the wardens and allays are listed once with their positions, and
an event with no sculk block in reach and no warden or allay within 24
blocks skips the context and the listeners. A mob that stands as it
stood with no step due (every mob outside the simulation distance)
leaves at once, and a mob riding nothing, as before, skips the mount
bookkeeping. Mods still get every event (native_game_event).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Native rebuild times measured on the desktop, live switch times in game
on the ThinkPad and the content root on the desktop. Option A as built:
trait mods switch at run time, RenderHooks and the launcher's build-all
come next. The browser's buttons and index fetch as implemented.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
blocks_and_items_declared_in_code_get_ids expected a changed declaration
to rewrite the same root's stamp; each set now has its own root, so it
checks the new root and that the old one is left as it was. Found by
running the content-root tests on real prepared resources.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
villager_pickups tested every loaded villager against every dropped item,
looking the item up in the villager food table and tags before the reach:
on a server with hundreds of villagers and thousands of items that was
most of the mob phase. The reach comes first now, and a villager with no
item in its cells skips the scan (ItemAreas, as the looting passes do).
Mobs outside the simulation distance only check despawning, which reads
the nearest player and the difficulty: they no longer copy the player
view they see. A ticking mob borrows its guest's view for its tick (the
tick reads it through &MobWorld) instead of cloning it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: unknown states now read as air at the three places they enter (chunk palettes, block updates, BlockFacts::get), and the palette scan is skipped when nothing is unknown, so a vanilla-width chunk costs one maybe_has over its palette. Vanilla would disconnect on such an id (MissingPaletteEntryException) instead; air matches what Horizon already did for a wider host, so I took it. Union-tested on the desktop (clippy, wide_states, horizon-items).
🤖 Agent: removing JAVA_TOOL_OPTIONS, _JAVA_OPTIONS and JDK_JAVA_OPTIONS in java::command reaches every exporter JVM and javac but not the game's own launch, so a player's global -XX:+UseZGC still applies in play and only stops clashing with BATCH_OPTIONS' SerialGC during preparation. Built and tested in a union on the desktop (clippy, horizon-prepare tests).
Answers martipops' review of #8735:
- A rotation finite as f64 but past f32::MAX became an infinity (and a NaN pitch) after the
cast; it is refused now, like any rotation that is not a number.
- A record with a vehicle still to restore (RootVehicle) seats the player back on it at the
vehicle's own place, which undid a position the request named; the two are refused together.
- A retried import of the same hand-over, or a retry after the commit, answers the same
`recentered` as the first reply (kept on the guest until it is placed).
- docs/server.md says the velocity is the server's only: the arrival's teleport carries no
motion, as a vanilla login's does not.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkMap.tick runs ServerEntity.sendChanges only for an entity that changed
section, needs sync, or is in DistanceManager.inEntityTickingRange. Horizon
ran it for every tracked entity, so an idle one sent its position every 60
ticks and advanced its update clock. EntityState::idle now carries the
range (the simulation level, Levels::in_entity_ticking_range), and the
tracker skips sendChanges for an idle entity that stayed in its section and
needs no sync; a lead change still goes out at once (Leashable).
The game then stops describing such entities in full every tick: an idle
mob or item that was in the tracker's last pass at the same place, carries
no passengers, and is already tracked by every client within its tracking
range is described bare (where it is, none of its data). The tracker never
reads its data and never pairs a bare state, so every pairing still sends
the entity as it is (game/lan_idle.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
draw_tab_list ran every frame and, through remote_tab_players, cloned every
listed player and formatted two object-skin keys for each before it knew
whether the list was shown; update_remote_skins then searched every entity
once per level player and cloned the whole skin map. With 150 players and
none drawn a frame still cost ~0.4 ms more than with none (#4664).
- The list's visibility comes from the listed count; the players are built
only while it is shown or when it opens/closes (narration).
- The id:/name: object skins publish only when a player, its skin or the
atlas's object skins changed (a fingerprint and an atlas revision), so
inline faces keep the server's skin as before.
- update_remote_skins finds players and mannequins through one id->index map
and moves the resolved skins in instead of cloning them.
🤖 Agent: merging. The default --clients vanilla,horizon run is unchanged, and so is main's one-client capture (now 'one or two of'). horizon-legacy only adds a horizon-extras.txt in its own scratch home. On the desktop, Modern vs Legacy matched on 14 of 16 scenarios; eat and dig_teleport also differed between two identical Modern clients there.
🤖 Agent: merging. Pictures are now sized to the window × scale and capped by the atlas limit (the wgpu adapters' min on Linux, 16384 on Metal/DX), instead of a fixed 3840. RSS stays flat at 173 MiB for 5 min on a real GPU. One thing to watch: the Linux probe opens its own Vulkan/GL instance at startup on a background thread, and until it answers pictures cap at 8192.
With a stitched colour atlas (sprites at their own sizes), the normal and
specular atlases were still packed as a grid by PackAtlas::material, so a
block's material texels sat under another sprite's colour (#9570). The
companion atlases now reuse the colour atlas's size, rectangles and padding;
each map is nearest-sampled from its single level into its sprite's
rectangle (an fs_material entry point), and animated sprites copy into the
same rectangles. The grid path is unchanged.
EnderDragon's constructor sets EnderDragonPhase.HOVERING; only EnderDragonFight.createNewDragon
sets the holding pattern. Dragon::new started every dragon holding its pattern, so a dragon from
/summon, a spawn egg or a save without DragonPhase flew off. It now starts hovering, and
create_new_dragon sets the holding pattern.
Hovering had never run on the server, and the flight step diverged there: the target is a hair
away, where Vec3.normalize returns zero under 1.0E-5 and vanilla's Mth.sin/cos table and
Mth.atan2 turn the dragon differently from the exact functions. The step now uses them, the
doubles of moveRelative's input vector and vanilla's 0.91F and 0.8F as doubles.
The command execution harness gets "summoned dragon hovers" (phase and position every 20 ticks
over 120): 0/13 before, 25/25 now. The dragon harnesses (server dragon, contacts, NoAI, not the
fight's, death outside the End, death roar, forced End, spawn eggs in fluids, dispensers) match.
Fixes#10285
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use conservative projected-shadow bounds, retaining shadows whose casters are outside the view. Keep shader packs and native mods on the original path. Add resource-backed bounds coverage and measured FPS results.
Co-Authored-By: Codex GPT-6 <[email protected]>
In the glass theme account settings (skin_panel), long translated option
labels such as French 'Bras classiques' protruded outside the segmented
control pill due to an inherited ml(40px) margin and cross-axis stretching.
Additionally, the multi-sentence caption wrapped into a 10-line block inside
the narrow column, clipping off the bottom on 1080p displays.
- Remove the ml(40px) margin in glass theme mode.
- Set items_start() on the control column so segments size to natural width.
- Adjust segments option padding to 12px with flex_none and justify_center.
- Reorganize skin_panel so the caption spans full width across the bottom.
- Adjust Microsoft card width to 360px and reduce vertical padding in glass_who_plays.
Co-Authored-By: Antigravity <[email protected]>
Join a Friend opens from the title screen's Friends button and from the
server list. Both exits always went to the server list, so the title's
entry landed the player on a list they never opened (#9190, item 10).
The page now remembers whether the title opened it.
#10236 capped screenshots and the background at a fixed 3840 px, so an
8K screen lost detail. Pictures are now made at what the screen shows:
- viewer and background: the window's longer side x its scale factor
(7680 px on an 8K screen), tiles 360 points x the scale factor, in
256 px steps so a resize doesn't remake them every frame;
- never above the biggest texture GPUI's atlas holds: on Linux the
smallest max_texture_dimension_2d over wgpu's Vulkan/GL adapters
(asked once in the background, 8192 until known), elsewhere the fixed
16384 of the Metal and DirectX atlases. A larger picture would make
the atlas add a max-size texture on every frame (zed#66595).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A save can now name an entity it only knows as a `Who`: `mob_to_nbt` takes a
`uuid_of` resolver, `who_uuid` writes a `Who` as a UUID and `who_by_uuid` turns a
stored UUID back into the `Who` of a loaded player or mob. `EntityMemory::uuid_of`
supplies the id -> UUID map the game layer keeps, and the game layer's `mob_by_uuid`
now resolves through the same seam.
The neutral mobs' `angry_at` is the first writer: anger at another mob now saves
that mob's UUID instead of nothing. The warden's suspect list and the shulker
bullet's target are the two follow-ups this bridge unblocks; neither is written
here.
Under Linux, executing a freshly copied test fixture can fail with ETXTBSY (os error 26) if another test thread forked while the file was being written. Copy the fixture via a temporary file before renaming it into place, and retry exec/spawn if ETXTBSY occurs.
Fixes#10271
With the legacy rules on, a player's knockback follows 1.8.9's
EntityLivingBase.knockBack: the motion halves and the 0.4 push lifts in the
air too (26.3 lifts only from the ground), and knockback resistance is a
chance to take none instead of a cut. The sprint and Knockback push is added
on top as attackTargetEntityWithCurrentItem's addVelocity (0.1 up) instead
of going through knockback, for players and mobs alike.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-server accepts and keeps a [combat] section (off by default, left out
of the file while off); the dedicated game reads it itself, like
[anticheat], and turns on the legacy rules a hosted world's PvP page switch
gives. A wrong section is logged and fights keep 26.3's rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under Combat feel Legacy the client draws no sweep_attack or
damage_indicator particles a server sends (1.8.9 had neither), and with
'1.8 hit sounds' plays none of the entity.player.attack.* sounds 1.9
added. Hurt sounds, crits and enchanted hits stay. Only what this client
shows changes; nothing is sent.
1.8.9 drew no red tint over armour (LayerArmorBase.shouldCombineTextures
is false, so setBrightness skips it) and the body's flash is red at 0.3,
as 26.3's: both already match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Combat feel: Modern (default) or Legacy (1.8.9). Legacy changes only the
picture: a flat item in first person takes 1.8.9's ItemRenderer pose
(swing, eating and drinking, bow pull) through 26.3's own display
transform, a sword is held up while use is held (1.8.9's block pose,
swinging as 1.7.10 did), the main hand no longer dips after each hit,
and the attack indicator hides unless the player keeps it. The clicks
and every packet stay 26.3's. Poses 1.8.9 lacked (crossbow, trident,
spear, brush) and resource packs' own displays stay modern.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A PvP page switch, off by default: single player and LAN worlds resolve
melee hits by 1.8.9's rules (EntityPlayer.attackTargetEntityWithCurrentItem):
no attack cooldown and no sweep, 1.8.9 weapon damage (ItemSword 4 / ItemAxe 3 /
ItemPickaxe 2 / ItemSpade 1 plus ToolMaterial), Strength x(1 + 1.3 per level)
and Weakness -0.5 per level, Sharpness 1.25 per level, and critical hits that
allow sprinting and need no charge. Only a game that is the server uses them;
a window playing on a server never does, so what it sends stays the same.
The window passes the switch to its server thread each frame, like the
distances. lan_e2e's legacy-pvp check measures two diamond axe hits 12 ticks
apart: 7 and 7 under the rules, 9 then a cooldown-cut hit without.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. These are #10079's commits merged with main and nothing else; #10079 had landed in the already-merged pvp/hud-layout. The editor opens only from a world and is the only screen that writes the layout.
🤖 Agent: verified on the desktop: horizon-mods 36 tests, launcher 589 tests, clippy -D warnings clean. broken_on is read only from the registry index (the publish job adds it from horizon-mods' daily native-compat run). The warning shows only when the entry's Horizon is the commit the mod would build on, so a mod pinned elsewhere doesn't warn.
🤖 Agent: the harness itself: 16 scenarios, real vanilla 26.3 client vs Horizon, per tick and field; on main 11/16 match, with the series + #9918 16/16. Named-only and solo in parity_hosts (needs a virtual screen, ~15 min). Merge of main resolved a game.rs mod-list conflict (tick_profile + tick_script).
🤖 Agent: wire stream harness (#10082) on main: number keys' SetCarriedItem left a tick early (hotbar_swap, 15 ticks; dig_teleport) and a key tapped with a swing at the sky sent the slot before the Punch; with this change every tick matches vanilla. Head = cc7200cd7a's +35 −43 plus merges of #10083, #10085 and main (conflicts in remote.rs resolved to the tree the harness after-run built and passed).
🤖 Agent: verified end to end on the desktop: with 30 registry mods on Nightly 2026-11-05, main's launcher fails the whole build (eatinganimation E_TARGET_NOT_FOUND), and this one builds 22 and leaves out 8 with their reasons. MIXINS_REV moves to 5a64af9 (horizon-mixins#27, #28). The risk to watch: a compile failure costs one full recompile per round, capped at 3.
Stereo music and records were downmixed to mono and panned by the camera, so turning away hard-panned them and distance attenuation could silence them. OpenAL Soft plays a multi-channel buffer without spatialization or distance attenuation (AL_SOFT_source_spatialize auto), as vanilla's HRTF-off context does.
Keep the decoded channel count through the cache and mixer; mono buffers keep the existing panning and linear attenuation. Add a mixer test that stereo frames stay direct and unattenuated at any camera angle or distance.
Fixes#10037
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Vanilla's Math.sin is HotSpot's intrinsic, which rounds one of the drop
test's throws a bit lower on AArch64 than on x86-64; a vanilla 26.3 run
on an M-series Mac gives the AArch64 value. The install.sh fixture only
holds the Linux x86-64 archive and ran the cases with /usr/bin's
OpenSSL, LibreSSL on macOS: a fake uname picks that archive, and the
cases get the OpenSSL 3 the script checked for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: inventory in the wire stream harness: a shift-click's changed slots went out 10, 37 where vanilla's Int2ObjectOpenHashMap iterates 37, 10; now byte-identical, unit test pinned to the captured order.
🤖 Agent: bridge in the wire stream harness: 19 of 19 UseItemOn cursors differed from vanilla by ~6e-5 on main (double trig in the pick ray), all bit-identical with Mth's float table. Hot path cost: one table lookup per frame instead of libm sin/cos.
🤖 Agent: read against LocalPlayer.sendChanges and AbstractBoat.tick (rider look, vehicle, then sprint; paddles first). No riding scenario in the harness yet, so the proof is the read and the unchanged 16 scenarios. Land before #10084, which merges this branch.
🤖 Agent: verified with the wire stream harness (#10082): on main, dig_teleport answered a teleport mid-dig with a new START (sequence 34) instead of vanilla's ABORT; with the series every tick matches. pr_ready clean except the desktop nix-shell's uname-less install.sh test.
The held slot leaves remote_send_state (this PR); the boat's paddles go first and a
passenger sends its look, vehicle, then sprint (#10085).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A loaded desktop's vanilla client failed to log in; rerunning Horizon alone and
comparing with the earlier vanilla capture crashed on the missing client.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review:
- A perched dragon shrugs off a direct arrow, trident or wind charge given as `by`
(AbstractDragonSittingPhase.onHurt is 0, under 0.01): the command is refused.
- dealDefaultKnockback: a counted hit of a type not in #no_knockback pushes the dragon away from
the command's source position (`at`, else the direct entity), or in no direction without one.
- credit_dragon_kill switches to the credited guest whenever they are not the one acting, as the
mob kill credit does, so a guest's command that credits another guest awards that guest.
The harness scenario summons its dragons holding their pattern (DragonPhase 0): vanilla summons a
hovering dragon, which is sitting, and Horizon one holding its pattern; that gap is separate.
The intermediate build: 36/39; now 39/39.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the keyboard path is a mechanical move from keys.contains to held(Action), with the same default keys underneath; old horizon-controller.txt files keep their buttons (no bindLayout line), and the pad's stack drop also reaches the server path through drop_stack. Union pr_ready green.
🤖 Agent: the check is SocketAddr::from_pathname, and the test compares it with a real bind at every length 1-150, so it follows the OS limit (104 on macOS, 108 on Linux) without a hardcoded number.
🤖 Agent: SOCKS goes through ureq's own socks-proxy feature (socks 0.3.4, MIT/Apache), and the loopback SOCKS5 test proves the request reaches an .invalid host only through the proxy. Nothing changes for players without a proxy set.
🤖 Agent: matches 26.3: OptionsList inherits AbstractScrollArea.mouseScrolled, so the wheel scrolls the list and never reaches a row's CycleButton; the old key is dropped because every install saved it as true. Main merged in, union pr_ready green.
Client mods that bake from the world in bulk (a minimap's tiles, a world
map export) had to clone `game.world.chunks` and read `world_biomes!`,
which pins them to one Horizon commit. Four stable helpers replace that:
- `native_client_chunks(from, to)` shares the client world's loaded
chunks of an area as `NativeClientChunks` (Send + Sync): block state,
biome id, loaded, each column's top non-empty section, build height.
- `native_client_chunks_changed(&snapshot)` names the chunks that differ
from the snapshot now. A held chunk is copied before any edit, so the
same allocation means the same chunk.
- `native_client_biomes()` gives the biome table the terrain is drawn
with (`NativeClientBiomes`): names and tints by `NativeBiomeId`.
- `native_client_biomes_changed(&biomes)` says when that table was
replaced (a mod's seasons, a resource reload, another world).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Aligning every tick after the teleport on its acceptance moved the script's own
packets (the button release, the 20-tick position) by the server's lag: on the
desktop vanilla got the answer at tick 26, Horizon at 29, and both clients'
packets were otherwise identical tick for tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: harness and examples-only probe, nothing players run. New head: CI's parallel shards raced on one schematic .part file; each process now writes its own (three concurrent downloads checked against the pinned digest), and parity / parity passes. Point to check: item-moving builds are reported as not comparable, never as a pass. Result: 111/111 verified contraptions match vanilla.
The second end-to-end build of 30 registry mods on 6475f62 stopped on
errors inside carryblock's, distant_horizons', smartf3's and treefall's
support modules (made before the crate split), which named no mod: only
injected helpers were traced. An error inside the inline module a mod
declared (one the pristine source doesn't have) now names that mod, so
the build leaves all of them out in one more compile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A windowless game with a resident chunk, a jukebox at (0, 64, 0) and a disc put
in through use_jukebox: 41 ticks of the jukebox tick post the event at ticks 0,
20 and 40 and nothing else, and a jukebox that is never played posts none.
JukeboxSongPlayer.tick posts GameEvent.JUKEBOX_PLAY at the block every
PLAY_EVENT_INTERVAL_TICKS (20) ticks of a song, right before the music
particles:
37: invokevirtual #97 // Method shouldEmitJukeboxPlayingEvent:()Z
44: getstatic #101 // Field GameEvent.JUKEBOX_PLAY
56: invokeinterface // LevelAccessor.gameEvent:(Holder,BlockPos,Context)
66: invokestatic #104 // Method spawnMusicParticles:(LevelAccessor,BlockPos;)V
Horizon drew the note particle in the same twenty-tick branch and posted
nothing, so a sculk sensor beside a playing jukebox never clicked and an allay
never heard the song, although the stop half (JUKEBOX_STOP_PLAY with level
event 1011) was already posted when the jukebox went away.
cmd_damage had no arm for them, so both said "Target is invulnerable".
EnderDragon.hurtServer is hurt(body, ...): a quarter plus up to 1; reallyHurt runs only with a
player behind the source or an #is_explosion type and refuses fire, and hurt succeeds either
way unless the dragon is dying or the hit is under 0.01. EndCrystal.hurtServer refuses an
Invulnerable crystal (unless generic_kill, out_of_world or a creative player), fire and the
dragon's own damage; else it is removed, and blows up unless the damage is an explosion.
The command execution harness gets a "dragon command damage" group: 15/31 before, 31/31 after.
Fixes#10265
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI runs the three shards at once on an empty .local/lra-catalog: two of them
downloaded the same schematic into one `.part` name and one rename found it
gone. The part file now carries the process id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: test-only oracle (mods/horizon/src/test). Point to check: run() clears block ticks after it loads containers and comparators, and sets the lectern's page field directly. Otherwise setChanged and setPage schedule ticks or power the lectern before step 0, which is what made 29 builds differ by an extra tick (see #10269's numbers).
canSeeSkyFromBelowWater stops when a block dampens light and is not liquid(). Kelp and seagrass hold water, so guardians under them were still rolling the 1-in-20 and spawned less often.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
emptyContents destroys the block only when it is not liquid(). A bubble column is liquid and not a LiquidBlock, so breaking it first played destroy particles and a sculk vibration.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A windowless game from the prepared resources, a bow in hand: the two events the
use posts are read back from the game's own event recorder, at the player's feet
(10, 64, -4). A spear, whose use effects set interact_vibrations false, posts
neither.
LivingEntity.startUsingItem ends with
this.useItem.causeUseVibration(this, GameEvent.ITEM_INTERACT_START), and
ItemStack.causeUseVibration posts only when the stack's use effects ask for
interaction vibrations - the same gate and the same position stopUsingItem
already used for ITEM_INTERACT_FINISH. Horizon posted only the finish event, so
a sculk sensor beside a player drawing a bow never clicked and an allay never
heard the use start.
use_vibration takes the event now, and start_using_item calls it where vanilla
posts it: after the use is set up, before the kinetic weapon's map (which this
change leaves alone).
🤖 Agent: data and reader only, nothing players run. The catalogue is WTFPL from maybeachromium (LICENSE beside it). Point to check: tools/lra_schem.py is the one parser of untrusted downloaded schematics. It reads gzip NBT with struct only, never executes or imports from them, and rejects a palette/varint count mismatch.
stamp_native_hooks.py refuses a fragment that lists a guarded file
(native_hooks.rs is the generated source it stamps itself), so the fragment
declared an invalid comment path and the repo-wide stamp check failed.
A server with mods or states the client does not have can send block states outside our registry. Instead of panicking with an out of bounds slice access when indexing BlockFacts or unpacking palettes, map unknown states to air.
🤖 Agent: Wardrobe layer, retargeted to main after #10144. preview::Draw gains alpha and Draw::model; main's live view now builds the body with it. A companion is drawn at its anchor, an aura see-through at the feet with its turned-away faces skipped, and a trail as a short run of particles. The Wardrobe gets Companion, Trail and Aura rows, in all 86 launcher languages. Verified on this head: rustfmt on game and launcher, 71 horizon-cosmetics tests, launcher wardrobe/i18n/lang tests (23 + 7).
🤖 Agent: render layer, retargeted to main after #10143. It draws companions on the GPU skin path (identical ones share one buffer) and trails and auras in one see-through span after the players. Nothing is drawn beyond 48 blocks or outside the view. Measured in-game with HORIZON_COSMETICS_LOCAL: +0.6 ms prepare at 16 wearers and +1.1 to 2.4 ms at 64, mostly the auras built on the CPU; the follow-up is #10251. Verified on this head: horizon-client clippy -D warnings.
🤖 Agent: settings layer, retargeted to main after #10142. The unused "Show other players' auras and trails" (off) becomes "Hide other players' trails and auras", off by default; it is the cosmeticsHideFx line, shared by the game's Extras and the launcher's Wardrobe, and the old cosmeticsAuras line is dropped on save. Translated into the game's 75 languages and the launcher's 86. Verified on this head: clippy -D warnings on game and launcher.
🤖 Agent: merging the native on/off switch. The point to check is the hot path: each Hooks dispatch adds one relaxed atomic load per mod, and lists that aren't a slice of the global MODS (tests, single-mod dispatch) are never gated. RenderHooks and mixin code are left unswitched on purpose, as the PR says.
🤖 Agent: horizon-report format 2 (mob costs, level spans, tick_lan/sculk sub-spans, workers, network, overhead, stop/status). verify green on eb55d13; live on the SMP since 21:08Z as prof2 at 0.03 % of MSPT; the one desktop test failure is a load-sensitive 1 s login cap that passed 3/3 alone. Only spans and counters added, no gameplay change.
🤖 Agent: runtime layer, retargeted to main after #10140. follow.rs (frame-rate independent lag, snap after a teleport, kept out of the crosshair cone and clear of the head) and trail.rs (spawns by distance walked, 24 particles per player and 512 in all) are pure horizon-cosmetics code, plus the fx_bench example. Nothing in the game calls them yet. Verified on this head: clippy -D warnings and 68 horizon-cosmetics tests.
🤖 Agent: any GUI sprite with an animation section now packs its first frame and animates, so the 16x64 music_notes strip no longer squashes into one slot; load-time only.
check already refused a jar whose data folders are named as before 1.21; carry and report did
not, so a 1.20 pack's mods were carried and counted as partial while 26.3 read none of their
recipes, loot tables, tags or structures (Prominence II: 149 of 308 "come over", 28,948 recipes
and 8,784 loot tables in recipes/ and loot_tables/).
old_layout moves to carry and reads every folder 1.21 renamed (structures/, advancements/,
tags/blocks/, ...), by the larger share: a 1.20 jar may hold a misnamed singular tag folder and
a 1.21 jar a stale plural copy. carry writes old_layout to carry-report.json and carries
nothing; the report's row says why, is not one Port can carry over, and the launcher does not
count it as portable.
Fixes#10249
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: sulfur cube blast is TNT-interaction only with mobGriefing (NONE without), ENTITY_INTERACT sourced by the cube, tnt.disabled overlay and used stat.
🤖 Agent: rider body follows the vehicle kind as in vanilla (boat yaw, solveBodyRot's 85° clamp for living vehicles, on-foot turn in minecarts); portal E guard only while inside the portal this tick.
🤖 Agent: format layer of the companions/trails/auras stack, rebased onto the emotes: Slot gets Companion, Trail and Aura next to Emote, and Item::parse checks both kinds of block. Item::animate also builds its animated-part table once at parse instead of searching by name every frame, which saves about 0.3 ms a frame at 64 wearers (profiled). Verified on this head: clippy -D warnings on game and launcher, 60 horizon-cosmetics tests; nothing draws the new slots yet.
The end-to-end build of 30 registry mods on 6475f62 stopped on
eatinganimation's `held-frame` helper (E0063, ItemRenderState's new fields)
without naming it: the staged item_render.rs carries the helper with a
trailing comma rustfmt added, so its tokens no longer equal the plan's
helper_source. A helper is now also the function of its name in the staged
file, when the pristine source has no function of that name; two mods'
helpers of one name still name neither.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The switch and remove buttons run on the window's thread: while an
automatic update holds the game folder they now say "Wait for the current
operation to finish." instead of freezing the launcher until its downloads
end. auto::run takes the lock after the registry index arrives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
redstone_catalog_fixture builds the same contraptions in a map-backed
horizon-world level, clicks the same levers and buttons
(Behaviours::use_without_item) and runs tick_scheduled, run_block_events
and tick_block_entities, printing what VanillaRedstoneCatalogOracle prints.
tools/test_redstone_catalog_parity.py reads the catalogue, downloads the
archive's schematics into .local/lra-catalog/ (pinned to da4dd51, one
sha256 over all 188), turns each entry into a case (switches off except
those the catalogue held on, a neighbour update on every block, then input
steps: every vector up to 8 bits, random ones beyond, random changes for
stateful builds) and compares both sides step by step. verified entries
must match; partial and unverified ones are reported. Entries whose inputs
are analog levels, lecterns or bare wire are listed as unsupported for
now. Three shards; --full runs larger sweeps and every vector up to 16
bits.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaRedstoneCatalogOracle builds each contraption (block states and
block entities the harness reads from its schematic) in the overworld of a
real flat-world server, one contraption per server tick. Comparators keep
their saved output, containers and lecterns get real items (set silently:
no comparator ticks, no lectern pulse), and the comparator level of every
block entity is printed for the Horizon probe. The game clock keeps
running between contraptions (LevelTicks orders chunks by trigger time);
the probe starts from the time printed.
Levers and buttons are driven with useWithoutItem, and each tick runs what
ServerLevel.tick runs for blocks: scheduled block and fluid ticks, block
events and block entities, so pistons move. After each tick it prints the
outputs that changed, and at the end of each step the tick it ended on,
whether the level went quiet and a hash of the contraption's box. A tick
budget per contraption (the probe counts the same) bounds machines that
never settle; containers at or next to hoppers, droppers, dispensers and
crafters are watched every tick, and a changed level marks the run as not
comparable (the probe does not move items). HORIZON_CATALOG_TRACE=1 adds
every block tick and every block at a step's end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/lra-catalog/entries.jsonl is maybeachromium's catalogue of the Logical
Redstone Archive (#10103): input and output positions, encodings and checks
for every entry, one entry per line. They shared it under the WTFPL
(LICENSE). The archive's schematics have no licence and stay out of the
repository.
tools/lra_schem.py reads Sponge schematics (versions 1 to 3), written from
the format's specification.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cull::supported previously gated early-depth occlusion culling to Vulkan and Metal. Enable wgpu::Backend::Gl when the device supports SHADER_EARLY_DEPTH_TEST alongside indirect first instance and storage buffer writes.
SulfurCube.tickFuse explodes with ExplosionInteraction.TNT only when
mobGriefing is on (NONE otherwise), and equipItem's gameEvent has the
cube, not the player, as its source. Drop tests that checked constants.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The three vanilla dimensions, built from the shipped data, must answer what
their noise settings say: the overworld and the nether add creature packs at
generation, the End does not. This is the value `spawn_generation_creatures`
now returns on, read through `VanillaTerrain`, so it covers the whole path from
`end.json` to the gate instead of just the JSON read.
With the setting read as a hardcoded false (the pre-change behaviour, where only
`Dim::End` decided) it fails as
`assertion left == right failed: disable_mob_generation of minecraft:the_end —
left: false, right: true`.
`NoiseBasedChunkGenerator.spawnOriginalMobs` reads
`NoiseGeneratorSettings.disableMobGeneration` and places nothing when it is
set. The shipped presets are explicit about it: `end` sets it true, the other
six set it false. Horizon never read the field and instead skipped the pass for
`Dim::End` alone, so a datapack dimension that used the End's noise settings -
or any preset asking for no creature packs - still got them, and the reason the
End was empty lived in a dimension check rather than in the data.
The setting now reaches the client seam: `Generator`'s settings carry it, the
pipeline trait exposes it next to `sea_level`, `VanillaTerrain` answers it, and
`spawn_generation_creatures` returns before doing any work when it is set. The
existing `Dim::End` check stays as well, so a preset that fails to load cannot
start filling the End; for the shipped data the two agree, since `end.json` is
the only preset that sets the flag.
Vanilla's Camel.canAddPassenger accepts a rider while two or fewer ride,
with no baby check. Drop two tests that only checked their own inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A boat sets its passenger's body to its yaw (AbstractBoat.clampRotation);
a living vehicle's rider is drawn from the vehicle's body with the head at
most 85 degrees off and a fifth of the offset past 50
(LivingEntityRenderer.solveBodyRot). Minecarts and cushions keep the
on-foot turn, as vanilla's LivingEntity.tick does. The inventory key is
held back only while the last tick found the player in the portal, not
during the effect's fade-out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
anvil_update fires where AnvilMenu.createResult ends, once vanilla has
worked out the result, while the left slot holds an item: on the host for
each player's menu (single player's server thread, a LAN or dedicated
host), before prepare_anvil, and in the window's own menu. A mod is told
the inputs, the rename box's text and vanilla's result. It may change the
output, the level cost and the material cost, or cancel, which leaves no
result (NeoForge's CommonHooks.onAnvilUpdate).
The host asks again after a rename that leaves the result as it was, as
setItemName runs createResult. The window's click prediction starts from
the station's cost and material cost, so a result a mod made can be taken
there as on the server. The window names the player as the server knows
them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #9996.
NoiseBasedAquifer::compute_substance is 4.7% of the overworld's proto
phase by itself. A sixth of that went to rebuilding the 12 grid cells
around a block whenever its aquifer cell changed: every location was
unpacked again from its packed long, and the neighbourhood was copied
as a 288-byte Option. The fill walks each column from the top down, so
a rebuild usually follows the cell just above.
Each drawn location is now also kept as the coordinates BlockPos.getX,
getY and getZ read back from it, and the 12 cells are arrays of grid
index, coordinates and horizontal distance to the column. One cell
down, the 8 cells at the old anchor's y and the y below move up two
slots and only the 4 new ones are loaded, in vanilla's loop order. A
cached fluid status is read inline; computing a missing one is out of
line. Default air, water and lava are classified by their IDs before
any block name is read.
Every block, pending fluid update, error and cached cell is the same as
before: the same operations in the same order, the same positional
random draws for the same cells. A test runs the previous search
against the new one over whole columns, scrambled blocks and positions
outside the grid, and the snapshots match byte for byte.
A first draft by DeepSeek V4.1 Flash, via pi, was reviewed and
reworked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
The ledger named the four renderer overrides but not the method they override;
it now says the box is the frame's, grown by 0.5, and points at the dropped
item's own copy of it.
An item sliding from x = 0 to 1 in camera space: the box is -0.625..0.625 at the
start of the frame, -0.125..1.125 halfway through and 0.375..1.625 at its end,
where the old code had the last on every frame. A falling item's box floor is
65.0 halfway down a 66 -> 65 drop instead of the tick's 64.5.
EntityRenderer.shouldRender tests the frustum against
getBoundingBoxForCulling(entity, partialTick), which EntityRenderer leaves as
Entity.getInterpolatedBoundingBox(partialTick) - ItemEntity overrides neither -
grown by 0.5 all round.
The dropped-item pass built that box from the tick's own position, so for the
frames of a tick in which an item crossed the frustum edge it was culled at the
cell it was about to reach: an item sliding from x = 0 to 1 had its box at
0.375..1.625 (camera space) on every frame, where at the start of the frame it
stands at -0.625..0.625.
dropped_item_cull_box(previous, position, alpha, camera) is that box now, the
interpolated position grown by the item's 0.125 half-width and 0.5, with the
0.25 height below and 0.75 above.
🤖 Agent: merging the in-game mod browser. The point to check is that install never trusts the download: each file goes through horizon_mods::index::check_file (size, SHA-256, signature, manifest against the index) before it reaches horizon-mods/, and revoked files are refused. Tests pin both cases with a real .hmod.
The Wardrobe's picture (horizon_cosmetics::preview, its live view
included) draws a companion at its anchor, an aura see-through at the
feet and a trail as a short run of particles behind; the Wardrobe gets a
row for each slot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A companion is posed on no bone: it follows its anchor with its lag,
keeps clear of the head and, for the player's own in first person, out of
the middle of the view. Trails spawn by distance walked and are drawn as
camera-facing particles; auras blend at the feet with their back faces
dropped. Trails and auras of every player go in one see-through span after
the players. Others' follow "Other players' cosmetics" and "Hide other
players' trails and auras". Nothing is an entity or reaches a server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The setting that was "Show other players' auras and trails" (off, for
items that didn't exist) becomes a hide switch, off by default: others'
trails and auras show like their other cosmetics unless the player hides
them. In the game's Horizon settings and the launcher's Wardrobe, the same
cosmeticsHideFx line of horizon-extras.txt; the old cosmeticsAuras line is
dropped on save.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
follow.rs: a frame-rate independent lag toward the anchor, a snap after a
teleport, and the two rules that keep a companion out of the way (out of
a cone round a first-person crosshair, clear of the head). trail.rs:
particles spawned by distance walked only, placed from their age alone,
24 per player and 512 in all. fx_bench measures their CPU cost for 1, 16
and 64 wearers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three new slots that ride no bone: a companion floats beside the player,
a trail leaves particles where they walk, an aura glows faintly at their
feet or round the body. Their blocks, limits and checks live in fx.rs;
item.rs reads them, refuses a bone on them and measures their extent.
Nothing draws them yet.
Item::animate no longer looks its parts up by name every frame (a tree
search and string compares per animation, for every wearer): the animated
parts are found once, when the item is read. At 64 wearers in view that
was 0.3 ms a frame on an i7-7500U.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob walking 0.9 -> 1.1 (eye 1.62) reads the cell at 0 at the start of the frame
and 1 from the boundary on, where the old probe read the tick's cell 1 on every
frame; a falling one reads 71.62 then 70.62. An arrow falling 64 -> 65 reads
64.13, 64.63 and 65.0, so the cell at 65 only at the end of the frame, where the
old probe read 65.0 throughout.
EntityRenderer.getPackedLightCoords takes BlockPos.containing of
Entity.getLightProbePosition(partialTick), and Entity leaves that method as
getEyePosition: getEyeHeight() over getPosition(partialTick), which is
Mth.lerp(partialTick, xo, x) on each axis.
The mob pass probed floor(position) with position.y + eye_height(), and the
projectile pass the same on its own position: the tick's own position, so a mob
walking from x = 0.9 to 1.1 was drawn lit by the cell at 1 for every frame of the
tick, and a falling one by the cell it was about to reach, instead of the cell it
was drawn in.
Mob::light_probe(partial) and Projectile::light_probe_y(partial) now give the
probe of the frame, through mobs::living::eye_probe_cell (Mth.lerp plus the eye
height, floored per axis).
Old/new for a mob walking 0.9 -> 1.1 (eye 1.62): cell x = 1 on every frame before,
x = 0 at alpha 0 and 1 from the boundary on. For an arrow falling 64 -> 65: 65.0
before, 64.13 at alpha 0 and 64.63 at alpha 0.5, the cell at 65 only at alpha 1.
Held mappings are named by an Action rather than read by key, per device, and more actions can be bound: hotbar
slots, fullscreen, toolbars, social and quick actions. Held drop repeats and throws the stack. Readers of the
movement keys see the left stick, and a bumper steps the hotbar through the wheel's gates. Prompts over gameplay
are pad-driven and keep what's behind them, the keyboard opens on every field, and focus follows its widget.
Buttons and the crosshair draw from Horizon's own art, one file per pad family, instead of text labels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. The screenshot tiles, viewer and background no longer go through GPUI's never-evicted img(path) cache. Two 32k×16k screenshots on Home went from an 8.7 GiB peak to 277 MiB on the desktop. One risk to watch: the 2 s grace lets the budget overflow while more than 256 MiB of tiles is on screen at once (about 280 tiles with Show all), which is bounded by the list itself.
Screenshot tiles (Overview's three newest, the Screenshots tab), the
viewer and Home's background went through gpui::img(path): GPUI decodes
the whole picture and keeps it, and its sprite-atlas copy, until the
launcher quits. One 32768x16384 screenshot is 2 GiB decoded; with two
of them Home alone reached 8.7 GiB (24 GiB reported on Linux).
- pictures (lib): PNGs are read a row at a time and box-filtered down to
the drawn size, never held whole; other formats decode whole only
under 256 MiB. Lru keeps made pictures under a byte budget, sparing
those drawn in the last 2 s.
- picture_cache: makes them off the UI thread (3 at once), 640 px for
tiles, 3840 px for the viewer and background, 256 MiB budget; evicted
images are dropped from the window's sprite atlas.
- glass_shot_blur's blurred copies go through it too, from a shrunk read
instead of image::open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The row said the probe was drawing-only and skipped; it is what selects the block
an armour stand is drawn with, so it now points at the code that walks it.
Four cases over the jar's values: a full stand's eye at 65.7775 (cell 65, where
the old base + 0.5 read 64), a small stand at 64.2 (65.1875, cell 65, where the
old read 64), a marker's box scanned for a 15 at (0, 65, 0) and falling back to
the first cell walked (0, 64, 0), y walked before z, the cell above the box left
out, and the marker branch ignoring the partial tick; and a burning stand's block
light at 15 with the sky light left as the probe cell's.
EntityRenderer.getPackedLightCoords reads the block Entity.getLightProbePosition
is in, and ArmorStand overrides that method:
- a marker walks BlockPos.betweenClosed of getDimensionsMarker(false) - the box it
would have without the marker flag, 0.5 by 1.975, or 0.25 by 0.9875 when small -
from position() with no partial tick, and returns the first cell whose
max(block, sky) brightness is 15, else the brightest, ties going to the first
cell BlockPos walks (x fastest, then y, then z);
- every other stand is Entity.getLightProbePosition, the eye: eyeHeight 1.7775 for
a full stand and 0.9875 for a small one (BABY_DIMENSIONS), at the frame's
interpolated position.
Horizon read floor(base + 0.5) for every stand, so a stand standing on a lit floor
in a dark room was drawn with the floor's light: a full stand on y = 64 read the
cell at 64 where its eye at 65.7775 is in the cell at 65, and a hologram marker on
a dark ceiling was lit from its feet's block instead of the brightest cell of the
box it would have without its flag.
EntityRenderer.getBlockLightLevel also puts a stand that isOnFire at 15 block
light, which was not read at all.
On a smoke server (2 ms ticks) the overhead line charged the chunk pump's spans of the
wait between ticks (1,788 a tick) to MSPT. Only the spans the tick records count now,
and a dedicated server records spans anyway for its slow-tick line (HORIZON_SPIKES,
on by default), so they are listed apart and left out of the report's own cost.
The 'other' worker jobs ran at 278k/s for 0.0 ms each: two thread-CPU clock reads
each cost more than the job; they are counted only. A sum of no players' rates no
longer reads -0.0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`CreativeModeInventoryScreen.selectTab` shows and focuses the search box only
on the search tab; any other tab hides it, lets it lose the focus and clears
its query. Horizon focused the box when the search tab was selected but never
took the focus away, so the box stayed focused behind every other tab.
`Creative::select` now sets the focus from the tab it shows, keeping the blink
reset the search tab always had.
🤖 Agent: the Wardrobe button opens the launcher's Shop tab now, so its label drops "on playhorizon.dev"; the key is renamed in place in all 86 tables (no stale key left), and the i18n test passes.
The registry's daily check (horizon-mods native-compat.yml) marks the
versions horizon-mixins refuses on the newest Nightly with `broken_on`
({horizon, code}) in index.json. The browser's fit line now says
"Doesn't build on the Horizon you play (CODE): a build leaves it out until
its author updates it" when that Horizon is the one the mod would build on.
- horizon-mods index: `Version.broken_on` (empty and absent by default).
- native_fit: `Fit::Broken { code }` for a hook mod, or a mod pinned to the
played commit, when an entry names that commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A collector chosen in JAVA_TOOL_OPTIONS or _JAVA_OPTIONS stopped every JVM
horizon-prepare starts, as they pass -XX:+UseSerialGC: "Multiple garbage
collectors selected", so the version was never prepared. java::command, which
they all go through, now removes those variables and JDK_JAVA_OPTIONS.
Fixes#10067
The ItemEntityRenderer row names where its light comes from now, and
EntityRenderer.getBlockLightLevel's note covers dropped items beside mobs and
projectiles.
The three cases quote the old values: the failing probe cell of a falling item
(64 against the eye's 65), the tick's own position against the frame's halfway
one (64.7125 rather than the destination 65.2125), and a burning item's block
light (15 over a dark cell's 0, sky untouched).
EntityType.ITEM declares eyeHeight(0.2125), and EntityRenderer.getPackedLightCoords
reads the block of Entity.getLightProbePosition, which Entity leaves as
getEyePosition: the item's position plus that eye height, at the position the frame
draws (getPosition(partialTick)), not this tick's own.
The dropped-item pass probed the cell of base + 0.1 at the tick's position, so a
falling item at y = 64.85 was lit by the cell at 64 (the floor's light) while its
eye at 65.0625 sits in the cell at 65, and the light jumped a frame early when an
item crossed a cell boundary.
EntityRenderer.getBlockLightLevel is isOnFire ? 15 : the level's block light, so a
burning item is now drawn at full block light with the level's sky light; the
item's fire ticks already clear for a fire-immune stack, which ItemEntity.fireImmune
makes never on fire.
A picked-up item's particle is not an entity and keeps the cell it is drawn in.
Background automatic registry updates and manual mod operations (install,
remove, enable, disable) could mutate the same game home folder concurrently.
Because auto-update loads `mod-browser.json` before downloading and saves upon
completion, a manual install or removal occurring during the download would
be overwritten by the stale snapshot upon completion, losing manually added
mods or reviving removed mods. In addition, temporary save files could collide.
Introduce a per-game-home re-entrant mutex (`browser::lock::lock_home`)
covering mod mutation critical sections across background auto-updates and
manual operations. Use unique atomic temporary file paths during save to
prevent collision.
Co-Authored-By: Antigravity <[email protected]>
A loaded server catching up ran a 20-tick bite's remaining ticks in a burst and finished the apple.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It opens the launcher's Shop tab now, not the website, so the old label
"Get more cosmetics on playhorizon.dev" was wrong. The key is renamed in all 86
tables, in place.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Pay checks that the checkout's uuid matches the recipient and only opens playhorizon.dev/shop/pay with a validated txn_ id; no card entry in the launcher.
🤖 Agent: the Shop is a plain top-bar tab with no badge; it never opens by itself and isn't the start page; a catalogue failure shows one quiet line and Play is untouched; try-on uses #10153's Viewer (no second renderer).
One mod that horizon-mixins refuses or that doesn't compile used to fail the
whole native build (#horizon-support 2026-11-05: eatinganimation's moved
target, hitbox_plus's support module, worldedit's moved chat.rs). The build
now leaves that mod out, with the mods that need it, and builds again:
- isolate.rs names the mod a horizon-mixins diagnostic is about (mod_id, its
manifest, its support module's name or destination), or the mods the
compiler's errors were traced to, and adds their dependents.
- A refusal costs one horizon-mixins run; a compile failure one compile, at
most 3 times. A server engine export still fails as before.
- native-mods.json records `left_out`, so the same mods turned on read as up
to date, and the Mods page says "X was left out: made for another Horizon
version / clashes with Y / it needs Z, which was left out" (86 languages).
- MIXINS_REV → 5a64af9: horizon-mixins#27 resolves `horizon_client::…`
targets that moved into horizon-client-lib, #28 follows workspace globs for
a module's name and puts the mod id on module errors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture registry takes a pack version so the test can install
cozy 1.0.0, turn coords_hud off as the Mods page does, and install
cozy 1.1.0: the file is updated and the mod is still off. Turned back
on, 1.2.0 keeps it on. With the old unconditional set_enabled(m.on)
the update re-enables it and the test fails at 'the update kept it
off'.
install_version_in() applied java::set_enabled(home, id, m.on) for
every mod of the pack on every install, and installing a registry pack
again is how it updates. horizon-mods.txt is the same list the Mods
page toggles, so a mod the player turned on or off in a pack profile
was reset to the pack author's 'on' by the next pack update.
The install now reads the pack's previous record (pack.json's 'id
version' entries) and carries each listed mod's current on/off choice
through the install, which turns a mod on as an explicit install. A
mod the new pack version adds takes the pack's own 'on', and a fresh
install has no previous record so every mod does, as before. The Java
mod path already keeps '.disabled' through updates the same way.
🤖 Agent: shop price rules (sets partly owned, items inside a set in the basket, Supporter once) match the site's pricing.ts and are tested against hand-worked values; nothing player-visible yet. Requeued after merging main (#10153's lang tails).
🤖 Agent: Requeued on the same head: the parity run died in 1 s on the runner's missing .horizon-prepare.lock (environment, no harness ran), so it's rerunning (42040505129). The interact packet now carries the ray's hit point from the mob's feet. Union on main dd675b574 green.
🤖 Agent: With a scaled resolution, final now draws at the picture's size into its own target, and one linear copy stretches it to the window. Same-size output and packs without a final keep their old paths, so only resolution-scaled pack users take the extra pass. Union on main dd675b574 green (fmt, clippy, 4426 client tests including the render crate).
🤖 Agent: Matches LivingEntity.calculateEntityAnimation: a passenger calls walkAnimation.stop(), which zeroes speed, old speed and position, so the seat moving with the boat no longer swings the legs. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
🤖 Agent: WebGPU allows binding a depth view as unfilterable-float, and textureLoad(...).r is the same texel, so Vulkan, DX12 and Metal read the same depth. The translation test now fails on any GL-run entry point that doesn't translate, except the one known glint case. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
break_speed fires at the end of Player.getDestroySpeed, on the Game that
works out a dig: the window's own prediction, and the host's (single
player's server thread, a LAN or dedicated host) for each player. A mod is
told the block and vanilla's speed, and may change it;
BlockBehaviour.getDestroyProgress then divides it by the hardness.
tick_mining now takes its progress from destroy_progress, like the other
digs, so the speed is worked out in one place. The remote client asks it
for blocks of hardness 0 too, so a mod's speed of 0 stops those as well.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Merged main (9906 landed with the INF-seeded outline_center fold, kept from main) and ran marker_particles on this head: PASS, 2,187 cases and 462,718 lines matching the pinned client. The hive's loop now draws nextInt(1) again on the check that ends it, as BeehiveBlock.animateTick's for-condition does.
Part of #9997.
ClimateIndex::search, vanilla's Climate.RTree search, is about 5% of
overworld proto generation (callgrind, generate_proto, seed 12345) and 2%
of the nether's. About 21 boxes are measured per search: the root's two
children and the 19 below.
Three changes, none of which changes which leaf a search returns:
- The best leaf's distance goes down into each subtree and back up with
its result. Vanilla measures the best leaf again at the start of every
subtree and after it returns; the values are the same.
- The offset axis is always 0 in the target, so each box's squared
offset distance is stored once instead of measured every time.
- When every bound in the tree and every target value is within 2^29,
no term or sum of a distance can overflow, so a partial sum only grows.
A box's sum then stops once it reaches the best distance so far: the
search would only compare it with that distance and skip the box. The
axes are added depth first, as surface biomes are points in depth.
Otherwise the full wrapping sum is used as before.
The search still walks the same children in the same order from the
same previous leaf, and replaces the best only on a strictly smaller
distance, so ties go the same way. New tests compare it with a copy of
the old search over long target sequences that keep the previous
result, on the vanilla overworld and nether lists and on generated
lists full of ties, inverted spans, extremes and values at the bound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
Commands typed in the window parse tags against the window's own tags,
which no server packet updates; that gap predates live switching and
needs its own change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Settings hint now reads (http://host:port, socks5://host:port). Each language keeps its translation, without the word HTTP and with the socks5 form added; i18n's tests want every string in every table.
Turn on ureq's socks-proxy feature and stop refusing SOCKS addresses, in port-pack's parse and the launcher's settings. socks5h, curl's name for SOCKS5 with remote DNS, is read as socks5, which already works that way in ureq. A test runs a request through a stand-in SOCKS5 proxy.
Fixes#8143
A workspace member with no code whose dependencies are all optional: Cargo
locks them, so a native mod's crate may name them, and no build compiles them
(horizon-client's dependency tree, default and --no-default-features, is
unchanged). All four are pure Rust and use the rustls + ring already locked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When entering borderless fullscreen on Windows while the window is
maximized, winit's WM_NCCALCSIZE handler forces the client area to
match the desktop work area (rcWork), which excludes the taskbar.
Because the window is sized to monitor dimensions, this clamps the
bottom edge above the taskbar and shifts the top edge offscreen by the
taskbar height (~48px), leaving the taskbar visible on ultrawide and
standard displays (upstream MC-311683).
Ensure that any transition to fullscreen unmaximizes the window first,
and explicitly pass the active monitor handle to Borderless mode so
the window covers the entire display area without vertical offset.
Fixes#9984
Co-Authored-By: Antigravity 2.0 <[email protected]>
MultiPlayerGameMode.handleContainerInput puts the changed slots into a new fastutil
Int2ObjectOpenHashMap and ServerboundContainerClickPacket writes them as it iterates: the key 0
first, then the table from its last position down. Horizon listed them in slot order. The wire
stream harness's inventory scenario caught it: a shift-click from slot 37 into slot 10 went out
37, 10 from vanilla and 10, 37 from Horizon, a byte-level difference any server can see.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: emotes, part 3 (#10069): others see your emote through the cosmetics service, now based on main. CI verify is green. Polling is bounded: at most once a second, only while Horizon players are in view, and only with Other players' cosmetics on Shown. Sending is signed and only while Show me as Horizon is on. Without horizon-cosmetics#14 deployed, the service answers 404 and the game backs off.
🤖 Agent: emotes, part 2 (#10069): the wheel and playback, retargeted to main after #10091. CI verify is green. Two clients played every emote in game; captures are in the PR. Visual only, nothing sent to game servers. #10093 follows.
Install ruby 1.0.0, turn it off in the Mods page, run the automatic
update to 1.1.0: the file is replaced and the mod is still off. Turned
back on, the next update keeps it on, and an explicit install of a
disabled mod turns it on again. Without the fix the first update
re-enables it and the test fails at 'the update kept it off'.
install_version_guarded() turned every mod it installed on with
java::set_enabled(home, id, true), and the automatic registry update
(browser/horizon/auto.rs update) installs through the same function.
A content mod the player had turned off in the Mods page therefore came
back on after every automatic update: 'off' is the absence of the
'mod.<id>:true' line in horizon-mods.txt, which the update re-added.
The guarded install no longer touches the flag. The player's own
install (install_version, which the browser and modpack paths use)
turns the mod on after the guarded install, as before, while the
automatic update leaves the mod's current on/off choice alone. A new
install still starts on because it goes through install_version.
`Creative::search_tab` and `select` as `Game::key_pressed` uses them: the
search tab is found by its type, selecting it focuses the search box, and the
query the previous tab left is emptied.
`CreativeModeInventoryScreen.keyPressed` takes the chat key on a tab that is
not the search as "show the search tab": it selects
`CreativeModeTabs.searchTab()` and sets `ignoreTextInput` so the letter the key
carries never reaches the search box. Horizon had neither: T on a creative tab
did nothing, and the letter went nowhere.
`Creative::search_tab` finds the search tab by its type, so the key selects it
through the same path a tab click uses (`select_creative_tab`, which clears the
quick-craft clicks as `selectTab` does). `Game::key_pressed` takes the chat key
there, and the `ignoreTextInput` flag it sets makes `type_chars_into_screen`
(`charTyped`) drop that one letter and no other.
🤖 Agent: emote format, the bottom of the emotes stack (#10069). pr_ready on the whole stack is green except the NixOS-only uname test. pose_body copies the target at full weight: 0.26–1.1 µs per emoting player and frame, measured. #10092 follows once this lands.
The items, orbs, projectiles, minecarts and boats were counted for the level in place
only (the End, on the SMP, when the report ends), so tick_boats' 3 ms had no count
beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--clients horizon,horizon-legacy plays every scenario with two Horizon clients, the
second with the Legacy feel and every combat extra on, the 1.8 rules included, and
compares their serverbound streams as the vanilla comparison does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command registries took item and block tags from the resources the
game started with and were built once per world, so /execute and
/give refused a tag a switched mod added (seen in game: Unknown item tag
'c:gems/glint'). They now read the world's tags and are built again
after a live switch's data reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the official SMP, 12.6 of tick_lan's 14.7 ms a tick and all of tick_sculk_entities'
6.4 ms had no span below them. Name the steps: guest ticks, entity states, the hub's
level pass (world sync, chunks, block changes, containers, entity tracking, player
state), the end of the tick and the host events; sculk's prepare, wardens, step-on and
entity game events. Spans cost nothing while off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client sends /tp at a fixed tick, but a loaded server answers 2-6 ticks later from one run to the
next (seen with two identical Horizon clients). The ticks before the teleport compare one to one; from
the client's AcceptTeleportation on they align on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10153 replaced the Wardrobe's still picture with viewer::Viewer. The Shop now
uses its own Viewer (drag to turn, scroll to zoom, Reset view) instead of the
still picture and its Turn button, so there is one renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
23f8bd4fe removed the only consumer of MotionEvent::EnterBlock, and the event was pushed only in the client pass. The server pass pushes it now and the guest server tick triggers it for the acting player.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
The official SMP is mob-bound (5,400 mobs, 73 ms a tick at 60 players) but the report
only said tick_creature_entities took the time, not which mobs. Format 2 adds, without
a restart and at a measured cost:
- mob_costs: one tick in four times every mob's tick and sums it by dimension and type
(ms a tick, mobs ticked, us a mob);
- level_* spans: each dimension's share of the server tick;
- chunk_workers: streaming::job_stats while recording (load/generate/decorate/light...);
- network: wire bytes in/out per player and in all (connection counters, no new
per-packet cost);
- overhead: spans and mob samples a tick at their price measured on the machine;
- horizon-report stop / status.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ServerLevel.setChunkForced` calls `getChunk` when a chunk becomes forced, so a
command right after `/forceload add` finds it loaded. Horizon only queued it, so
`execute in minecraft:the_nether run function` with `forceload add`, `place
jigsaw` failed with "That position is not loaded" and a skyblock pack's nether
island never appeared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A data pack's `data/<ns>/structure/*.nbt` were ignored. A jigsaw pool naming one
placed nothing, so `/place jigsaw` failed with "Failed to generate jigsaw" and
`/place template` said the template does not exist; that is how a skyblock pack
places its island. Packs also could not replace vanilla's templates.
The enabled packs' templates now join the worldgen overrides, highest pack first,
over the prepared ones, as `StructureTemplateManager` reads resources. `/place
template` and structure blocks look in the world's `generated` folder, the packs,
then the game's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`LevelAccess::load_block_entity` decoded a template's block entity, and that
drops `LootTable`, so a chest placed by `/place template`, `/place jigsaw` or a
structure block came out empty. A skyblock pack's starter chest has its items in
a loot table. Report the table as a level event and hand it to `take_loot_table`,
as `/setblock` and `/data` do (`RandomizableContainer.tryLoadLootTable`).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
26.3 reads a direct block state provider as a state object. A bare string names
a provider in the `worldgen/block_state_provider` registry, so an upgraded
feature with a default-state `simple_state_provider` failed with
`unknown state provider minecraft:air` and was skipped at world start. Vanilla's
own features write `{"id": "minecraft:clay"}`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Void packs turn off vanilla's iceberg noises with
`{"firstOctave": -1000, "amplitudes": []}`. The upgrader left this as it was and
called it "more than 32 octaves or negative amplitudes"; 26.3 cannot read it, so
the world failed to start with `invalid world generation field: base_octave`.
With no octaves the noise is 0 everywhere, which 26.3 writes as one muted octave:
`{"base_octave": 0, "amplitude_modifiers": [0.0]}`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`state()` in the generator read only `{"Name", "Properties"}`, or the id alone,
so noise settings with `{"id", "properties"}` failed the world start with
`invalid generator resources: block state`. That is the form 26.3 uses and the
one `horizon-port-pack upgrade-datapack` writes for any non-default state. Decode
these two fields with `BlockStateJsonCodec`, as the rest of worldgen does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A run's state ids count every change its server made since the login, so a click numbered 53 in one
run and 55 in the other were both up to date. hotbar_swap now also taps a number key with a swing at
nothing, where vanilla sends the slot only on the next tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.pick aims along Entity.calculateViewVector: float radians through Mth.sin and Mth.cos,
the 65536-entry table. Horizon aimed with exact double trigonometry, which moved the hit point a
placement sends (ServerboundUseItemOnPacket's cursor) and an interaction's point by up to ~1e-4 of a
block. The wire stream harness's bridge scenario showed it: same position and look, cursor z
0.7173712 from vanilla, 0.7173119 from Horizon. Anticheats that recompute the cursor from the ray
compare it to vanilla's arithmetic. The other view_vector callers (projectiles, dragon, boats...)
are left for their own checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on the desktop at this tree: horizon-cosmetics fmt/clippy/tests (40) and the launcher's fmt/clippy/tests (695) green; pr_ready skipped because the desktop disk was under 45 GB. Point to watch: preview::render now takes a Camera (launcher is its only caller), and lib.rs will conflict by one line with the emote format PR #10091.
Shift-clicking a stonecutter result queued ui.stonecutter.take_result for
every item taken, and the server side played every copy at once: cutting
60 granite played the sound 60 times stacked. Vanilla's lastSoundTime
lets it through once per tick. The menu now queues each take sound once
per click (stonecutter, loom, cartography table), and lan_station_sounds
plays one sound per tick.
Fixes#10148
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Block.pushEntitiesUp` runs when these blocks gain the collision they add on
top. The farmland/path to-dirt path already emits the `push_entities_up` block
event; the three growth paths did not, so a body standing on them sank into the
new block.
🤖 Agent: Fixed to the 26.3 jar's DTOs: the region preference travels as its id (0/1/2) and the region as its nameId, so a Realm that has a preference parses again (it used to fail the whole RealmServer parse). preferredRegions reads regionDataList/regionName, and the configuration POST puts the slot under options with update_slot's RealmsSlotUpdateDto. New tests pin the POST body and the parsing; CI verify is green on this head.
Preserve the preceding scene color when a translucent shader draw reads
and writes the same buffer, rather than sampling a white fallback.
Reuse the existing snapshot mechanism to avoid attachment feedback.
Validated with Sildur’s Vibrant before-and-after captures and an
original asset-free GPU regression.
Co-Authored-By: Codex <[email protected]>
living_damage_pre and living_damage_post fire at the end of
LivingEntity.actuallyHurt, for players, mobs and the ender dragon. Pre is
told what is about to come off the health, once armour, enchantments,
effects and absorption have taken their part, and a mod may change it.
Post is told what came off, 0 when nothing did. The translated actuallyHurt
in horizon-world gets two hook points whose defaults do nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review, against vanilla (getAttackBoundingBox and getHitbox read the
current getVehicle()):
- A mob vehicle's box comes from the mob snapshot, which holds it as it is
after its own tick this tick, not from the last positionRider.
- The seat positionRider records names its vehicle, and is used only while
the mob rides that one: a dismount or a new vehicle no longer reads an old
vehicle's box or attachment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.1's client had every recipe and 27 of ATM10's mods listen to
RecipesUpdatedEvent to rebuild what they made of them. 26.1's client gets
only the recipe types a mod asks for, in RecipesReceivedEvent (NeoForge's
documentation, "Client-Side Recipes"), which recipes_received answers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: wear() still fires the durability trigger after Unbreaking, and the broken stat now comes before the rod. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The height probe and the draw share bundle_tooltip, so a hidden grid can't leave an empty box taller than its lines. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The tag is copied before name_mob uses it up, like handleInteract's itemStackBefore. Follow-up gap, not this PR: vanilla also fires the trigger with an empty stack on non-item successes. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: Same two calls finish_consuming makes, before the stack shrinks, as Consumable.onConsume orders them. drink_ominous_bottle awarded neither, so nothing counts twice. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: The pack-less structure tags are a OnceLock keyed on the first resource root, as StructureSpawns::shared is. location_structure_tags_hold_their_structures passes with prepared resources. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
🤖 Agent: On playhorizon (vscreen, lavapipe) every_screen_has_targets, pause_icon, erase_checkbox, controller_settings_pad and settings pass. The empty-label target can add a second target on a rect that already called target_as: drawn_targets dedups only against labelled widgets, not between targets. Nothing on main seems to do that today. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
The Wardrobe's still picture becomes a live view of the player's own skin
(both layers, slim or wide arms) wearing their items, drawn by the same CPU
renderer (horizon_cosmetics::preview) only when something changed:
- drag to turn (followed past the view's edges while held), scroll to zoom
up to 3x on the head, Reset view; hovering an owned item's card tries it
on (Nothing takes the slot's off) until the pointer leaves;
- items animate (swing, bob, spin) on the game's 20-tick clock with
Item::animate, as the game does, only while the window is in front and
not under reduced motion (HORIZON_LAUNCHER_MOTION=always keeps them going);
- horizon_cosmetics::viewer::Stage decides each frame: nothing changed, no
picture and no timer; at most 20 pictures a second while animating (one a
tick), 60 while dragging. Pictures are drawn at the view's size in points
(240x330, about 2 ms) and baked up to device pixels;
- preview::Camera holds the turn, tilt and zoom (with headroom above the
head for tall hats and back items), Draw::item_posed keeps items on posed
bones, and viewer::BodyMotion is the hook an emote plays through
(Viewer::play), for the emote format in flight (#10091).
The view is a GPUI view of its own, so a new picture rebuilds it alone;
the pointer is handled by the page's element around it, since GPUI Fast
hit-tests the page's own elements. "Turn" gives way to "Reset view" and a
hint, in every language.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Deferring the attempt to the event phase lets update_from_neighbour_shapes see the real level and ticks, as EndermanLeaveBlockGoal.tick does. leave_block_goal_uses_real_neighbor_shapes_and_keeps_air_placement passes with prepared resources. Still missing: canSurvive and the BLOCK_PLACE game event. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
frame, gui_render_post, tooltip_component, experience_orb_pickup and
item_dropped get the NeoForge event ATM10's mods listen to for the same
point: RenderFrameEvent.Pre, RenderGuiEvent.Post,
RenderTooltipEvent.GatherComponents, PlayerXpEvent.PickupXp and
ItemTossEvent. The bracketed notes say where a hook differs (#9921).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's Gui.setScreen calls Screen.clearFocus on the screen it leaves, so
a screen that comes back starts with no widget focused. Horizon kept the
focus a mouse click gave a button (the title's focused_button, pause_focus,
death_focus, the options' focus), so going back to the main menu redrew that
button highlighted (AbstractWidget.isHoveredOrFocused) with the pointer
somewhere else.
The front screen is now tracked with key_screen() (which the keyboard's focus
already uses for its own screen changes); when it changes, every menu drops
its click focus. The startup menu does the same for its title behind the
options, before the loading hands that title to the game.
Fixes#9273
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: native_fluid_at now allocates the block id String on every call, drains and tank queries included. That's fine for mod helpers, but worth an &'static str if a tick path ever calls it. The fragment stamps (tests.test_stamp_native_hooks passed in the union run). I ran the native_fluids tests on this head on playhorizon, 12 passed, the new dispatch test included.
- click-slot without a renderer clicks the menu itself (the vanilla driver's handleContainerInput); horizon-ctl's
clicks where the slot is drawn, which --headless never does.
- The reset's last teleport is the player onto itself: from the console `~` is the world spawn, which put the
player inside bridge's stone.
- BlockPos.asLong decodes with each part signed.
- eat cuts its two bites short: when a full one ends is the server's entity event, late on a busy server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.ensureHasSentCarriedItem runs in attack, interact, useItem, useItemOn,
releaseUsingItem, dropItem, piercingAttack and continueDestroyBlock only. A swing at nothing,
the start of a dig or an offhand swap sends no slot: a number key pressed in that tick goes out
at the start of the next one (MultiPlayerGameMode.tick). Horizon sent the slot whenever the tick
had a click. The wire stream harness's hotbar_swap scenario caught it: a tap of 2 with a swing at
the air sent SetCarriedItem before the Punch, a tick before vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.openMenu, when createMenu returns null for a spectator, sends
container.spectatorCantOpen to the action bar (red). Horizon's refusal in
spectator_open_menu and the chest-boat guest handler was silent.
🤖 Agent: merging. The layout lines only add keys to horizon-extras.txt; files without them load as before (every widget at its default place and style). Verify is green; stacked on #10077.
🤖 Agent: merging. Every widget reads local state only (no packets, no server queries); with every widget off, draw_pvp_hud returns after Hud::any(), so a player who never turns one on pays nothing. Verify is green; pr_ready --quick on playhorizon fails only the render crash:: tests that SIGSEGV under llvmpipe there, and crash.rs is untouched.
🤖 Agent: open_menu already counts the guest as an opener (container_opener_event), so the raise in lan_open_container counted them twice and stopOpen never reached 0, for chests as well as shulker boxes. The author's lan_e2e shulker_menu check pins the lid open, then shut, on both host and guest. Union on main dd675b574 green.
🤖 Agent: The server now sends one minecraft:vibration LevelParticles (count 1, no spread) with the listener's PositionSource and arrival ticks, and only draws locally when the window is the client, so a server-thread world doesn't draw it twice. Tested at a729b3cfe in a union on main dd675b574 (fmt, clippy, 4426 client tests green); this head only adds the author's main merge (test helper conflict) on top.
🤖 Agent: The launcher code is identical to what I tested in a union on main dd675b574 (launcher fmt, clippy -D warnings, 699 tests green). With no check before the build, a machine whose build programs ran before Smart App Control was turned on still builds (Mayonesu0's case 1), and only an os error 4551 leads the error with the explanation.
🤖 Agent: Merged main into this branch (the conflict was in command_exec_corpus.py: the transmute group now sits beside main's mob-effect and frame-map groups, and its recipe pack stays in ISOLATED_RECIPES). The result patch is now applied after the input patch, so a '!minecraft:damage' removal wins over inherited damage, as ItemStackTemplate.apply does. Union on main dd675b574: fmt, clippy, 4426 client tests green; CI parity runs command_exec.
Mob.getAttackBoundingBox widens the attacker's box on x and z to its
vehicle's before the melee reach is added, so a zombie on a horse, a
chicken jockey or a mob in a boat reaches as far as the side of what it
rides. LivingEntity.getHitbox starts a rider's box at its seat
(vehicle.getPassengerRidingPosition(this).y), so its legs can't be hit; for
a riding player that is 0.6 above its feet (Avatar.DEFAULT_VEHICLE_ATTACHMENT).
positionRider now records each mob rider's own vehicle attachment and its
vehicle's box (mobs, boats, minecarts, primed TNT, displays), which the
melee check and the slime's touch read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An Entry on the Servers list can name a host: its folder is then on that
computer, and Entry::command runs the same horizon-server arguments there
as `ssh HOST horizon-server --dir DIR ...`, with the system's ssh and the
player's keys (BatchMode, so no password is asked for or kept). Each
argument goes to the remote shell in single quotes, and a host that ssh
could read as an option is refused. remote::check asks the server there
for its status and says in plain words what is missing: a key, the host,
horizon-server, or a server in that folder.
Lists written before keep reading as they were; nothing on the page uses
remote entries yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
🤖 Agent: tests now check all four hosts' cargo commands on every runner (main's launcher red since #9479); launcher dry run 42072143059 green on windows-arm64 and macos-x64.
🤖 Agent: The stars come from the registry index, so the launcher makes no new GitHub API call, and both strings go through button()'s tr() and trf(). Until horizon-mods#285 lands, every page shows "0 stars on GitHub". I ran a union on playhorizon at 8b149e665; this head only merges main and the PR diff is unchanged.
🤖 Agent: Matches CommandSourceStack.sendSuccess(…, false) for every query. I ran the ignored a_silent_source_drops_query_results_but_still_stores_them with prepared resources and it passes. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
🤖 Agent: I pushed d4e13b4c0, which drops the "🤖 Agent:" markers from code comments and from the 0.2 prelude's COLOR_HELPERS text, since that text is composed into every 0.2 stage. Only 0.1's prelude digests are pinned, so they don't move. Added frame-identity now that #9900 is in. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
Vanilla reads network NBT with NbtAccounter's 2 MiB quota, which
charges each tag an estimate of its heap size: at least 8 bytes (EndTag,
the smallest; a byte tag in a list costs 13). Horizon counted only the
bytes read, plus up to 1,000,000 nodes, so a crafted server could send a
1 MB list of byte tags that vanilla refuses and have Horizon build ~1 M
32-byte Tags from it, about 16 times the memory.
- buf::Reader::nbt caps a network NBT value at NBT_QUOTA / 8 = 262,144
tags (NBT_MAX_TAGS). Every value vanilla accepts has at most that
many, so nothing vanilla accepts is refused, and one value's tags now
take at most ~8 MiB here.
- custom::validate checks component values with the same reader, so it
follows.
- Test: a list of 262,143 byte tags (with its root, 262,144) reads; one
more is refused although it is a few hundred KB.
A full port of NbtAccounter's per-type charges would be closer still;
this bound needs only its smallest charge, so it can't refuse what
vanilla takes.
Found in yahya-nusseibeh's review of Horizon (L2).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
tools/ first on sys.path made unittest discovery import tools/test_world_spawn_parity.py for
tests/test_world_spawn_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tab_list::visible with the HUD hidden answers false for a held key with two
listed players and for a local server with a list objective, and the
scoreboard's LAN test asserts the same through the game's call.
Hud.extractRenderState calls extractTabList inside `if (!isHidden)`, so with
the HUD hidden (F1) the player list does not draw at all, whatever the
player-list key or the list objective says.
Horizon's hud_layers only skipped the tab list under F1 when the chat screen
was open: with no screen, holding Tab drew the list over the hidden HUD. The
gate is now in tab_list::visible, next to the key and the local-server rules
it already applied, so every caller gets it.
After a refused login the connection stayed open, so within the 30 s
login window a client could send as many AUTH packets as it liked: with
the 16 connections allowed at once, thousands of guesses a second
against an RCON port on 0.0.0.0, each adding a supervisor log line.
Vanilla does the same, but this module already departs from vanilla for
safety (constant-time comparison, login deadline, client cap), and this
belongs with those.
- A refused login (wrong or empty password) answers as before, waits
REFUSED_PAUSE (1 s) and closes the connection. Its place stays taken
meanwhile, so the client cap also bounds how fast passwords can be
tried: at most 16 guesses a second instead of thousands.
- A command before the login is still refused without closing, and the
wire protocol is unchanged; standard RCON clients connect again after
a refusal.
- Tests: a wrong and an empty password each end their connection after
the pause; a failed login after a successful one does too. The pause
is a Limits field so tests use 200 ms.
Found in yahya-nusseibeh's review of Horizon (L3).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A focused test drives the public block_entity_accepts_fluid dispatch, as the game does: a water tank that refuses a mod's fluid leaves both insert passes empty and offers native_move_fluids no room, while water and a tank with no hook still fill.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The server runs two async workers and keeps SQLite on tokio's blocking
pool (lib.rs, `blocking`), but /hub/vote, /hub/votes and /hub/report and
the token-protected /v1/export/* still called app.store() on the
workers. While a blocking-pool task holds the store mutex (a large batch,
the daily upkeep), two votes or reports from different addresses park
both workers on it and every handler stops answering, /healthz
included; per-address limits don't help, it takes two requests.
/hub/votes also held votes_cache's lock while waiting for the store.
- hub::vote and hub::votes run their store calls through blocking();
votes reads the cache, releases its lock, counts on the blocking pool
and stores the result after.
- intake::receive checks the day's count and queues the report in one
blocking() call, under one store lock as before; report marks the
issue filed through blocking().
- export::benchmarks and export::hardware read through blocking().
- A failed blocking task answers 503 "try again later" through each
module's own refusal (the hub's keeps its CORS headers).
Behaviour is unchanged otherwise: the crate's 58 tests pass as they are.
Found in yahya-nusseibeh's review of Horizon (M1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Part of #9996.
The terrain noise (old_blended_noise) is a lerp between two 16-octave
smeared Perlin stacks. Lerp reads its first branch only where alpha is
not 1 and its second only where alpha is not 0, and in the overworld
about 37% of the coarse points have alpha 1 and 50% alpha 0. The lerp
now passes each branch a bit per Y of each column it reads, and a noise
volume skips the others (DensitySampler::sample_needed,
Noise::add_to_needed; samplers without it still sample everything).
When a column's Y cells change at every Y, as in the stacks'
high-frequency octaves, four Ys in four cells now go through the same
vectorized lane code as four Ys in one cell, each lane with its own
cell's corner terms. Gradient lookups are a loop rather than
array::map, which LLVM had left out of line.
Every value is computed by the same operations in the same order:
snapshots match byte for byte and the tests compare against the
unbatched, unmasked loops.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
horizon-port-pack read jar entries with read_to_string/read_to_end and
no limit. zip stops an entry only at the size it declares, which can be
4 GiB (more with zip64), and the launcher runs check::check in its own
process on every Java mod a mod-browser search lists, without a click.
So one published mod with a highly compressible mods.toml could abort
the launcher of everyone whose search showed it. A 522 KB jar whose
mods.toml inflates to 512 MiB made `horizon-port-pack check` reach
594 MiB and abort; with this change it peaks at 9 MiB and refuses the
jar.
- crate::entry: read()/read_text() with a per-entry cap, and a Budget
for a running total over one pass of an archive, so many entries
under their cap can't add up either. Limits: 16 MiB per text entry
(JSON, TOML, the manifest), 64 MiB per file kept as it is, 1 GiB per
archive pass. Only bytes actually read count; an entry declaring more
than what is left is refused unread.
- Every zip read in the crate goes through it: jar::read's metadata
(a too-large file refuses the jar), carry's namespace scan and file
copy, worldgen's piece counts, structure-set and biome tags,
processors, upgrade::read_pack and import's JSON index. check.rs kept
its 16 MiB per-class cap and now also refuses a jar whose classes
together unpack to more than 1 GiB (a thousand entries just under the
per-class cap was the same attack).
- Tests: the cap and the running total (declared or not, refused
entries counting), UTF-8, and check() refusing a small jar whose
mods.toml unpacks past 16 MiB.
No parity change: real mod files are far under these limits.
Found in yahya-nusseibeh's review of Horizon (H1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
It drives a vanilla client on a virtual screen for ~15 minutes: no automatic selection on every client change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Mine: while an emote plays, the game tells the cosmetics service, signed in as for "Show me
as Horizon" and only while that is on: `POST /v1/emote`, with a session kept an hour
(`account::Kept`). A loop is said again every 30 s.
- Theirs: `horizon_cosmetics::Feed`, a thread, asks `POST /v1/emotes` which Horizon players in
view play one, at most once a second and only while some are in view, then fetches the item
files once. On failure it waits 15 s, doubling up to 10 min. Their models play the emote from
where it is; a player who moves ends theirs at once.
- Follows "Other players' cosmetics": with anything but Shown, nothing is asked and nothing is
drawn.
- The wheel: the account's emotes (`GET /v1/me`'s `emotes`), read when it opens, at most every
10 minutes.
Nothing reaches game servers; vanilla clients see nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher/Cargo.lock pinned rustls 0.23.45 (through ureq 2.12), which
cargo audit flags with RUSTSEC-2026-0346 (client session Debug impls
expose TLS secrets), -0347 (buffered data sent to the outer server after
an ECH rejection), -0348 (Brotli certificate decompression) and -0349
(post-quantum downgrade from an unauthenticated ServerHello), all fixed
in 0.23.46. The launcher carries Microsoft sign-in tokens over this
stack.
- `cargo update -p rustls --precise 0.23.46`: rustls alone moves.
- cargo audit on the new lockfile: no vulnerabilities (8 informational
warnings: unmaintained or yanked crates, unchanged).
Found in yahya-nusseibeh's review of Horizon (L4).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
From review, against vanilla:
- Sensor.isEntityTargetable is range(16) from feet to feet, a sphere; the
nearby scan alone was a cube.
- Panda's PandaPanicGoal and an adult PolarBear's panic use
#panic_environmental_causes as well; their own copies of it lacked freeze
and sulfur_cube_hot. They now share armadillo::environmental.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mod page opened the project named by HORIZON_LAUNCHER_PROJECT (a
screenshot aid) and then called std::env::remove_var so it wouldn't open
again. By then the download, check and keyring threads are running, and
remove_var races getenv on any of them (glibc's DNS resolution
included): unsound, which is why edition 2024 makes it `unsafe`. The
launcher is on 2021, so it compiled silently.
- A static flag remembers that the page was opened this run; the
environment is no longer written. Same behaviour: the page opens once,
after the first search that finds it.
Found in yahya-nusseibeh's review of Horizon (L5).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
A busy server ran hit_target's reset teleport on the scenario's first tick: the vanilla client's first move
said off the ground and every later move differed. The reset now ends with a teleport, and the scenario waits
until the client has accepted it and ticked 40 more times.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The wheel: Emote Wheel (`key.emote`, B by default, unbound on a controller; rebindable in
Controls) shows up to 8 emotes around the middle of the screen. To play one: hold the key and
release on an emote, click one, or press its number.
- The emote cancels (and fades out) on movement, an attack, mining, using an item, being hurt,
riding, sleeping, gliding or sneaking. Only a standing player starts one.
- From first person, the camera turns to the front view and eases out from close for the emote,
then comes back.
- Bones: `anim::setup` lays the emote over vanilla's animation, keyed bones only.
- Props: drawn with the cosmetics, growing in and out with the blend.
- `horizon-ctl emote [ID|stop]`.
- HORIZON_COSMETICS_LOCAL: the wheel offers the folder's emotes, and mannequins play its first
one (the art and the bench).
Visual only: nothing reaches the game server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9479 made the native-mod and toolchain tests build their Toolchain from
`rust_toolchain::host().unwrap()`. On the macOS x64 runner there is no
managed host (None: 7 panics); on the Windows ARM64 runner it is the ARM64
host, whose cargo gets its target's rustflags as `--config` before the
subcommand, which the tests' argument layout didn't expect.
The code is right on both: the rustflags are scoped to
target.aarch64-pc-windows-gnullvm and only the ARM64 host adds them. The
tests now run each check for every host the launcher ships a toolchain
for, expect the host's own arguments first, and pin that only the ARM64
host adds any.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server built its world tables from the content root only when the
world had data packs; without one it kept the tables loaded at startup,
so a switched mod's recipes and tags never applied (seen in game:
/recipe give still said Unknown recipe). After a live switch the tables
are built from the new root. An install that waits for a restart says so
on the project page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings the branch up to date; the parity job's last run failed in the
runner's preparer (.local/.horizon-prepare.lock missing), as it did for
every PR between 14:17 and 14:41 UTC.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An item of the new `emote` slot carries an `emote` block: per-part tracks of keys (rotation,
offset, a prop's scale) eased smooth (Catmull-Rom), linear, in_out or step, one-shot or looping,
faded in and out over `blend` ticks. `Item::parse` checks its limits (200 ticks, 16 tracks,
32 keys a track and 192 in all, 360° turns and 8 px moves, the root 720° and 16 px, props'
reach through every key). `Emote::pose_body` poses the six bones (keys replace a bone's
rotation, keyed bones only), `pose_props` the props, and `Playing` gives an emote's time and
weight, its cancel included. Visual only, nothing random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`screen_draw::paint`: an inner scissor draws to the overlap of both, a scissor
with nothing around it is cut to the screen, and one that misses the outer
clip draws nothing while the outer clip takes over again when the inner closes.
`GuiGraphicsExtractor.ScissorStack` pushes each clip intersected with the one
around it (the screen while there is none, `ScreenRectangle.empty()` when they
miss) and pops it back when it closes. `screen_draw::paint` kept a single clip
that each `Draw::Scissor` replaced: a nested scissor stopped the outer clip
applying at all, and the outer one never came back after the inner closed.
`paint` now keeps the stack: `Draw::Scissor` pushes the intersection with the
enclosing clip (or the screen) and `Draw::NoScissor` clips what was drawn since
and pops back to the enclosing clip. `intersect` is `ScreenRectangle.intersection`.
`RegionFileVersion.configure` was the row that named this gap: read from
server.properties and never applied. `Compression::configure` now applies it
from the dedicated server's startup and `compress_records` asks
`Compression::selected()`, so the row says `port` with what each half does.
NautilusAi.findNearestValidAttackTarget picks the closest #nautilus_hostiles
in water from NEAREST_VISIBLE_LIVING_ENTITIES, the sensor's list of what the
nautilus can see; Horizon took any pufferfish within 16 blocks, through
walls. Its cooldown and 1 in 2 are drawn from the level's random
(level.getRandom()), not the nautilus's own.
Armadillo.actuallyHurt panics (rolls out) on #panic_environmental_causes,
which also holds freeze and sulfur_cube_hot; Horizon listed the other six.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two cases, one per half of the port:
- `compression::tests::the_selected_region_version_follows_the_property` walks
the four names `RegionFileVersion` registers (`gzip`, `deflate`, `none`,
`lz4`) against `Compression::selected()`, checks the default before any is
set is `deflate`, and checks that `zstd` and the wrong case `LZ4` are refused
and leave the selection alone, as vanilla logs the value and keeps the one it
had.
- `save::tests::a_chunk_is_written_with_the_selected_region_version` runs the
function the save path calls, `compress_records`, once per name, and asserts
the record's codec is the selected one and that its bytes decode back to the
same chunk.
With `compress_records` back on `Compression::Zlib` the second test fails with
`left: Zlib, right: None` ("written with none") and would then fail on `lz4`
with `left: Zlib, right: Lz4`.
Also here: the helper-parity test next door now dates its expected records with
`Compression::selected()` rather than `Zlib`, so it keeps checking that the
helpers agree with one thread whatever version the process selected instead of
pinning the default.
Vanilla's `Main.main` hands `server.properties`' `region-file-compression` to
`RegionFileVersion.configure` right after the settings are saved, and every
`RegionFile` written afterwards uses the version it selected (`getSelected`):
`gzip`, `deflate` (the default), `none` or `lz4`. Horizon parsed the key into
`ServerProperties.region_file_compression` and nothing read it, so every chunk
region was written as `Compression::Zlib` whatever a server asked for.
`Compression::configure(name)` and `Compression::selected()` are the two halves
of `RegionFileVersion` over the same kind of process-wide selection, starting at
`VERSION_DEFLATE`, with `REGION_FILE_NAMES` in the registration order vanilla's
error message lists. A name is matched exactly, as `VERSIONS_BY_NAME` does; an
unknown one changes nothing and says so. The dedicated server's startup calls it
where vanilla does, before the EULA check, and logs vanilla's line for a value
the table does not know. `save.rs::compress_records` encodes each record with
the selection, so the chunks the save path writes carry it; the read side needs
no change, as `Compression` already decoded all four and the header's version
byte names each chunk's codec.
focus_on looked a widget up before stepping, so a row scrolled out of view
ended the case: it now steps until the row is drawn. optimize_world's job
finished before the case could look on a fast machine: 60000 linked regions
take long enough and cost almost nothing. A tiling compositor sizes the hidden
window too, which fails every case that scrolls, so the harness warns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RegionSelectionPreference travels as its id (0, 1, 2) and the region as its nameId, so a
Realm with a preference no longer fails to parse as a String. PreferredRegionsDto reads
regionDataList / regionName / serviceQuality, RealmsConfigurationDto posts the slot under
options with update_slot's RealmsSlotUpdateDto (worldTemplateId, worldTemplateImage), a Realm
without a preference is automatic by owner (RegionSelectionPreferenceDto.DEFAULT), and the
labels use the realms.configuration.region* keys vanilla's settings tab shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_wire_stream_parity.py: a Horizon client and a real vanilla 26.3
client join a vanilla server through tools/net_capture.py and play the same
tick-indexed scenarios (walk, sprint-jump, strafe, w-tap, sneak, hits,
sprint combos, block-hit, rod, bow, eat, bridging, inventory, hotbar swaps,
a teleport during a dig). tools/wire_stream.py cuts each serverbound stream
into client ticks (ClientTickEnd ends one) and decodes the packets; every
tick must hold the same packets with the same fields.
Horizon plays a scenario from HORIZON_TICK_SCRIPT (game/tick_script.rs),
before each client tick; vanilla from VanillaTickScript.java, which hooks a
fixed point of Minecraft.tick (Tutorial.onLookAt) and presses the keys
through KeyMapping.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.isShiftKeyDown reads input.keyPresses, which LocalPlayer.aiStep
sets from the keys: what the last ServerboundPlayerInputPacket said.
Minecraft.handleKeybinds runs before it, so a sneak pressed in the frame of
a right-click counts from the next tick. Horizon read the live Shift key for
ServerboundInteractPacket's secondary flag and for every secondary-use
check (isSecondaryUseActive): a click in that frame sent a sneaking
interaction a server saw before the sneak itself.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LocalPlayer.sendChanges sends a passenger's look, then the vehicle it
controls, and only then its sprint (sendIsSprintingIfNeeded); riding a
vehicle it doesn't control, it sends no sprint at all. AbstractBoat.tick
sends the paddles from the boat's own tick, before its passenger's
sendChanges. Horizon sent the sprint first and for any vehicle, and the
paddles last.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MultiPlayerGameMode.tick, first in Minecraft.tick and run with a screen
open too, sends the slot chosen since the last tick
(ensureHasSentCarriedItem); LocalPlayer.sendChanges sends no slot. Horizon
also sent it at the end of the tick that changed it, before the input and
the move: a number key pressed in a tick went out one tick early, on the
other side of the movement packet a translating proxy (ViaVersion to an
older server) turns into that tick's flying packet. An action in the same
tick still sends the slot first, as ensureHasSentCarriedItem does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleMovePlayer accepts the teleport, then runs
MultiPlayerGameMode.stopDestroyBlock: a dig the teleport interrupts (an
anticheat's setback while mining or bridging, a /tp) sends
ABORT_DESTROY_BLOCK and starts the attack strength again. Horizon dropped
the dig without a word, so the server kept counting it.
BlockStatePredictionHandler lives in the ClientLevel: handleLogin (a proxy's
server switch) and handleRespawn into another dimension make a new one, so
the use and dig sequence numbers count from 0 again. Horizon counted on for
the whole connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mapping bug report needs to know whether gilrs found an SDL row for a pad
and which id such a row would need. pad status now lists mapping_source
(sdl, driver or none) and uuid (32 hex digits, the id an SDL row starts with).
With splitscreen on, PadRouter holds the gilrs reader, so pad status said
backend:false and listed no controllers, unless the reader was lent to P1.
The router now leaves what it sees for the status of each game that has a
control socket; that list is only built when one is open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add to basket on the chosen card, the basket's lines and total with the
service's rules (owned items and items inside a set in the basket cost
nothing), a gift to a friend looked up by name, the Supporter badge's
amount within the catalogue's range, and "Ask whoever pays before you
buy. Refunds within 14 days for any reason." by Pay, which starts
Paddle's checkout on the service and opens playhorizon.dev/shop/pay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The catalogue's items, sets, collections and the Supporter badge as cards
with their prices, the chosen card tried on the account's own skin, and
the founder set's claim (one click where Paddle doesn't sell, else
Paddle's free checkout in the browser). No badge on the tab, it never
opens by itself, and a catalogue that doesn't load says so in one line.
The Wardrobe's one shop button now opens this tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 of ATM10's mods listen to RenderLevelStageEvent to draw in the
world at a stage of the frame. Main has three hooks for that, none
labelled:
- world_submit (fragment world_canvas): quads and text right after
the particles, before the hand;
- world_primitives (fragment): lines, boxes, beams and text at the end
of the world pass, before the hand;
- world_drawn (API 58): the drawn world's colour, depth and matrices,
once the whole level is drawn.
Each gets "neoforge": "RenderLevelStageEvent (...)", with where it
draws in words, so horizon-port-pack events maps the 81 mods to them.
world_drawn also gets side client and cancellable false; the fragments
had theirs. NeoForge's user documentation doesn't cover this event,
so the labels use only its name.
Fixes#10036
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Mods page froze every content mod's toggle while a world was open.
Those with only assets and data now reload live, so their toggles stay
active in a world this game serves; the others keep the lock. Each mod
is classified once when the mods are listed, and a native mod's toggle
no longer triggers a resource reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Extras and PvP & HUD measure each label in the screen's font, with its first values; one wider than a half-width button takes the whole row, so 'Keep channels for UI sounds' and 'Scroll wheel changes settings' no longer slide, in any language. verify green at this head; no caption, key or default changes.
PR-comment markers do not belong in source, and the 0.2 prelude text is
part of every composed 0.2 stage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: one-word fix (closure gets move) for the Windows build error E0373 that #10003 left on main; verified with cargo check --target x86_64-pc-windows-gnu --bins --tests (mingw) on the desktop at this exact head.
The note kept 0.1 at least until 0.2 leaves experimental, which would keep
today's HDR path (marks, the 16-bit unorm frame, Smooth Gradients) alive for
0.1 packs alone. The shader API is experimental, which stability.md lets
change in any release without notice. So step 5, which turns 0.2 on by
default, also stops serving 0.1: 0.1 packs are refused with stability.md's
message, the registry lists 0.2 packs, and Golden Hour's 0.2 port becomes the
example. Later experimental lines follow the same rule. The shader API page
tells pack authors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A --control-socket path over the system's limit was only found when the
game tried to listen, after loading resources: it logged the raw SUN_LEN
error, wrote a crash report and exited 1, leaving startedCleanly:false so
the next start reset fullscreen. It is a bad command line: exit 2 with a
plain message, before the game folder, options.txt or resources are
touched. horizon-ctl launch refuses it as usage too, without starting a
game or creating a log beside the socket. A .json endpoint is TCP and has
no such limit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge documents ServerStartingEvent as fired after
ServerAboutToStartEvent and before ServerStartedEvent. Horizon has
server_about_to_start at the start of loadLevel, and world_lifecycle's
WorldOpenReady once the open has finished, before the tick loop.
Nothing of Horizon's runs between the two NeoForge events, so
WorldOpenReady answers both (30 ATM10 mods listen to Starting).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1.21.11 sends instrument, provides_trim_material, damage_type,
chicken/variant and zombie_nautilus/variant as EitherHolders (a flag,
then the entry's holder or its ResourceKey), as it does jukebox_playable;
26.1 made them plain holders. Horizon read the flag as the holder, so a
goat horn's slot came one byte long and the entity data around it one
byte short ("could not translate 1.21.11 packet 20/101").
The flag is read for all six now: a holder reads on as the pinned one,
a key (no pinned numbering for it) drops the component.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
From review, against vanilla:
- getActiveItem().get(ATTACK_RANGE) includes the stack's patch: an added
minecraft:attack_range (the codec's defaults: max_reach 3, min_reach 0,
mob_factor 1) sets the reach, and !minecraft:attack_range takes a spear's
away.
- AbstractCubeMob.dealDamage calls isWithinMeleeAttackRange, for the player
it touches and for iron golems; it repeated the default formula.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The catalogue's shop fields (currencies, prices, sets, collections, the
Supporter badge), the player lookup and the web checkout in the wardrobe
client, and the basket's price rules in shop.rs, as the website applies
them. Nothing shows them yet: the Shop tab comes next.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: registry tags come from known_index(), so tags fetched before the first search finishes stay empty for that kind until the launcher restarts; searches always load the index first, so in practice the row is filled.
🤖 Agent: "from Horizon" now follows browser::horizon::official (authors ["Horizon"] + horizon-rust source, the site's isOfficial), read from the already-loaded index; the instance mod list's install-source label is left as is.
🤖 Agent: launcher-only (plus docs), green launcher rustfmt/clippy/669 tests on the desktop at this head, i18n coverage test included. The one thing it keeps on purpose: Claim for free where Paddle doesn't sell, since the site sends those players to the Wardrobe.
🤖 Agent: verified on the desktop (pr_ready --quick at this head: clippy, 1026 tests incl. the new lift test) and on the ThinkPad's GPU (before/after shots). Point to watch: the lift is only ever added when an item is drawn on the body, so tags without tall cosmetics keep vanilla's exact 2.3 (tested); the extra per-tag cost is one max over ≤5 floats inside the existing cosmetics lookup.
Mob.isWithinMeleeAttackRange takes the active item's attack_range when it
has one: the box is inflated by effectiveMaxRange (max_reach * mob_factor,
2.25 for every spear) instead of the default sqrt(2.04) - 0.6, and a target
within effectiveMinRange (min_reach * mob_factor, 1.0) is out of range.
Horizon always used the default, so a mob given a spear without a
SpearUseGoal (a vindicator, say) hit what stood right beside it and missed
what the spear reached.
New mob AI scenarios: a vindicator with a spear against a player out at the
spear's reach and right beside it, and zombies with a spear (their
SpearUseGoal; the probe now names it as vanilla does).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: fixes the red launcher i18n tests that #10003 left on main (reuses already translated strings); fmt, clippy and the 579 launcher lib tests were green on the desktop for this change.
Every language file must hold every string the code asks for; the new wording needs
translations from the language owners, so this keeps the existing text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A registry result, its page and its Installed row said "from Horizon" for
every project in the registry, so a contributor's mod read as if Horizon
made it. Now only Horizon's own projects say it (author Horizon, source
under github.com/horizon-rust: the site's "Official", the rule
browser::horizon::official already used for the trust line); a
contributor's reads "by <author>" alone. Modrinth and CurseForge keep
"from <source>".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seven more hooks whose doc comments already name their NeoForge event:
screen_render_post (ScreenEvent.Render.Post), the five screen input
hooks (ScreenEvent.MouseButtonPressed.Pre, .MouseButtonReleased.Pre,
.MouseScrolled.Pre, .KeyPressed.Pre, .CharacterTyped.Pre), all client,
and item_picked_up (ItemEntityPickupEvent.Pre, server), whose cancel
stands for setCanPickup(TriState.FALSE), with no forced pickup.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Checked against SpawnEggItem.useOn/spawnMob, EntityType.spawn/create and SpawnEggItemBehavior. I pushed d7e7b9273: the spawn yaw is wrapped, Mob.finalizeSpawn's triangle and nextFloat come from the level random, and the dragon seeds its own random. The author's dispenser height (16x8 box) and growl are kept. Known gap: Horizon keeps one dragon per level, so a second egg does nothing.
🤖 Agent: Checked against HangingEntityItem.useOn, ArmorStandItem.useOn, EndCrystalItem.useOn, MinecartItem.useOn and EnderChestBlockEntity.startOpen. The armor stand now falls up to two blocks (getYOffset with movedUp). I pushed ada2c6b81: drop_offset keeps its three arguments and drop_offset_from takes movedUp, so #9859's new caller compiles whichever lands first. Main is merged in, and use_item_on keeps the wake, used-stat and placement cases.
The host's open unpacks the loot table it still holds; the guest's
(lan_open_container) read the slots without it, so a LAN or splitscreen
player met an empty container that kept its table, and
player_generates_container_loot never fired for them. unpack_opened_loot
now runs for the acting guest too.
ChestBlock.getMenuProvider unpacks both halves of a double chest; only
the clicked half was unpacked. After its own roll is consumed, the
partner's is unpacked too (the consumed own roll ends the recursion).
Every documented key: particle, fog and sky colours, potions, spawn eggs,
map, sheep, collar and dye colours (with their old names), text and its
colour codes, the loading screen, clouds, xporb.time, palette.format and
palette.block.* assignments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod hears the window's player log in to a server, single
player's own included: native_client_player_network_logging_in(), at
the end of the Login arm (ClientPacketListener.handleLogin), after the
new client level. Respawns and dimension changes don't log in again.
Horizon makes the player with the server's first position, so
native_player() is None there; native_send_packet reaches the server.
client_disconnecting, which main fires before a connection is
discarded (Minecraft.disconnect), gets the catalog label
ClientPlayerNetworkEvent.LoggingOut, so horizon-port-pack events finds
it. The docs describe the pair.
Checked in game with horizon-ctl and a local probe mod: one login on
opening a world and one on reopening it, none on a trip to the Nether
or a respawn, and a logout on Save and Quit and on quitting the game.
Fixes#10008
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Horizon's registry, Filters already listed the kind's categories. Now:
- A Tags row lists the tags the kind's projects carry (the 30 most used,
from the index the search read): a result has every tag picked, as for
categories, and the search words narrow them further.
- Categories read as playhorizon.dev names them ("Game mechanics", "World
generation", "GUI"), in the registry's order, translated where the
launcher's tables have the words.
- While the Filters panel is closed, every picked category, tag, loader,
version or side stays in view as a chip with a cross that removes it,
then Clear filters (which keeps the sort).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Edit HUD on the PvP & HUD page (in a world) or the HUD editor key
(unbound at first) opens a screen over the game: each widget in a frame,
dragged anywhere with snapping to the screen's edges and middle and to
the other widgets, the wheel resizing it, a right click sending it back
to its corner; buttons for text colour, shadow, boxes and opacity, Reset
and Done. Widgets with nothing to show yet stand in by their name. The
layout is saved to horizon-extras.txt as it changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each HUD widget can leave its corner for a place kept as fractions of the
screen's free room (so it stays on screen at any size) and a size from 50
to 200%, saved as hudPlace lines in horizon-extras.txt only once moved.
The page gains the boxes' opacity, the text colour and the text shadow.
Snapping (screen edges, middle, other boxes) is here for the editor.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Six more widgets on the PvP & HUD page, off by default, each from the
client's own state: the frame counter, presses of the last second, the
movement and mouse keys while held, the local time, the distance of the
last hit the crosshair's ray measured, and horizontal speed sampled from
the player's position. Each caches its lines and rebuilds them only when
the shown value changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolved the one conflict, in game/lan_particles.rs: test_hub keeps the
shared helper the vibration test needs, and its guests now carry main's
Joined.expected field and bind through NetConfig::loopback. The
sculk_vibration LAN check survives main's rewrite of tools/lan_e2e.py.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Projectile.mayInteract asks mob_griefing for every owner that isn't a
player, and canBreakBlockInAdventureMode lets no non-player owner break
a block without can_break. An owner UUID that names a loaded armor
stand, boat or other non-mob entity was treated as no owner, so a
burning arrow lit TNT and campfires with mob_griefing off. The arrow now
asks the level for the UUID, as getOwner does.
Fixes#9999
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: launcher-only fix (Control Panel Uninstall opened nothing: the CLI parser refused 'uninstall'); fmt, clippy and tests green on the desktop at this exact head; the Windows build runs in CI.
🤖 Agent: Video Settings... opens the graphics screen (as Sodium's), which gains vanilla's Video Settings rows it lacked and the picture extras; FOV and View Bobbing stay where vanilla has them; Horizon Settings and Extras drop their duplicate ways in. A test walks every menu and pins one place per setting and one way in per page. verify green at this head; saved keys and defaults unchanged.
🤖 Agent: Shader packs (both kinds) move to a new Shaders page of the graphics screen; Horizon Settings loses its second way into the pack list. Shadow Cache moves from Performance to Shaders. verify green at this head; no saved key or default changes. Bottom of the settings-unify stack (#9979, #9980 follow).
The shulker_menu check now keeps host and guest screenshots of the lid before and after the guest closes the menu, so a stuck lid is visible as well as asserted.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Keep stale home memories while checking the current HOME POI before
counting a loaded bed claim as a village center. Preserve unavailable
claims and existing job/meeting paths.
Co-Authored-By: Codex <[email protected]>
From review of the previous commit, against vanilla:
- Enderman.canRandomlyTeleportTo refuses ground that holds water (a
waterlogged slab, leaves or roots).
- containsAnyLiquid and findBlocksIn scan the same closed range of cells.
- Both teleport sounds play at the feet (Entity.playSound).
- The game's sculk layer now hears GameEvent.TELEPORT from a mob.
- Enderman.hurtServer teleports inside every hurt, not only the rain's: the
fire, lava and block hurts of baseTick and a fall's, and a NoAI
enderman's, which never teleported. A lingering cloud's indirect_magic
draws the chance too; only a thrown potion skips it.
New mob AI scenarios: enderman_rain_waterlogged (red on the previous commit,
it landed on the slabs at tick 2) and enderman_rain_noai.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlayer.die posts entity_die at the player, and with a kill credit
awards killed_by for the killer's type. A creeper, sulfur cube or wither
whose own blast kills the player is still credited, as vanilla's player
dies inside the explosion: ExplosionSource carries the mob's type.
New lan_e2e check death-credit.
Fixes#9955
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
When the end portal frame ring is completed by placing the last eye of ender, vanilla destroys blocks in the 3x3 portal opening so that their drops and break effects occur instead of silently deleting them. Also awards minecraft:used for minecraft:ender_eye upon insertion into a frame.
Co-Authored-By: Antigravity <[email protected]>
Tested on Windows 11 with Smart App Control on (VerifiedAndReputablePolicyState
1): a build whose programs already ran before it was turned on still
succeeds, so refusing every build when the registry value is 1 stopped
builds that work. The check before the download is gone; a build that hits
os error 4551 still leads its error with what to do.
The message is translatable now, and says Windows also blocks some of the
toolchain's own programs (dlltool.exe was refused in a player's build), not
only the ones compiled on the computer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y255SLkXwxkdNVZnS8dNbf
main() ran the CLI parser before looking for the setup window, so
`horizon-launcher.exe uninstall` was refused as an unknown subcommand, with the
error going to a console Control Panel's start doesn't have: no window, nothing removed.
The setup modes are now decided first (setup_args::classify, pure and tested on every OS).
The uninstaller also asks a launcher that is still open to quit before removing its files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options > Mods' switch now reaches the trait mods' dispatch: the game skips
a turned-off mod's Hooks from the next call, on the server thread and in
the window, and turning it on again resumes it with no rebuild while it is
in the build. Registration at start is untouched, so its content stays
registered; RenderHooks keep drawing until a restart; mixin code still
checks native_on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content root is built per set of mods (.horizon-cache/mod-content/<stamp>),
so a running game keeps reading the root it was given while a switch
builds another; roots unused for a day are removed. Turning a mod on or
off in Options > Mods, or installing one from Browse Mods, applies at once
when the mod has only assets and data: the client reloads its resources,
then the server reloads its data packs as /reload does and resyncs its
players. A mod that adds blocks, items, mob effects, entity or menu types
or changes world generation keeps loading at start, and the Mods page
says to restart.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options > Mods > Browse Mods lists the registry's content mods for this
game (the index the launcher reads), searchable by name, tag and category,
with a page per project: authors, licence, categories, description and
changelog, Install or Update, its switch once installed, and its page on
the website. An install is checked as the launcher's (size, SHA-256,
signature, manifest against the index) and recorded in mod-browser.json,
so the launcher lists, updates and removes it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What content mods can switch live (assets and data), what needs a world
reload (registries, worldgen), the in-game mod browser, and the native
fast path: every installed native mod built in and switched at run time,
with the dylib and wasm paths left to arismoko's fork.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In vanilla (\LivingEntity.tick\), a passenger's body yaw matches its vehicle's rotation
(or living body yaw), clamped to keep head yaw within [-85°, 85°], and does not update
limb swing / walk distance from vehicle motion.
Furthermore, \Minecraft.handleKeybinds\ blocks opening the inventory (\keyInventory\)
while in a portal transition (\handlePortalTransitionEffect\), preventing repeatedly
reopening the inventory screen while standing in nether portals.
- Compute ridden vehicle yaw for boats, minecarts, mobs, and cushions in \perspective.rs\.
- Align rider body yaw to vehicle rotation, clamped to [-85°, 85°] of head yaw.
- Disable walk distance accumulation when riding so the player model does not swing limbs.
- Guard \KeyCode::KeyE\ in \game.rs\ and \
emote.rs\ when in portal transition.
Co-Authored-By: Antigravity <[email protected]>
🤖 Agent: The host now forwards the mob game events vanilla's mobs post (prime_fuse, eat, container_open and close, entity_place, and entity_action beyond the armadillo's), and the sculk harness gets creeper and sheep scenarios that main fails. They run last so the catalyst scenarios' entity ids don't shift.
🤖 Agent: PadRouter now adopts P1's gilrs reader and hands it back, so routing no longer opens one reader while another closes, which upset KWin's holder count. The router's reader also gets EXTRA_SDL_MAPPINGS (8BitDo Ultimate 2). It merges cleanly with #8233's triggers.rs.
🤖 Agent: The new junction is made as current.switching before the old one goes, and the old one is restored if the rename fails, so a failed mklink no longer leaves the server with no current release. Paths are quoted through raw_arg, and % or a quote is refused. The author's Windows CI run built and tested it.
🤖 Agent: the catch now rolls from the table's random sequence (minecraft:gameplay/fishing, else the level random), as vanilla's LootTable.getRandomItems(LootParams) does, and test_fishing_rng_parity checks the hook's draws against a 26.3 server. I removed four tests that compared LegacyRandom with itself.
Enderman.teleport is LivingEntity.randomTeleport(x, y, z, true,
#enderman_does_not_teleport_to). Horizon took the first full-collision
block as the ground and landed on top of it, checked water only at the
eyes and lava only at the feet, and never looked at the tag, so endermen
teleported onto magma, into fire, sweet berry bushes and wither roses.
Now, as vanilla: the ground is the first #entities_can_teleport_to block
below the spot, and the enderman lands at the spot's height less the blocks
it went down. The ground and every block in the new box must not be in
#enderman_does_not_teleport_to, and the box must hold no liquid. The
motion is kept (Entity.teleportTo -> snapTo), both teleport sounds play at
the new spot, and GameEvent.TELEPORT goes out from the old one.
The new rain scenarios also showed when the water's hurt comes. It is the
end of LivingEntity.aiStep, after pushEntities, with or without AI, not
baseTick. Enderman.hurtServer draws its 1 in 10 teleport chance after
super.hurtServer (its hurt sound's pitch first), also when the hurt
cooldown refused the damage, and teleports at once. Horizon instead forced
a teleport in rain without the draw, one tick late.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The fit line is worked out on the search thread, not the UI's. hooks_at reads raw.githubusercontent (no API quota) and is cached per commit. The four new strings go through tr/trf. One limit: it compares only the NATIVE_HOOKS_API number, so for a revision older than the hook catalog, a mod naming hook points can read 'Builds on the Horizon you play' and still be refused at build time. horizon-mods and launcher tests pass on the desktop with this head merged into main.
🤖 Agent: horizon-server's one-shot chunk prune ([prune_chunks]); one point to know: a delete run rewrites region files, so it only runs before the game starts and deletes nothing if a keep_files file can't be parsed. Checked with vanilla 26.3's RegionFile on 3,743 real chunks from the official server: kept chunks equal as NBT, nothing with InhabitedTime >= 1200 pruned.
🤖 Agent: Player.playSound passes the player as except, so the server's broadcast skips the glider and its own client must play big_fall or small_fall. The client's travel now plays it and still never hurts. The unit test checks both.
🤖 Agent: fallFlyTicks is counted after the movement as LivingEntity.tick does, so updateFallFlying's +1 wears the elytra on the 20th glide tick, not the 19th, for the local player and LAN guests. Main merged in (both new motion tests kept).
🤖 Agent: The shared vehicle stand-in at CART_KEY_Y is now removed only when no other guest's menu (or the host's container) still shows it. The items are still stored back on every close. The proof comment's container.viewers mechanism isn't in this diff; I reviewed the diff itself.
The configuration screen's Settings tab (RealmsSettingsTab): the world's name and
description fields (the click moves the caret between them), the region preference line
and its Region Preference button, and the open/close toggle moved here from the footer
(vanilla keeps it on this tab). Leaving the tab — switching tabs or Back/Escape — saves
the changed name, description and preference through RealmsClient.updateConfiguration
(POST worlds/{id}/configuration with RealmsConfigurationDto's JSON: the active slot, its
settings, the preference and the description).
RealmsPreferredRegionSelectionScreen: the automatic choices and every manual region, a
click selects, Done writes the choice back to the tab and Cancel drops it.
RealmsClient gains getPreferredRegionSelections (GET regions/preferredRegions) and
updateConfiguration; RealmServer reads regionSelectionPreference; RegionSelectionPreferenceDto,
RegionDataDto and RealmSetting serialization join the DTOs.
The tier's render/simulation distance options and the region quality icons are the next
PRs (the icons need sprites the GUI atlas does not carry yet); the region rows say their
name for now.
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: Box selectors test the target's bounding box with strict AABB.intersects (EntitySelector.getPredicate), while distance and sorting stay on the origin. Dragon, fangs, spawner, cloud and other kinds keep the point test until their boxes are modelled.
🤖 Agent: Predicate views now get the mob's live effects as (id, amplifier, duration), which MobEffectsPredicate reads. Main merged in (corpus groups kept side by side); game tests pass on the desktop in a union with main.
🤖 Agent: Checked against Entity.applyMovementEmissionAndPlaySound and checkFallDamage in 26.3: STEP carries the getOnPos state and HIT_GROUND the mainSupportingBlockPos state, so a carpet dampens. A mob's support is looked up only when a step is due, not every tick. Climbing still reads the block at the feet.
"Keep channels for UI sounds: Off", "Scroll wheel changes settings: Off"
and "Other players' cosmetics: Hidden" ran past their half-width
buttons and slid back and forth. Extras and PvP & HUD now measure each
item's label, with its first values, in the screen's font when the page
is drawn; one wider than a half-width button holds gets the whole row.
It works in every language, with no caption shortened.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options > Video Settings... now opens Horizon's graphics screen, as
Sodium's replaces vanilla's video screen. It gains the vanilla rows only
vanilla's screen had (Graphics Preset, Exclusive Fullscreen and its mode,
Menu Background Blur, Texture Filtering, Max Anisotropy) and the extras
that change the picture (Fullbright, Dynamic lights, Hide block-break
particles, still saved in horizon-extras.txt). It drops FOV and View
Bobbing, which stay where vanilla has them (Options, Accessibility).
Horizon Settings drops its second ways into the graphics screen, the
controller screen (Controls has it) and Mods (the Options screen has
it); Extras drops its PvP & HUD link and the PvP preset, which PvP & HUD
opens with. Vanilla's own Video Settings layout stays for
horizon-ctl options video-vanilla and its tests; no menu opens it.
A test walks every menu from Options and checks that each setting and
each page has one way in, and that every row of vanilla's Video
Settings is on the graphics screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Axis triggers are read only for an unmapped six-axis pad with the Xbox face layout (C and Z absent), so a generic pad's centred right stick on LeftZ/RightZ never reads as a half-pulled trigger. Main merged in beside #9830's face_buttons; game tests pass on the desktop in a union with main.
🤖 Agent: the slots now parse as the service sends them (a list, each slot's options a JSON string, missing world slots empty, worldTemplateId/Image), so the detail answer and the Players tab no longer fail; official Realms API only.
🤖 Agent: under 1380 px the tab strip only shrinks when the buttons need the room, so nothing scrolls in English where it already fit; the placeholders still go through tr() inside line_edit::content.
🤖 Agent: a direct port of TeamPredicate (exact, case-sensitive name match, no team fails), filled from the real scoreboard for the player, mobs (a tamed pet answers with its owner's team) and LAN guests.
🤖 Agent: one more mat4 x vec4 per vertex (16 FMAs), only in pack gbuffers and shadow vertex shaders: negligible next to fragment work. The GPU test tells the two rounding orders apart with exact expected bits.
🤖 Agent: the marker_particles harness gains 27 wither rose cases that fail without the fix (x and z off by 0.5); I made outline_center bound only the shape's own boxes instead of starting from 0.5.
🤖 Agent: removes the R32Float depth copy made every frame (512 MiB and about 10 ms at 8192², measured); each plain-sampler depth tap becomes one gather plus a bilinear blend, behind a uniform branch. The gather order (w,z / x,y) matches the blend.
🤖 Agent: hot path: at most 4 texel loads per mip texel (9 where both sides are odd), only for buffers that set MipmapEnabled: about 1.2M loads for a 1280x720 chain, well under 0.1 ms. Odd levels no longer skip texels.
🤖 Agent: per-frame cost goes down: cloudHeight now just reads the dimension's visual/cloud_height (vanilla's EnvironmentAttributes default is 192.33), with no alpha branch; Nether and End packs stop seeing NaN.
🤖 Agent: the layout matches DisconnectedScreen.init in the bytecode: cell padding 10 for the title and reason, 2 for the button, the reason wrapped to width - 50, the block centred. I moved wrap's doc comment back onto wrap.
🤖 Agent: grant and revoke now run as each selected player's own turn (for_player), and the feedback follows CommandResponseTracker: changed players on success, every selected player on failure.
🤖 Agent: matches MapRenderer.render's name label in the bytecode: scale clamp(25/width, 0, 6/9), and z -0.025 then scale -1 and +0.1, so -0.125 in map space. I dropped a test that only checked its own closure.
🤖 Agent: mesh-time only: one rotate_left per fluid top while a shaders/ pack is loaded, and a remesh when that changes; nothing per frame. The split rule is fitted to probe readings of Iris that was only run, not read.
🤖 Agent: I pushed b0f885600: this branch now merges #9772 and gives the new test's `ShadowCull` `reach: None`. Without that, whichever of the two landed second would break main's build. The union with #9772 passes every `shadow_caster_tests`. Parity's first run failed only on tab_list (its harness's missing vanilla.log) and the known lunge_motion flake, neither reached by shadow culling: rerun.
🤖 Agent: Off by default, and the client completion tree only gets `/pregen` when the dispatcher has it, so vanilla's command tree is unchanged until a player turns it on. Note for later: `pregen::command_tree()` duplicates the shape of `horizon_commands` in horizon-command; a change to one must follow in the other. I merged main (7899c12c4): the pregen line sits beside main's new `horizonMarks`/`cosmeticsSolo` extras, and 773e3526d queues every toggle on the server thread (no blocking `call`), with a test that a quick on-off reaches the server in order.
test_fishing_rng_parity now checks the hook's draws against vanilla; the
four scenario tests seeded two LegacyRandoms alike and compared them, so
they could not fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic (2026-11-05): the shop is on the site only; the launcher has the
Wardrobe and a link to the site. The Wardrobe already had no prices,
product lists or checkout; its "Open the shop" button becomes one plain
secondary button, "Get more cosmetics on playhorizon.dev", opening
https://playhorizon.dev/shop, by the title or, for an account that owns
nothing yet, in the empty state. Translated in all 86 languages.
Kept: what you own, equip and unequip, the preview, the purchase history
and the founder set's "Claim for free" where Paddle doesn't sell (the
site sends those players to the Wardrobe: only the launcher can prove
the account with Mojang).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The graphics screen gets a Shaders page between Performance and Advanced:
Shader Packs... opens the pack list (Horizon's .hshader packs and shaders/
folder ones, with the picked pack's options), and Shadow Cache moves there
from Performance. Horizon Settings loses its Shader Packs button, so the
list has one way in, beside the other graphics settings, as Iris hangs
off Sodium's video screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A crown's star on the head slot or an orrery on the back slot crossed the
wearer's name tag, which vanilla draws at a fixed height. Each item now
knows its top (Item:🔝 pixels above the model's neck with the bones at
rest, animations included), found in Item::parse with the reach check.
When the tallest item drawn on a player passes the tag's lowest pixel,
the whole tag (Horizon's mark and the badge row included) goes up to one
text pixel over it.
Players with no tall item, and anyone whose items aren't drawn (others'
cosmetics on Name tags only or Hidden, My cosmetics hidden, a helmet over
a hat), keep vanilla's height exactly: the lift is never added then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client mod hears each client tick of the window, at the top and the
end of vanilla's Minecraft.tick: native_client_tick_pre() before any of
the tick's work, native_client_tick_post() once the world's tick, the
chat queue, music and sounds are done. They fire 20 times a second on
the title screen, while paused and in a world alike, as vanilla's tick
runs, and never on a server (single player's ticks on its own thread).
The fragment labels them ClientTickEvent.Pre / .Post, client, not
cancellable.
Checked in game with horizon-ctl and a local probe mod: about 20 calls
a second of each, always paired, on the title screen, in a creative
world and under the pause menu.
Fixes#9971
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The landing's raw write is applied before the game finishes it: the game
now asks the hook there, and a cancel puts the previous block back and
skips the neighbour updates and the level events. The crop above trampled
farmland stays, because breaking it is the neighbour update and not a
write of its own.
`FallEvent::Write` carries the block it replaced, so the game has
something to put back. A guest's landing is asked with that guest's UUID.
A player's landing tramples farmland into dirt and breaks turtle eggs with
no hook a server mod can cancel, so a land-claims mod cannot stop a guest
from trampling inside a claim (#9036, #9873 item 3).
`native_entity_change_block_v2` is asked with the player whose landing it
was, the position and both block names, and can cancel the change. It is
additive: `native_entity_change_block` keeps its signature and firing point.
PrimedTnt.addAdditionalSaveData stores the igniter as an `owner` UUID
(EntityReference.store) and getOwner resolves it against the level when
asked. tnt_to_nbt now writes the live owner's UUID (a guest, the host
player or a mob), and a loaded TNT looks its saved owner up when it
explodes, so the blast is credited to the player or mob that lit it after
a reload, a portal trip or a chunk unload. An owner that is not online
keeps its UUID through the raw compound.
Fixes#9951
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Only the glass look changes: the actions move under the profile and wrap inside the 400 px card, and Night keeps its row through the same `slot_row`. Launcher fmt, clippy and 674 tests pass on the desktop with this head merged into main.
🤖 Agent: Launch… reuses the existing translated string and the glass sliders-horizontal icon, and clearing `launch` before `toggle_launch` stops a panel left open for another instance from turning the click into Close. Launcher fmt, clippy and 674 tests pass on the desktop with this head merged into main.
38 entries still held their English text (the modpack coverage reasons, Performance mode, the
Vulkan fallback notice and a few command-line hints). 15 entries said tu while the other ~110
said vous, sometimes side by side on one page; they now say vous, as the game's own French
table does. Names that read the same in French (Mods, Console, Nightly, Snapshots...) stay.
`horizon-port-pack events <jar or folder>` reads class files for the
NeoForge events a mod listens to (an @SubscribeEvent method's parameter,
a method reference or lambda that takes one event) and the Capabilities
fields it reads, and looks each name up in native-hooks.json's `neoforge`
labels: a hook, another phase's hook, a hook whose label says why it
isn't quite one, or none. Over a folder of jars it ranks the names by
how many mods use them. Names NeoForge changed after 1.21.1 are looked
up under their new ones. It reads names, never code.
On ATM10's 498 jars it takes 8 s: 416 of 497 mods use 326 names, 6,616
uses in all. Main's catalog has a hook for 23% of them, and 43% with
#9924's labels. The top gameplay gaps are ClientTickEvent (95 and 46
mods), RenderLevelStageEvent (81) and ClientPlayerNetworkEvent (53, 35).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the loot table random sequence (or level fallback), preserve level draw order, and create the experience orb directly. Add vanilla server comparisons for casting, bobbing, catch timers, and retrieval.
Co-Authored-By: Codex (GPT-6) <[email protected]>
🤖 Agent: The reach test prunes with the same grown box as the caster test. Packs without `shadowDistanceRenderMul` take `reach: None` and walk exactly as before. #9838 is stacked on this branch: it now contains these commits.
🤖 Agent: The positions now come from JOML's quaternion turns, ported under the MIT notice that `camera.rs` and THIRD_PARTY_NOTICES already carry. It's a few float ops per frame, and the test pins five Iris readings bit for bit.
🤖 Agent: Every repaint is now one `BitBlt` from a memory DC, and every GDI object (DC, bitmap, old selection) is released on both the success and failure paths. PR CI doesn't build Windows; your Windows clippy run covers that.
#9859 adds a drop_offset caller (the dispensed dragon); keeping the
existing signature lets both land in either order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EntityType.create draws the wrapped spawn yaw and Mob.finalizeSpawn the
follow-range triangle and the left-handed roll from the level random;
the dragon seeds its own random (Entity.random), not from the level one.
test_spawn_egg_fluid_parity gains the dragon egg used on a block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A vanilla list keeps the wheel: AbstractContainerWidget inherits
AbstractScrollArea.mouseScrolled, which scrolls and never reaches the rows, so
CycleButton.mouseScrolled only runs for a button outside a list. Horizon stepped
the row under the cursor, so scrolling a long page changed options.
"Scroll wheel changes settings" keeps that behaviour as an opt-in extra, off by
default. Its saved key is new because the old one was written as true by every
install, and its tooltip has a new key because other languages said "as in
vanilla"; they fall back to English until translated.
Fixes#9633
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: f4c4ab173 keeps `lastLevelUpTime` and `tickCount` per player entity (`LevelUpSound`, reset on respawn), as `Player.giveExperienceLevels` reads them, and `/xp points` now sounds per level crossed like `giveExperiencePoints`; the lan_e2e `levelup-sounds` check pins both players apart. Wither head particles are drawn on a following client only: the server's `particles.add` draws nothing. The guest tick loop is one add per player per tick.
🤖 Agent: I pushed c17d14c73. `GameLevel` now borrows `guest_boxes` outside a guest turn instead of cloning them on every level build. `for_turn` searches only the few borrowed ancestors (hub conns are unique), so each guest's turn is O(players), not O(players²), every tick. Your tests are unchanged and pass. Then I merged main (c4b85eaed): only the `lan_actions.rs` imports conflicted.
🤖 Agent: The swap applies only on Linux, for a driver mapping with exactly the four face buttons and a non-Sony/Nintendo vendor. Pads with an SDL row aren't touched. It costs six `button_code` lookups per pad per poll.
🤖 Agent: Matches `ThrownTrident.tick`'s `isAcceptibleReturnOwner` (checked in the decompiled source): a dead or spectating owner drops the trident (unless creative-only) and discards it, and an owner not in this level still keeps the old any-player pickup.
🤖 Agent: I pushed 8cff8f837: with the vanilla data present, the new test failed because the llamas weren't tame (`AbstractHorse.canParent`), so they never bred. Without the data it returned early, which is why it passed for you. Both llamas are now tamed, and it passes on real data.
A scan of ATM10's 491 mods for their NeoForge event listeners (#9921)
showed nine unlabelled hooks behind events many of them use:
RegisterCommandsEvent (95 mods), ItemTooltipEvent (74), the item and
fluid block capabilities (129, 88), RenderHighlightEvent.Block (41),
ServerStartedEvent / ServerStoppedEvent (48, 41), LivingDropsEvent (36),
LivingKnockBackEvent and ExplosionEvent.Detonate. Each label comes from
the hook's own doc comment and firing point; `cancellable` matches its
mod_api contract, and block_highlight keeps the side and cancellable it
already had.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: `--only` keeps first-parent order and leaves the other 18 fragments and their markers byte-identical. The pending signatures in #9431 and #9649 belong to other fragments, so stamping 410/411 doesn't settle them early. If another stamp lands first, regenerate.
🤖 Agent: With no listener, `toast_allowed` is one dispatch over an empty list, and it's asked only when a toast is created, not per frame (a cancelled hint is remembered). Updates to a shown toast are not re-asked, as with vanilla's addOrUpdate. I pushed 6fa2c9933: main added `Hint::SocialInteractions` (#9796), so `Hint::name` needed the arm to build, and that toast now also goes through `toast_allowed` (listed in the docs as `socialInteractions`).
🤖 Agent: I pushed cfa94ac87: vanilla's `AABB.intersects` is strict (`minX < maxX && maxX > minX`), so a player box that only touches a face of the 17-block box is out. The test is now `<`/`>`.
🤖 Agent: `dunce` is already in Cargo.lock (MIT/Apache/CC0) and is Windows-only. The Help link now goes through `open_uri`, so a URL is no longer canonicalized as a folder path.
🤖 Agent: Only the block atlas gets the new sampler, and only when filtering is on: normals, specular and Horizon's own packs keep `NearestRepeat`. The GPU test pins the 0.5/0.25 level blends. I merged main (latest b859bcf31): the conflicts were only the test-module list in `pack_post.rs`.
A one-shot step before the game starts, switched on in server.toml: chunks
with InhabitedTime under min_inhabited_ticks (1200) are deleted from region/,
entities/ and poi/, files rewritten compacted and emptied ones deleted.
Chunks near the spawn, named by keep_files (SMP claims and homes), that can't
be read, and a ring around every kept chunk stay. mode = "dry-run" only
writes the report while the game runs; either mode turns itself off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: This matches `ItemFrame.readAdditionalSaveData`: the old map is detached only when the old stack is non-empty and `ItemStack.matches` fails (count, item, components), and a glow frame keeps its type. I merged main (latest 5d54563cc), following your keep-both note: both isolation sets, the `expect_map` guard, and `frame_map_state`, `mob_anger` and #9848's `menu_state` all in the record accounting.
🤖 Agent: Only a bare `type` is qualified, through the existing Identifier parser, at both the decode and the encode boundary, which matches `MapDecorationType.CODEC`'s holder-by-name read. #9814 (already merged) handles the empty default separately. I merged main (latest b9df50777), keeping your `canonical_other_component` beside #9814's empty-default filter in `encode_components`, and your fixture flags beside #9848's piglin ones.
🤖 Agent: The villager now uses the same `findStandUpPosition` search as the player, with its own 0.6 x 1.95 box and vanilla's fallback (0.1 above the block over the bed). The player path is a move with no change, and the `1.0E-5F` border inset is now the float vanilla uses. I merged main (8bbedb4d4): the use_item_on harness keeps your wake cases and #9775's used-stat cases as two separate seeded streams, so neither changes the other's cases.
🤖 Agent: `pauses_game` now reads the same `split_players` list `set_paused` already uses, so the window can no longer stop ticking while the server thread runs on for P2. Solo, LAN and guest windows take the old path.
🤖 Agent: The release names its hand-over, so a stale or repeated `player-release` cannot remove a player who is playing again, and `remove_guest` only skips the leave line when `quiet_leave` is set: ordinary leaves are unchanged. Your rebase (ebfab7fc8) is the same patch; checked in a 21-PR union on main: fmt, clippy -D warnings, workspace tests.
VibrationSystem.Ticker.trySelectAndScheduleVibration sends one vibration particle to every player within 32 blocks; the check asserts both the host's window and a LAN guest make it (#8615).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Now that the golem keeps its anger target through updatePersistentAnger:
NeutralMob.playerDied ends anger at the dying player even once the golem
lost sight of them; the save writes the anger target, not the current
target; and anger at a mob is written with that mob's UUID and read back
as anger at a mob, so a reloaded golem that was fighting a monster is not
isAngryAtAllPlayers under universal_anger. A calm mob no longer keeps a
loaded angry_at in its save.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`transmuteCopyIgnoreEmpty`: at 18 + 7 of 25 the carrot on a stick becomes
a fishing rod that kept its custom name and starts whole, where the old
path wrote a fresh plain rod. Wear that Unbreaking took away (0) breaks
nothing, and a stack with no durability never breaks.
`FoodOnAStickItem.use` wears the stick with
`ItemStack.hurtAndConvertOnBreak`, which runs `hurtAndBreak` - and
`hurtAndBreak` asks `processDurabilityChange` first, so Unbreaking has its
say before any damage lands.
`use_steering_stick` added the whole 7 (carrot on a stick) or 1 (warped
fungus on a stick) straight onto `stack.damage`, so an Unbreaking stick
lasted exactly as long as a plain one, and it never reached the mods'
item-damaged hook.
A mod tank can now refuse a fluid through native_block_entity_accepts_fluid (NeoForge's IFluidHandler.isFluidValid). The server asks it before filling a tank in both insert passes and before native_move_fluids counts a tank's room; every tank accepts every fluid unless a mod cancels. A water tank can take no create:chocolate, and a removed mod leaves no fluid id it cannot use behind.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
🤖 Agent: docs only. telemetry.md now lists Show me as Horizon (on by default) as the second exception to 'nothing without consent', which settles socksprox's point on #9440.
The row pointed at the geometry and carried the missing rule as a gap;
the grid now goes through `item_tooltip::bundle_tooltip`, which asks
`TooltipDisplay::shows("minecraft:bundle_contents")` before the layout.
A registry project's page said "0 followers": followers are Modrinth's,
and the registry has none. Its creators collect stars on their GitHub
repository instead, so the index gets `projects[].stars` (the source
repository's stars, which horizon-mods' publish job reads every hour, as
it adds `downloads`; absent when 0, the schema unchanged) and the page
shows "{count} stars on GitHub" there, with a "Star on GitHub" button
beside "Sign in with GitHub to rate" that opens the repository, where
the player stars it. No sign-in or permission is added.
`browser::horizon::github_repo` reads the repository from `source` (a
folder of it, `.git`, a query or an anchor are the same repository;
another host or a name GitHub doesn't allow gets no button). A project's
details now go to the page as the `Arc` it keeps them in, so the larger
`Details` doesn't make `Loaded` a large enum. Both strings are in every
language's table.
Fixes#9929
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`BundleItem.getTooltipImage` returns nothing when the stack's
`minecraft:tooltip_display` hides `minecraft:bundle_contents`, so a
bundle given with `hide_tooltip` or with that component hidden drew no
grid. Horizon drew the grid for every bundle, and the tooltip box grew to
hold it.
`item_tooltip::bundle_tooltip` asks the display first and then defers to
`bundle::tooltip_layout`; the tooltip draw uses it for the height probe
and for the draw alike.
A Resolution Scale leaves the pack's picture smaller than the window. Its final pass drew at the window's size, so a shader using gl_FragCoord as texel coordinates (Photon's final) only filled a quarter of the screen. Draw final at the picture's size into an offscreen texture and copy it once to the window with a screen-row fullscreen pass; the no-pack path and same-size output are unchanged.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
IronGolem.doHurtTarget throws the target up only when the hit lands, by
0.4f * max(0, 1 - knockback resistance); Horizon threw everything it swung
at by 0.4. The hit and the toss are one event now, like the warden's
sonic boom.
IronGolem.aiStep ends in NeutralMob.updatePersistentAnger(level, true), the
same as a wolf's: every tick it holds a target, a monster too, the anger
timer restarts with a draw of the golem's random. Horizon only angered it at
a player who hit it, so a golem going after a monster drifted from vanilla's
random within a few ticks. The golem now uses the wolf's port, and its
player target goal asks isAngryAt.
LivingEntity.aiStep still drops motion below 0.003 and runs pushEntities
without AI; Horizon's NoAI branch skipped both, which the new mob AI
scenarios (a golem fighting a NoAI zombie, ravager and warden) showed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run neighbor-shape updates on the authoritative level before the existing eligibility checks, and clear the verified carrier after the handled placement.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: fixes the OpenGL startup crash on old AMD drivers (#9892), a regression from #9584 in Nightly f319701. The new test fails on main and passes here.
A guest's use raised its opener count twice: open_menu counted the block entity's startOpen, then lan_open_container counted it again once the menu was sent. Closing lowered it once, to one, so stopOpen never ran and the lid stayed open with its raised collision (#9500).
The guest open path now relies on the count open_menu already made; the harness keeps the lid open and closed checks for both host and guest.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
The tick cases' stand-in world stopped at y 32..96 and x/z -32..31, one block
short of every sample the loop takes and of the neighbours those samples read,
so a block at the edge compared air against the oracle's unending world: that
is where the beehive, the redstone ore and the pointed dripstone differences
came from. Fill the whole box plus a block either way, let a case name the
floor below y = 64, hand the loop the state's own fluid (a waterlogged block
has a water source, which it ticks and drips) and bind the client jar's block
tags in the oracle the way a datapack load does, which a bare bootstrap leaves
empty. Then add the families that read a neighbour, a shape or their own fluid:
dried ghast over soul sand, soul soil, stone and air; hives and a nest embedded
and hanging; a lit redstone ore and a deepslate one embedded and over air; and
stalactite tips over stone and water. 2,097 cases and 456,747 output lines
become 2,187 and 462,718.
BeehiveBlock.animateTick is a `for (i = 0; i < random.nextInt(1) + 1; i++)`
loop, so the bound is drawn again on the check that ends it: a hive always
spends two nextInt(1) draws around its single trySpawnDripParticles attempt.
Horizon hoisted the bound out of the loop and spent one, which left the level
random a draw ahead of vanilla from the first hive the ambient loop touched:
every particle after it moved, and the honey drip of a hive hanging over air
came out at the wrong column.
The corpus had no wither rose case at all. Add a world that is all wither rose,
run the whole ClientLevel.animateTick loop over it and compare every smoke puff
by exact position bits in call order: twenty-seven cases, three seeds and
twenty-four at random ones. The probe now supplies the flower's per-column model
offset the way the client's mesh tables do (a case without it would compare a
shape the game never shows). 2,070 cases and 402,440 output lines become 2,097
and 456,747.
WitherRoseBlock.animateTick reads its centre off getShape(state, level, pos,
CollisionContext.empty()), a flower's own box whose bounds centre is 0.5/0.5
before the block's per-column model offset. AnimateContext::shape_y answers with
the collision shape, which is empty for every flower, so the centre came out as
0 and the whole smoke column sat half a block towards the block's north-west
corner. Take the centre from the pick shape's boxes (BlockFacts::outline) and
keep the model offset on top of it.
The hooks that came before docs/native-hooks-neoforge.md had no
`neoforge`, `side` or `cancellable` keys, though NeoForge mods listen to
their events most: server and level ticks, block and item clicks,
attacks, joins, chunk loads, logins. Each value comes from the hook's
own doc comment or the vanilla method the proposal's table names, and
`cancellable` matches the hook's mod_api contract. Metadata only: no
name, signature or call site changes.
The proposal's status, its next-hooks table (most of it has landed)
and its capabilities and rollout notes now match main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Surface.color's "0..1" stays in the 0.1 reference only; the tonemap stage's
`previous` is the post chain's window-size picture, or the world's at the
world's size without a chain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The detail answer's slots are a list (RealmsServer.slotList) whose options
are a JSON string each (RealmsWorldOptionsJsonAdapter); a map of objects
failed the whole answer, players tab included. Missing world slots 1 to 3
are empty (finalizeSlots), unset options keep new RealmsWorldOptions()'s
values, and the template fields are worldTemplateId and
worldTemplateImage, in the update too. Cycle labels join through
options.generic_value.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fold started from 0.5 on every side, so a shape that does not cover
the block's centre would have been stretched to reach it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Step 2 of docs/design/hdr-linear-light.md in horizon-shader-api. The line is a
value (`Api`), and `check` and `horizon-shader` serve 0.1 and 0.2. Under 0.2
the preludes and entries decode colours at the stage boundary (a 2.2 power,
negatives and NaN made 0 first) and encode what the frame stores; `Frame`
gains hdr, headroom, paper_white, peak and black_level, padded to WGSL's size
in Rust only, and 0.1's prelude still declares only its 49 fields. A
`[tonemap]` stage and `[frame] blend` are 0.2-only manifest keys.
0.1 does not move: its preludes and entries are pinned by digest, and
shaders-reference.md is unchanged; 0.2 has shaders-reference-0.2.md. The game
refuses 0.2 packs and `blend = "linear"`, and the registry refuses 0.2, each
with a message saying why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An icon-only button recorded nothing, so the pad could not reach the pause
menu's Report Bugs, Feedback and Player Reporting icons, or Realms' icons. An
active button with an empty label is now a focus target. So are the
Multiplayer warning's "Do not show again" box and Optimize World's Erase
Cached Data box, which A now ticks.
tools/pad_menu_e2e.py gains every_screen_has_targets, which walks the title's
submenus and every options page and fails on a focus screen with no targets.
Replay Studio is allowlisted because it registers nothing yet. New pause_icon
and erase_checkbox cases press the new targets. The pause menu now has
unlabelled widgets, so splitscreen_e2e's setup scenario reads labels with get.
controller_settings_pad's check for a Navigation page now matches exactly: it
tripped on the General page's Navigation Repeat rows.
docs/controllers.md no longer says every rectangle registers itself or that
both modes reach every screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world's depth was bound as a depth texture and read with textureLoad, which naga's GLSL backend cannot emit, so TAA, the depth blit and the shader depth test failed to build on OpenGL. Bind those views as unfilterable float textures instead: wgpu accepts a depth view there, and textureLoad returns the same depth texel on every backend (Vulkan, DX12, Metal, GL).
The shader translation test now fails on any shader the GL backend runs that cannot translate, and pins fs_taa and fs_blit; the HUD's glint, which fails for an unrelated two-samplers-per-image naga limit, is the one allowed exception.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
#9584 replaced FXC-rejected dynamic component stores with a select between uvec3s on a
bvec3. naga writes that as GLSL's mix(uvec3, uvec3, bvec3), which needs GLSL 4.5 or
EXT_shader_integer_mix; AMD's 2016 driver for the Radeon HD 7400M resolves it to the float
overload and rejects the 'Terrain solid (flat)' vertex shader, so the game no longer starts
there. One scalar select per component compiles to ?: on every backend.
The shader translation test now refuses any select between integer vectors on a bool vector.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps the map decoration namespace flags beside #9848's piglin anger
memory flags in the command_exec harness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: launcher-only fix, green on the desktop at d984823 (cargo fmt --check, clippy --all-targets -D warnings, cargo test: 559 + 102 passed). One point to watch: the catalog is fetched from raw.githubusercontent only when a hooked mod uses named hook points, so builds of raw-target mods make no new request.
MultiPlayerGameMode.interact sends EntityHitResult.getLocation() minus
entity.position() in every ServerboundInteractPacket. Horizon sent a
fixed [0.0, 1.0, 0.0] for mobs, a point in their middle: armour stands,
saddles, leashes and anything else that reads the click carried
interactAt to the wrong place, unlike vanilla, and a server's reach or
ghost-hand checks saw clicks that vanilla never makes.
mob_hit and the spear's attack_range pick now return the point along
the view ray where the mob's box was met, measured from its feet, and
every caller destructures the third element. A regression test checks
the packet's point against the ray.
ServerGamePacketListenerImpl.handleInteract fires the trigger with the
held stack as it was before the interaction whenever an item interaction
succeeds. NameTagItem.interactLivingEntity and DyeItem.interactLivingEntity
both succeed, but interact_mob returned after name_mob and after the sheep
dye before reaching the shared trigger call.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: with levzzz5154's commits an entry's icon must be exactly icons/<id>.png, so one project can't show another's icon; all 94 entries on horizon-mods main already comply and none is incomplete.
🤖 Agent: The new pages go through the same English-as-key lang::get scheme as graphics_ui, and the detected glyph style now follows the named device's vendor before that pad has been used. fmt, clippy and the controller and settings tests pass merged with main.
The configuration screen's Worlds tab: the four slots as vanilla lays them out (the
active one 80 square, the others 50, 16 apart), each with RealmsWorldOptions.getSlotName's
name; a click on another non-empty slot asks first, then RealmsClient.switchSlot
(PUT worlds/{id}/slot/{slotId}).
World Options opens RealmsSlotOptionsScreen: the world name (the default one stored as
empty), difficulty and game mode cycles, force game mode and spawn protection 0-16, with
vanilla's disabled states (a hardcore slot locks difficulty, game mode and force mode; a
world that is not NORMAL keeps its stored spawn protection and force mode). Done saves
through RealmsClient.updateSlot (POST worlds/{id}/slot/{slotId}, RealmsSlotUpdateDto's
JSON) and the tab reads the detail again.
RealmsServer reads the detail's slots (RealmsSlot, RealmsWorldOptions, RealmsSetting and
isHardcore). Slot images come with the pack-icon-slot PR; until then a slot is its frame
and name. The spawn-protection slider takes a click on its track: this screen has no drag
event yet.
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: The new calls go only to the official Realms service through the existing RealmsClient, and the UUID path segment must be 32 hex digits. Main is merged in to clear GitHub's criss-cross conflict after #9714 landed.
A mod naming hook points (blockscope 0.3.1: hooks 2, all five by name) beside
one pinning 5e391eb (shulker_peek 0.1.0) built on the pin: 5e391eb already had
hook points v2, but native-hooks.json only came in d3e0774, still v2. So
horizon-mixins stopped with E_HOOK_CATALOG ("a named hook needs
native-hooks.json at the workspace root"), on every platform.
A revision now fits such a mod only when its native-hooks.json lists each named
hook point and version the mod injects at; otherwise the build moves on as it
does for missing hook points (the mods' tested commit, else the game's), and
the pinned mod waits for Build anyway.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read back with probe packs of ours, Iris 1.11.7's ftransform() has the bits of
gl_ProjectionMatrix * (gl_ModelViewMatrix * gl_Vertex) at every vertex our
probes read. Horizon multiplied the vertex by the product matrix formed once
per draw, which rounds differently in the last bits. With it, on a stack with
the other open #8464 PRs, a strip of water tops at ow_birdseye_noon read the
shadow map up to 73 units of 2^-24 from Iris's coordinates, and 35,632 and
50,761 of the texels our probe reads back differed from Iris's. How last bits
move a whole strip is not established.
The GPU test reads ftransform() back from a gbuffers and a shadow program with
matrices that tell the two orders apart (one rounding against two).
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A location predicate whose `structures` named a tag
(`#minecraft:shipwreck`, or a data pack's own `#pack:village`) never
matched: the predicates' tag lookup had no `worldgen/structure` case and
the world's pack tags didn't hold that registry. The world's tables now
load the structure tags with the packs' over the game's own, like the
entity type, biome and damage type tags, and a world without packs
reads the game's own structure tags (loaded once).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Consumable.onConsume awards the item's used statistic and fires the
consume_item trigger for a server player before the consumable's listeners
run and the stack shrinks. A potion and an ominous bottle finish through
finish_drinking, which did neither; food, honey bottles and milk go
through finish_consuming, which already does both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: The angry_at memory is written only while this live guarded-block event owns it, and removed once the anger ends, so a stale raw Brain value cannot come back. Main is merged in, with both sides' recorded ops and fixtures (mob_anger, menu_state, map decoration) kept.
🤖 Agent: read_as is only checked for built-ins while a shaders/ pack binds its programs, so nothing changes without a pack. Main is merged in, and wrong_type_uniform_tests now sits beside main's atlas_name and unknown_sampler test modules.
🤖 Agent: native mods update only in the start's run; Play then runs the existing build_native(false) (stays on the instance's Horizon, mismatch notice as usual) and plays only if it succeeds; Nightly-pinned instances are left alone.
The registry's index now lists a native mod's hook points version
(`hooks`), and the browser and `mods search` compare it, or the mod's
pinned commit, with the Horizon the instance plays: builds there, needs
newer hook points, moves the game to its commit, or clashes with the
pins of mods turned on (Build anyway). Closes#9860.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The corpus had no wither rose case at all. Add a world that is all wither rose,
run the whole ClientLevel.animateTick loop over it and compare every smoke puff
by exact position bits in call order: twenty-seven cases, three seeds and
twenty-four at random ones. The probe now supplies the flower's per-column model
offset the way the client's mesh tables do (a case without it would compare a
shape the game never shows). 2,070 cases and 402,440 output lines become 2,097
and 456,747.
WitherRoseBlock.animateTick reads its centre off getShape(state, level, pos,
CollisionContext.empty()), a flower's own box whose bounds centre is 0.5/0.5
before the block's per-column model offset. AnimateContext::shape_y answers with
the collision shape, which is empty for every flower, so the centre came out as
0 and the whole smoke column sat half a block towards the block's north-west
corner. Take the centre from the pick shape's boxes (BlockFacts::outline) and
keep the model offset on top of it.
Windows 11's Smart App Control lets only signed programs run, and a native
build runs programs it has just compiled (build scripts, horizon-mixins, the
game), so every build failed with os error 4551 and an opaque cargo error.
The launcher now checks Smart App Control before downloading anything
(VerifiedAndReputablePolicyState 1) and says how to turn it off; a build
that hits os error 4551 anyway leads its error with the same explanation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y255SPgcPcs89pQpqu1eES
🤖 Agent: the content-id guard runs after index::check_file and before the rename, so a held update leaves the installed .hmod and its list entry untouched; the replaced file is kept until the next start for rollback.
A command run by a data pack function printed its query result in the
chat: `time query`, `seed`, `gamerule <rule>`, `difficulty`,
`experience query`, `list` and `help` wrote a system message directly,
past the silent source a function runs with. They now take
`feedback_unbroadcast` (`CommandSourceStack.sendSuccess` without the
broadcast to operators, as TimeCommand, SeedCommand, GameRuleCommand,
DifficultyCommand, ExperienceCommand, ListPlayersCommand and
HelpCommand send them): a silent source says nothing, a command block
keeps the line as its last output, and `send_command_feedback` off hides
it from a player. The returned value still reaches `execute store`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A pack reading shadowtex0/1 through a plain filtering sampler got an
R32Float copy of the shadow depth every frame, made through a buffer
(#8999: wgpu filters a depth format only in a comparison). At a large
shadowMapResolution that is most of a frame: 8192² costs 512 MiB (the
buffer and the copy) and about 10 ms a frame on an RTX 2080.
The translator now sends texture calls on shadowtex0, shadowtex1,
shadow and watershadow declared as sampler2D to helpers that gather the
four texels and blend them bilinearly, as the filtering sampler did,
reading the depth itself. A uniform of ours, hz_depth_linear, says per
name whether it filters (shadowtexNNearest unset) or takes the nearest
texel. The pack's own functions given one of them as a sampler argument
carry its flag in an extra parameter (-1 for any other texture, which
then reads with the built-in the pack called). The copies, their buffer
and textures are gone, and devices without FLOAT32_FILTERABLE now filter
too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Each colour run is its own text call, and plain lines are still one call each. Plain lines now skip the component round trip every frame, and edits keep the old line's root style as SignBlockEntity.setMessages does.
Registry v2, step 4 (docs/design/registry-v2.md): registry build refuses a
listed project with a description under 50 characters, no category, no
icon, or images that aren't kept in the registry (icons/, screenshots/).
The site and the launcher then load images only from the registry's host.
Lands once every entry on horizon-mods main is backfilled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-mod-api-gen generates Rust from tools/native-hook-fragments/, so
a change there builds the base instead of reusing the change's binaries.
Golden Hour with HDR on is checked against hdr_toggle's first shots (the
same settings without the pack), sidecars included, so a pack that only
fails under HDR doesn't pass as identical.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With automatic updates on, the start's run also updates native mods
(.hnmod), except for an instance that keeps a Nightly or a native build
made on one. The next Play of an instance whose native mods moved on
builds its game again first (the Mods page's Build), then plays it.
Play's own run never updates native mods: no build would follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A setting, on by default (Settings > Game > Update registry mods
automatically): at start (every Horizon instance) and before Play (the
instance it starts), what the browser installed from Horizon's registry
moves to Index::installable, checked as an install is.
Never updated automatically: a modpack's profile (its .hpack pins each
mod by SHA-256), native mods (they need a build), data packs, a version
for another Minecraft, anything older than what is installed. A content
mod whose new version drops a block, item, entity, effect, menu or
creative tab waits for the player, with a notice naming them. The file
an update replaces is kept in mod-browser/replaced/ until the next
start, which puts it back if the update was cut short.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: screenshots go through gallery_of, which drops any URL images::refuse rejects, so the image host allowlist stays as it was; the Gallery tab hides when a project has none.
A registry project's `screenshots` (mirrored into the registry, served
from its Pages beside the icons; other hosts are left out) fill its
page's Gallery. The Gallery tab shows one image large, a click on it
shows the next, arrows and a strip of thumbnails pick another; a project
without images has no Gallery tab.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A buffer's mip level was one bilinear sample of the level above at the
centre of each destination texel. Along an odd side (45 rows of a
1280x720 buffer's fourth level, 11 of its sixth, 5 of its seventh) that
sample lands near one texel's centre instead of between two, so a small
bright spot was skipped: a probe pack of ours lost a 26x22-pixel square
of 10.0 entirely from level 7 down, where Iris 1.11.7 kept it down to
its 1x1 level, the buffer's mean.
Each level is now a box filter of the level above (OpenGL 4.6, 8.14.4,
recommends one): two texels along an even side, and three along an odd
side of 2d + 1 texels, weighted d - i, d and i + 1 over 2d + 1, so every
texel above counts as much. Our probes' Iris values fit this within
0.1 % at levels 6 and 7 and 1.5 % at level 10, in all 16 colortex
buffers, in 10 formats. Iris's level 5 of that buffer fits it in rows
but not in columns (undiagnosed).
Fixes#9891
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps the pregen extra next to main's Horizon marks entries in the
extras file and the menu-only item list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: registry data packs reuse install_datapack but go through index::check_file first (signature and pack.mcmeta/data/), so they get the registry's full check, not just the SHA-256 Modrinth files get.
🤖 Agent: the ownership point to watch is Edit project: page edits are allowed only to an owner matched by numeric GitHub ID (Ownership::ById), never by login alone, and an image upload always makes an update a reviewed one rather than an owner update.
🤖 Agent: With the switch off, single player now asks the service nothing for the player's own model. Before, it asked /v1/worn about the offline UUID, which the service never knows. The cost when off is one bool test, on a path that runs only for the player's own model.
A shader pack's `cloudHeight` read NaN in a dimension whose cloud colour
has no alpha (the Nether, the End), as ShaderDoc describes it. Probe
packs of ours read 192.33 there in Iris 1.11.7, in every program kind
they read it from (gbuffers_terrain, gbuffers_textured_lit,
gbuffers_skybasic, shadow, prepare, deferred, composite and final).
`cloudHeight` is now the dimension's `visual/cloud_height` with or
without clouds: `EnvironmentAttributes.CLOUD_HEIGHT`'s default, 192.33,
where the dimension doesn't set it.
Fixes#9890
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps the villager wake cases and #9775's used-stat cases as two
separate streams in the use_item_on harness.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The Horizon mark and the badge share one lock look per name tag (Frame::horizon beside Frame::worn), so the name tag cost did not move on playhorizon (eg_name_tags median 0.027-0.029 ms with the mark on and off). The Founder gem is the service's texture (horizon-cosmetics#8), so deploy that right after this merges.
Merging main brought Hint::SocialInteractions (#9796); Hint::name was no
longer exhaustive. The first-server hint is now asked like the others, and
is still spent when cancelled, as vanilla sets joinedFirstServer either way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
It fails any change that moves the picture on purpose, so no automatic
selection may pick it (parity-main would bisect every visual fix to it).
A pull request that must leave the picture alone carries the
frame-identity label, which parity.yml reads as an input and then runs
the harness by name after the selected ones.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each player-* request writes one log line with what became of it: INFO
and what it did, or WARN and the reason it was refused. The line names
the player and, for the export, the import, the confirm, the release and
a named drop, the hand-over id, so the lines of one hand-over can be
found on both servers. The record and the request's body are never
written.
docs/server.md gives a hand-over in order, with the ids, the commit
point and what undoes itself, and the log lines.
tools/server_transfer_e2e.py runs a whole hand-over between two
dedicated servers over their real control sockets: two engines with
[transfer] on and Velocity forwarding, a small relay per engine that
answers the forwarding request as a proxy does, and horizon-bots
clients. It follows the documented order step by step, checks the
target's player file is on disk when the confirm answers and that the
source writes nothing for a player moved away, then aborts a hand-over
back and checks the player plays on.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The request may name where the player arrives, over the record's
compound: `position`, `rotation`, `on_ground` and, now that #8693 keeps
a guest's motion, `velocity`. They belong to the `position` part. The
pitch is taken as Entity.setXRot takes it (modulo a turn, then cut to
+-90) and the yaw as it is; a velocity component over 10 blocks a tick
is taken as 0, as Entity.load takes a saved Motion. A position must be
within the same bounds as the record's own (Entity.load's clamp:
30,000,512 on x and z, 20,000,000 on y), and a rotation or velocity must
be finite numbers.
When the import moves the player to another chunk than the one its view
was built around, the view moves at once: the chunk centre is sent
before the position to confirm, and the chunks out of reach are
forgotten. The reply says so with "recentered".
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9829 made the join and leave lines name a player as Player.getDisplayName
does (its team's format and its nickname). place_arrival, which announces
a player placed by player-import or player-confirm, still used the plain
name; it now uses player_display_name too, so an arrival is announced as
a login is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_frame_identity_parity.py builds the base (BASE_SHA, else the merge-base
with origin/main) in a worktree and target folder of its own under
.local/frame-identity/, draws fixed scenes offscreen with it and with the change
under HORIZON_STILL=1, and requires identical decoded pixels, writing a diff image
where they differ: HDR off (day over water, night with light sources, stained
glass, the Nether), Golden Hour (API 0.1) in SDR and with offscreen HDR output,
and HDR turned off in a running game. Each session checks it drew what it is for,
so a pack that fails to load, or HDR that stays off, in both builds can't pass.
Lavapipe where it is installed, else the same adapter for both builds. A change
that touches no file a game binary is built from reuses the change's binaries.
The differential screenshot test of docs/design/hdr-linear-light.md (Tests 1, 2),
first of the shader API 0.2 series (#9861).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
martipops's second review of #8730.
The commit point now covers the three files and a power loss: the
player file, advancements and statistics are each written to a new
file, synced, then renamed into place, and the folder is synced on Unix
(save::write_durable, write_dat_durable). Each write returns its error,
and the player stays held and imported when one fails. Ordinary saves
are unchanged.
A retry of `player-import` or `player-confirm` after the commit, whose
reply was lost, names the hand-over of a player already placed from it:
it is answered ok, placed, with `"retried": true`, and nothing is done
again. Another hand-over's id is still refused.
The record's health is refused over 1024 and its absorption over 2048
(max_health and max_absorption), its food exhaustion over 40
(FoodData.addExhaustion) and an effect amplifier outside 0 to 255, as
26.3's bytecode bounds them. A stack's count needs no check:
item_from_nbt already takes one outside ItemStack's 1 to 99 as 1.
docs/server.md says what a proxy does when `moved` is refused after the
commit (the freeze ran out, or an operator's /kill), and that it
confirms only while the export's `freeze_ticks_left` leaves it time.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`player-import <uuid> <json>` applies a hand-over's record (from
`player-export <uuid> freeze`) to the held player on the target, whole or
in parts, and tells its client what changed. Unless the request says
`"place": false`, it then places the player as `player-confirm <uuid>
<hand-over>` does.
Placing is the hand-over's commit point: the player file, advancements
and statistics are written to disk synchronously, then the player is
placed, and only then does the reply say ok. A proxy releases the
source after that reply, so a crash of the target no longer leaves the
only current copy in memory. If the file cannot be written the player
stays held and imported.
The record's hand-over id is bound to the held player at the import: a
record without one (an export without freeze) is refused, a retry of the
same record is taken and another hand-over's is not. `player-confirm`
names it, and `player-drop <uuid> [hand-over]` may: a named drop must
name the imported record's. A held player that is leaving (dropped or
kicked, its connection not closed yet) can be neither imported nor
confirmed.
The import reads the whole compound as a file load does: the keys
Horizon does not model, the mods' attachments (given to the game's own
map) and the body's values. The body follows the parts: fall and motion
with `position` (with the saved vehicle), air, fire, freezing and food
exhaustion and timer with `health`. Values no player can have are
refused before anything changes: a position, rotation, motion, health,
absorption, saturation, experience progress or fall that is not a
number, a position past vanilla's clamp, a health of zero or less.
The client is sent the effects the import removed and the recipe book
it brought. Every player gets the arrival's list entry, since every
player is shown its entity; `announce` is the join line only. At
confirm the client is sent its position and inventory again, since what
it did since the import was not acted on.
The control endpoint refuses a request line longer than 16 MiB, which
the game would otherwise read and parse between two ticks.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The config reply pinned by api-v1-config.json lists [transfer] with its
defaults; this PR adds max_hold_seconds (60 by default), so the golden
lists it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A login that used up a proxy's expectation (player-expect, taken as the
login starts) is held on the target server: it gets the whole join
sequence and the level around it, and its acknowledgements are handled,
but it is not placed. No join message, no list entry for the others, not
among the entities they are shown, not simulated, and nothing else it
sends is acted on. The world is loaded around a held player as around a
placed one. The log says "<name> is held: it arrives from another
server".
A held player nobody takes over is dropped after [transfer]'s
max_hold_seconds (60 by default, 1200 ticks), with one warning: a player
already kicked or dropped, whose connection is slow to close, is not
kicked again on every tick. horizon-server passes the setting with the
others.
A held player keeps a true picture of the others while it waits: a
player who joins meanwhile is added to its list, and one who leaves is
removed from it with its entity, or its client would drop the entity of
a player it has no entry for.
`player-drop <uuid>` forgets the player's expectation and disconnects
the held player without a save or a word to anyone. player-export
refuses a held player. Placing it is the import's work, next.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`player-release <uuid> <hand-over> moved [announce]` takes a player that
`player-export <uuid> freeze` froze out of the world, because another
server has it now: the others see its entity and its list entry go, no
player file or record is written, the log and the leave event record the
departure ("moved to another server"), and the connection is closed. The
"left the game" line is neither sent nor logged unless `announce` is
added. `player-release <uuid> <hand-over> abort` lifts the freeze.
Both name the hand-over id the export returned, and are refused for a
player who is not in a hand-over, for another hand-over than its current
one, and for a player who died while frozen. Once the freeze has run out
the hand-over is over and a late `moved` is refused: the player has
played on since, and the target's record is older. So a stale or
repeated release cannot remove someone who is playing again.
A frozen player takes no damage (Abilities.invulnerable, which the void
and /kill still pass) and picks up no items, arrows or experience orbs,
so its record stays true until the release. A player who dies while
frozen all the same ends the hand-over with a warning.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: queued. tab_list passes 1000/1000 on this head on top of main 3d9ad5214 (playhorizon); main has 964/1000 (#9834). The change brings back vanilla's SkinManager.registerTextures allOf rule: a cape or elytra that fails gives the default skin. The retry and the decode-before-cache from #9420 are kept.
"Cosmetics in single player: On" was cut off on the Extras button; the
Cosmetics header already names it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: queued. `python3 tools/parity.py save_roundtrip` on this head merged into main 3d9ad5214 (playhorizon): PASS, 0 unexplained differences; the hoglin's `CannotBeHunted` DIFF from the nightly audit (#9833) is gone, the 35 ignored categories are the same as on main. Matches `Hoglin.addAdditionalSaveData`, which writes the boolean unconditionally. Also closes#9833.
Registry v2 (docs/design/registry-v2.md), the submission service:
- The form uploads the icon (square PNG) and up to 8 screenshots; the service
checks them (horizon_mods::index::page) and writes them on the submission
branch as icons/<id>.png and screenshots/<id>/<n>.<ext>, so the site and the
launcher load them only from the registry. An icon address is refused.
- Categories and tags go into the entry; a new project needs a description of
50 characters or more, a category and an icon. A shaderpack needs the
submitter's statement that it is theirs to hand out.
- "Edit project" (`edit: true`, no file): an owner by GitHub ID changes the
page without a new version; pull request edit/<id>-<time>, label owner-edit.
- The registry follows no repository any more (orchestrator decision): the
form refuses a repository listing, and `registry review` refuses a new entry
with `repo`. Every version is a submitted, pinned file.
- A retry takes up its own branch when every commit on it is the form's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: travelInAir's client branch now sees the real client side and the dimension's min Y, so a client over an unloaded chunk falls at 0.1 * 0.98. The new unit test runs with prepared resources on playhorizon.
🤖 Agent: Join and leave messages now use player_display_name, team prefix and nickname included, as PlayerList.placeNewPlayer's getDisplayName does. Chat already went through it.
🤖 Agent: Each award sits in the branch vanilla counts in (FoodOnAStickItem's PASS, a cast but not a reel-in, trident and armor only on the server). The use_item_on harness's 400 new cases pin them.
🤖 Agent: shadow_ortho reproduces the measured bits with JOML's setOrtho and invertAffine (MIT, credited in THIRD_PARTY_NOTICES). Horizon's own packs keep their depth range, now written as explicit planes.
🤖 Agent: The second texture is decided once at load (doubled), and a single-sided buffer's B is a clone of A's view. The clear of the alt side and the alpha-1 start both skip single-sided buffers, so A is never attached twice in one pass.
🤖 Agent: RandomSource.triangle is mean + spread * (nextDouble() - nextDouble()) in 26.3, and the local copy drew two floats. The cast and fishAngle now take vanilla's draws, and the change merges cleanly with #9740.
🤖 Agent: A rider's sprint now goes through LocalPlayer.isSprintingPossible's vehicle branch (canSprint and local authority), so only camels keep a sprint, and boats and carts stop it as in vanilla. The released-key case is covered by drive_mount reading player.sprinting.
🤖 Agent: FishingHook.<init> draws the throw's three triangle values from the hook's own Entity.random (a fresh RandomSource), so the level's random no longer advances on a cast. #9755 sits on the adjacent lines and merges cleanly on top.
Creeper.tick's PRIME_FUSE, Mob.ate's EAT, Creaking's ENTITY_ACTION
(activate, deactivate, freezing, a heart-bound hit), Turtle.aiStep's and
Wolf.tick's ENTITY_ACTION, Shulker.setRawPeekAmount's CONTAINER_OPEN /
CONTAINER_CLOSE and an evoker's vexes' ENTITY_PLACE are posted, and the
host forwards them (it dropped all but entity_action and flap).
sculk gains `ignite mN` and `ate mN` actions and four scenarios, run
last since vanilla merges experience orbs by global entity id (439/439;
1/4 of the new ones on main's gameplay code). Unit tests cover the
creaking, wolf and shulker.
Part of #9776
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
🤖 Agent: An unsupported initializer such as double(2.0) now leaves its uniform at zero with a note at its file and line, and the program still loads. Supported defaults and conflict errors behave as before.
🤖 Agent: The lead snap now uses the entity-bound packet on NEUTRAL and excludes nobody, as FireworkRocketItem.use's playSound(null, player, ...). The ignored game test passes with prepared resources on playhorizon.
🤖 Agent: The box is drawn only for a shaders/ pack's skybasic program at the far plane, so terrain stays in front and Horizon's own sky is untouched. The design comes from the public shaders.properties docs, nothing copied.
🤖 Agent: With tnt_explodes off, the overlay now reaches the acting player and the item gets PASS, as in TntBlock.useItemOn. Redstone, fire and a finished fuse are gated by the same rule as TntBlock.prime and PrimedTnt.explode.
🤖 Agent: The hint follows Minecraft.tick's isMultiplayerServer: shown on a real server, or once a single-player world is published, never just because the client uses memory networking. joinedFirstServer is saved so it shows only once.
🤖 Agent: Spectators now open neither a block container nor a chest boat with a pending loot table (BaseContainerBlockEntity.canOpen, AbstractChestBoat.createMenu). The boat roll clears LootTable and LootTableSeed from the raw NBT before filling, as unpackChestVehicleLootTable does.
🤖 Agent: The 65536ths rounding is applied only to section origins while a shaders/ pack draws terrain or shadow. Culling, sorting and Horizon's own rendering keep the exact camera.
🤖 Agent: I moved the award into hurt_by_player, which melee, sweep and spear hits all go through. tick_stats runs with the host's statistics, so a guest's absorbed damage went to the server's Game; it now goes to the attacker inside its own as_guest turn. Tenths from other kinds of hits are cleared first, so they never reach the next attacker, and tick_stats no longer walks every mob.
🤖 Agent: The floor to 32768ths runs only in the pack's terrain vertex stage (hz_Vertex), so Horizon's own drawing and at_midBlock stay exact. Whole blocks and sixteenths are already on that grid.
🤖 Agent: The sky now writes gl_FragDepth = 1.0, so the depth test still keeps it behind the world, and gl_FragCoord.z stays the geometry's own. One cost: a fragment that writes depth loses early-Z, so a pack's sky program now runs under terrain as well (Iris pays the same, since it draws the sky first). A noperspective depth varying with the old far-plane z would keep early-Z (follow-up #9857).
🤖 Agent: The slide check reads block cells with the 1e-5 deflated box and the 0.4375 + half-width overlap, as HoneyBlock.isSlidingDown does, and markers and NoGravity stands keep skipping it. The cost is a few cell reads per falling stand per tick.
🤖 Agent: The sweep now ignites each swept player only after mob_hits_player lands, after the knockback, the same order doPostAttackEffects runs in Player.doSweepAttack. It uses the same ignite lookup as the direct hit.
🤖 Agent: A live /data write now restarts the wolf's anger counter from anger_end_time at the level clock, the same conversion load_mob uses, so a future end no longer saves as -1. The resource-backed game test passes on playhorizon.
Entering a world opened the split's gilrs and closed P1's. gilrs opens a pad twice
at once, which inotify can merge into one event, and closes it twice apart. KWin
counts those events to pause its emulation, so its count could reach zero while
Horizon still held the pad, and the left stick also moved the mouse.
The router now takes P1's reader and gives it back. Its reader also gets the extra
SDL rows, which it was missing.
Refs #9730
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon's registry now serves the Data packs tab (beside Modrinth): a
`datapack` project is checked against the index like every registry file,
then goes into the world picked under the search, through the same
install_datapack flow Modrinth's use. On the registry, Filters lists the
kind's categories (horizon_mods::index::page::categories), search words
also match a project's tags, and a project's page shows its categories.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The empty map_decorations is dropped only for minecraft:filled_map, after components_tag returns, so the stone and empty-map controls and the explicit !map_decorations removals still save as before.
🤖 Agent: skin_slots.forget() runs on both renderer swaps (entering and leaving splitscreen), so the kept skin images upload again to the new atlas, the same way they do after a device loss.
🤖 Agent: An infinite effect (duration -1) now shows effect.duration.infinite, as MobEffectUtil.formatDuration does after addPotionTooltip's endsWithin(20) check. A named cushion keeps CustomName in its NBT and on the dropped item.
🤖 Agent: Only HostEvent::Left takes the Disconnected removal: cursor and grid inputs are dropped before the guest is saved, and the untaken result is not. A live close still puts items back in the inventory.
🤖 Agent: The coverage report keeps its English text through Row::plain_text, and the launcher translates the fixed sentences plus the separate waiting count. An older report without the waiting field still shows its own text.
🤖 Agent: shadowAngle keeps its bits, and the matrix now turns by (shadowAngle - 0.25) * 360 in f32 (+360 when negative). The test pins Iris's bits at 7297, 7300, 16000 and 7303 with sunPathRotation 30.
🤖 Agent: Nothing outside the template lists changes: the content root and MobTypes::load already accept any template's species. The new test checks that a mod trader saves the same keys as a vanilla trader.
🤖 Agent: I moved the EntityAttachment.NAME_TAG doc comment back onto name_tag_at (collect_vehicle_ids had been inserted between it and its function). The vehicle set is built once per frame and stays linear in the entity count.
🤖 Agent: The wait now ends when the held job leaves the queue, so drop(reg) always has a running job to wait for. The 10 s bound fails with a message instead of hanging.
🤖 Agent: moving_slowly now follows LivingEntity.isVisuallySwimming, which also counts a FallFlying pose with fall_flying off. The 0.0196 against 0.00588 acceleration in the body is exactly that 0.3 factor.
🤖 Agent: The palette pick matches StructureTemplate.getRandomPalette with settings.getRandom(pos) (a LegacyRandomSource seeded with Mth.getSeed), and ids with a . or .. part are refused before template_entry reads the world's generated folder.
With the vanilla data present the untamed pair never bred, so the
test failed; without it the test returned early and passed vacuously.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The loopback default only applies through lab.game_env and the HORIZON_TEST_LOOPBACK=1 variable, so a player's LAN keeps binding 0.0.0.0. On Windows the launcher now copies the version folder (minus .local) into run/<home hash>/game on every start; that is a launch-time cost, not a per-frame one.
🤖 Agent: The two new Realms calls go through the existing RealmsClient (GET worlds/{id}/subscription, DELETE worlds/{id}), and Delete is only offered on an expired Realm, behind its question, as RealmsSubscriptionTab does.
LivingEntity.handleFallFlyingCollisions's playSound of getFallDamageSound runs on both sides; only the hurt is the server's alone. Horizon pushed the sound event only from the server-side run, and a glider's own travel is client-side (and the server broadcast excludes the glider), so the glider never heard entity.player.big_fall / small_fall.
Co-Authored-By: OpenCode <[email protected]>
point() removed the `current` junction before making the new one, so a
failed mklink left the server with no current release, and std quotes an
argument only when it holds a space, so a folder like D:\mc&co\ reached cmd
unquoted and & split the command.
- The new junction is made first as current.switching; the old one is
removed and the new one renamed into place; if that rename fails the
old junction is put back.
- mklink runs as `cmd /D /S /C "mklink /J "link" "target""` with
raw_arg, both paths quoted; paths holding % or " (which keep a meaning
inside quotes) are refused with a message.
- Test (Windows only): real junctions in a folder named with `&`:
current moves v1 -> v2 and reads each release's file, the old release
is untouched, no current.switching is left, and a release whose path
holds % is refused with current unchanged.
Compiled, linted (clippy -D warnings) and tested on GitHub's
windows-latest runner: all 146 horizon-server tests, the two junction
tests among them. The Unix build and tests pass too.
Split from #9129.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
🤖 Agent: the compatibility point is the extension check: released launchers refuse the whole index when a version's extension mismatches its kind, which is why .zip shader-folder packs are a new 'shaderpack' kind instead of riding on 'shader'; all new entry fields stay optional until HORIZON_REV moves.
SkinManager.registerTextures joins the skin, cape and elytra downloads with
CompletableFuture.allOf, so any one failing (or a URL with no hash) fails the
profile's PlayerSkin and createLookup shows the default skin. #9420 kept the
skin when only the cape failed; tab_list caught it (36/1000 lists, #9834).
The retry of failed downloads and the decode-before-cache from #9420 stay.
Fixes#9834
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's FluidRenderer puts a fluid top's corners NW, SW, SE, NE and the
quad indices join corner 0 to 2, so every top is split north-west to
south-east. Our probe packs' readings of Iris 1.11.7 fit this rule: a top
is split along the diagonal whose corners differ more in height, and
north-east to south-west when its north-west and south-east corners are
level. It is a fit, not Iris's code. Block faces keep vanilla's split in
Iris too.
Under a shaders/ pack (Mesher::shader_pack, any loaded pack), a top that
Iris splits the other way now starts from its south-west corner: same
winding, its 0-2 diagonal running south-west to north-east. Horizon's own
drawing keeps vanilla's order.
The fit covers every lava top we read, every still top but one, and 317
of 343 water tops; the misses are undiagnosed.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Package installs (/usr, /nix/store, /app, not /usr/local) lose Download again and get package-manager advice, and other stuck copies are told to move to Downloads. The path check now runs once per launch instead of on every sidebar draw.
🤖 Agent: The window backdrop is baked once per generation/viewport at main's 4 px texel density with a one-texel border, so scrolling no longer rebuilds a piece per plate. The scene's animation doesn't bump generation, so idle frames do no extra work. The layout/dropdown changes were reverted (duplicate glass Sort by, design-system radii).
TntBlock.useItemOn sends block.minecraft.tnt.disabled to the player who
clicked (ServerPlayer.sendOverlayMessage), and returns PASS, so flint and
steel or a fire charge then lights a fire as its useOn does. The overlay
went to the server thread's own HUD, which no player sees. The overlay
helper moves from debug_stick.rs to titles.rs for both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Project live effect IDs, amplifiers and durations into the existing view.
Cover named if/unless predicates and actual Game view construction.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: zip 2.6.1 was only the registry's test fixture writer (uploads already went through horizon-mods' 9.1.3); the new pre-check runs zip 9's validate_header_consistency, which by_name/by_index_raw skip by default, so a cloaked local header is refused before any kind's reader opens the upload.
zip 2.6.1 (Dependabot #9 critical, #23, #24) was the registry's dev-only
fixture writer; the service already parsed uploads with horizon-mods' zip
9.1.3. Move zip to a normal dependency at 9.1.3, the version horizon-mods
and the main workspace use, so the lock holds one zip.
Before any kind's reader opens an upload, src/archive.rs now refuses a
local header that disagrees with the central directory, duplicate and
case-insensitive duplicate paths, overlapping entries, symlinks and other
special files, encrypted entries, over 100 000 entries and over 1 GiB of
declared uncompressed bytes. Tests craft each case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Route Ender Dragon spawn eggs through the dedicated dragon entity implementation for hand and dispenser spawns.
Fixes#9799
Co-Authored-By: Codex (GPT-5.6) <[email protected]>
Step 1 of registry v2 (docs/design/registry-v2.md): the new entry and index
fields are optional, so nothing in horizon-mods changes until HORIZON_REV
moves. Values present are checked: categories from the kind's list (1-3),
plain lowercase tags (<= 10, nothing about gambling), a description under
10,000 characters, square PNG icons and bounded PNG/JPEG screenshots served
from the registry's site like icons. Data packs (pack.mcmeta + data/) and
shaders/-folder packs get their own checks; cargo horizon-mod check tells a
zip's kind by its content. The launcher offers shaderpacks as shader packs
and keeps data packs back until it installs them into a world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractChestBoat.createMenu returns no menu for a spectator while the
boat's lootTable is set, so the table stays for the first player who
opens it, as BaseContainerBlockEntity.canOpen does for blocks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
sign_text_calls runs for every visible sign every frame. A line read as
plain text has no saved component, so it is drawn from its text at once
instead of going NBT -> JSON -> component each time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The version cards' radius and spacing and the mods sort dropdown are
back to main's:
- glass already draws its own Sort by beside the search
(glass_mods_sort), so showing mods_sort_dropdown in glass gave two
buttons sharing one sort_menu flag;
- the classic theme is square (theme::RADIUS = 0, "Blocks, not rounded
cards" in docs/design-system.md);
- versions are panels, and panels take radius::PANEL (20).
The two Glass/Float bake branches are folded into one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
package_managed_launcher() canonicalized the update target on every
sidebar draw; the launcher doesn't move while it runs, so keep the
answer in a OnceLock. A launcher an admin copied to /usr/local/bin has
no package manager to update it, so it gets the move-it advice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: docs only (docs/design/registry-v2.md); the one rollout risk it fixes is the order: optional entry fields and HORIZON_REV bump before any entry uses them, since the pinned cargo horizon-mod has deny_unknown_fields.
GameLevel borrows guest_boxes outside a guest turn instead of cloning them,
and for_turn only searches the few borrowed ancestors, so a turn costs
O(players) rather than O(players^2) for each guest every tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tick_stats runs with the host's statistics in place, so draining the mobs
there credited every guest's absorbed damage (and on the server thread, the
single player's own) to the server's Game instead of the player who hit.
hurt_by_player, which melee, sweep and spear hits all go through, now takes
the hit's tenths right after the damage, inside the attacker's as_guest turn.
Tenths left by hits that came another way are cleared first so they are never
credited to the next attacker, and tick_stats no longer walks every mob.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HangingEntityItem, ArmorStandItem, EndCrystalItem and MinecartItem post
ENTITY_PLACE where vanilla does; an ender chest's first opener posts
CONTAINER_OPEN (ContainerOpenersCounter.incrementOpeners).
On the same paths: each counts ItemStack.useOn's used statistic; a
painting or frame that can't hang there is CONSUME (counted) or FAIL as
in vanilla; paintings pick their variant and placed crystals their time
with their own random, not the level's; a placed armor stand falls onto
what is under it (EntityType.getYOffset with movedUp) after drawing
create's spawn yaw from the level random.
use_item_on gains placement cases (4800/4800; 4474/4800 without these
gameplay changes). The oracle's stand-in level gets hasEntities; the
fixture clears and prints crystals, paintings and frames.
Part of #9776
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
collect_vehicle_ids was inserted between name_tag_at's doc comment and the
functions below it, so rustdoc gave the name-tag height text to
collect_vehicle_ids. Move it back above name_tag_at.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Checked against main: HORIZON_HOME (logging::game_home) holds logs and crash reports but also telemetry/consent.json, perf reports, recordings/repro and the DX12 shader cache, while saves and options.txt follow the working directory or --game-dir (game.rs set_current_dir). I pushed 3eeb29cda to name those in the parenthesis so the rule doesn't read as "HORIZON_HOME barely matters".
🤖 Agent: the Linux fallback reuses vulkan_start_crashed unchanged and only rewrites graphicsApi when it is auto, so an OpenGL start that crashes again in Vulkan can't flip the setting back and forth.
game_home() also keeps telemetry consent, perf reports, recordings and
repro bundles, and the DX12 shader cache, so "logs and crash reports
only" undersold why the scratch HORIZON_HOME matters.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clippy rejects `if count == 0` on that arm; the pattern `count: 0` is the same match.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
In single player the window's own server names the player by their
offline profile, which the cosmetics service doesn't know, so the
player's own cosmetics, name tag style and Horizon mark never showed
there (and the game asked the service about the offline UUID for
nothing). Cosmetics in single player (Extras > Cosmetics, off by
default, en/fr/de) asks by the signed-in account's UUID instead. Off,
the own model asks and draws nothing in single player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/v1/worn takes no sign-in, so anyone can learn whether a listed player
played Horizon in the last 3 hours: the tooltip (en/de/fr) and
docs/cosmetics.md now say so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A flat fang pitch, a powered wither's missing head particles, silent advancement loot and /xp levels, and hurting projectiles with no bubbles in water are all visible on the server-thread client.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Composite-style programs read a buffer's "main" side and draw into its
"alt" side; gbuffers, shadow and final programs write in place, compute
programs through images. A buffer no prepare, deferred, composite or
shadowcomp program draws into or flips never touches its B texture, so
B is now a second view of A for those buffers. The alt-side clear and
the three-channel start skip such buffers so A isn't attached twice in
one pass. At 3440x1440 this saves 35 to 224 MiB with BSL, Complementary,
Bliss and Super Duper Vanilla, most of it in shadowcolor buffers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each table gets the English text, as the other new strings do until
someone translates them. The gfx.rs comment stays as it was, so the
change touches the launcher alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HIT_GROUND carried no affected state, so a jump or fall on wool reached sculk
sensors and shriekers. It now carries mainSupportingBlockPos's state, as
Entity.checkFallDamage does, for players, mobs, items and armor stands.
STEP read the block 0.2 below the feet, which is the block under a carpet. It
now reads the state at Entity.getOnPos, as applyMovementEmissionAndPlaySound
does.
tools/test_server_wool_vibration_parity.py compares both servers through a
vanilla-codec client.
Fixes#8036
Co-Authored-By: Claude Opus 5.5 <[email protected]>
0.5 × 255 is 127.5, which drivers can round either way: NVIDIA gives 127. The test
checks that the surface reads back exactly, so 0.75 (191) checks the same thing.
Fixes#9831
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Windows a native crash in the Vulkan driver at the renderer's start
switches Auto to DirectX 12 (#746). Linux had no such rescue: since #4064,
Auto runs Mesa's hasvk on Ivy Bridge and Haswell, which Mesa calls
incomplete on Ivy Bridge (#4293), and a crash inside it would end every
start with no crash report and no fallback. The same switch now sets
OpenGL on Linux (crocus or iris through EGL, which presents to the window
since #2875). A player's own choice stays, so it never loops.
Refs #4293
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mods() lists trait mods only, so returning early when it was empty kept
a bundle injected at the toast_add hook point from ever running.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
gbuffers_skybasic and gbuffers_skytextured put every vertex at the far
plane (gl_Position.z = gl_Position.w), so gl_FragCoord.z read 1.0 on every
sky fragment. Our probe packs read the sky disc's, the sunset fan's, the
stars' and (we take it) the sun quad's own perspective depth there in Iris 1.11.7.
The vertices now keep their depth, and the translator writes
gl_FragDepth = 1.0 before the pack's main (GbuffersInputs::far_depth), so
the depth test still sees the far plane: the sky shows only where the
world left nothing, and a pack's own gl_FragDepth still wins.
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Over 76 scenes of our probe at fields of view 30 to 110, Iris 1.11.7's
shadow maps keep every section whose box needs a growth of up to 1.1224
blocks to meet the casters' shape, and drop every one that needs 1.1252 or
more, whatever the field of view. With #9707's 1.15, main's maps held 20
columns Iris drops, in 18 of those scenes; with 9/8 they hold exactly
Iris's columns in all 76 (a fit, not Iris's code).
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
server-admin links #9767 and #9748 (merged), so sync ships it once a release has them. The panel's later
features and vanilla's management protocol become server-panel-management (Later). sync also ships
languages (v0.2.65), end-city-banners (v0.2.67) and end-city-loot (v0.2.73).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla awards Stats.ITEM_USED in each item's own use/releaseUsing/useOn
(or in ItemStack.useOn); Horizon's ports left it out. The bow, crossbow,
trident, fishing rod (cast), eye of ender, boats, carrot and warped
fungus on a stick (no boost), empty map, equippables swapped from the
hand, books, flint and steel / fire charge on TNT, firework boosts and
the spyglass now count it, in vanilla's branches.
On the same paths: fire starters lighting a fire count the use and post
BLOCK_PLACE / BLOCK_CHANGE; sneaking with one on TNT lights a fire
instead of priming it; a placed boat posts ENTITY_PLACE; the spyglass
sound plays at the feet.
use_item_on gains used-statistic cases (4400/4400; 4054/4400 on main's
gameplay code). The oracle's stand-in player gets what these items read.
Fixes#9775
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
The configuration screen's Players tab: the invited count, a row per player with
Op / Deop / Remove (the removal asks first), and Invite Player behind it.
RealmsInviteScreen: a name field whose blank or already-invited values are refused
before any request, the service's answer deciding otherwise.
RealmsClient gains getOwnRealm (GET worlds/{id}, the slots and players),
invite (POST worlds/{id}, OutboundPlayer's JSON), op (POST worlds/{id}/{uuid}),
deop and uninvite (DELETE worlds/{id}/{uuid} and worlds/{id}/invite/{uuid}), all
with the UUID as UndashedUuid's 32 hex digits. The tab reads again after an
operator change or an uninvite.
The Invite Codes button and its screens, and the player faces, follow in their
own PRs (the faces need the skin-face mechanism).
Co-Authored-By: ZCode <[email protected]>
Retain template patch presence so explicit removals and zero-valued
components override the input without losing inherited data. Preserve
transient map postprocessing through assembly and add powered-crafter
and resource-free regression coverage.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A sign line kept only its plain text, so a server's {"text":"Online",
"color":"green"} drew in the sign's dark dye colour, as did `§` codes
(drawn as a raw `§`) and hex colours.
SignText now keeps each line's saved component beside the text it showed
when read, so the style survives loading, saving and the network. A line
edited since is a literal in the old line's root style, as
SignBlockEntity.setMessages makes it. List components and translate
components no longer lose their text.
sign_text_calls draws a line as one call per run of one colour: a glyph
takes its style's colour, else the sign's (Font.StringRenderOutput), with
`§` codes resolved as the font does. A line with no style is still the one
call it was. The outline of glowing text stays the dye's dark colour.
Bold, italic, underline, strikethrough and obfuscated text are kept but
not drawn yet.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Read back with a probe pack of ours, Iris 1.11.7's terrain gl_Vertex puts
each vertex's place in its section on the 32768th of a block below it:
water tops (8/9 - 0.001) at 29094/32768, plants offset by 13/15 and 0.1 at
28398/32768 and 3276/32768, on all three axes (a fit, not Iris's code).
Horizon passed the mesher's floats on, up to 1/32768 block from Iris's,
enough for a shadow-map coordinate on a texel boundary to fall the other
way. The pack's terrain vertex stage now takes the position down to that
grid; whole blocks and sixteenths are on it already.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Animal.finalizeSpawnChildFromBreeding draws the orb's 1-7 from the breeding parent's own random, and only when mob_drops is on. Horizon drew it first and from the level's random (llamas always, the other animals from world.random): with the rule off that consumes a draw vanilla does not make, and outside llamas, turtles and frogs the parent's and the level's later draws drift from vanilla.
Co-Authored-By: OpenCode <[email protected]>
The kernel defines BTN_X as BTN_NORTH and BTN_Y as BTN_WEST, and gilrs' default
mapping takes those names literally, so a pad with no SDL row had X and Y crossed.
Sony and Nintendo pads send the same codes by real position and stay as they are.
Closes#9813
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Generated with tools/stamp_native_hooks.py --only gui_render_post client_services. Leave the other 18 fragments pending while their contracts are reviewed.
Co-Authored-By: Codex GPT-6 <[email protected]>
Keep unselected contracts pending, preserve merge order within the selection, and reject unknown names before writing.
Co-Authored-By: Codex GPT-6 <[email protected]>
The upper sky disc leaves directions near and below the horizon uncovered.
Shader packs keep the buffer's clear colour there, producing a sharp sky
band and pale distant terrain during fog compositing.
Draw an enclosing sky background before the existing disc. Keep its depth
at the far plane and preserve the foreground's colour and depth. Add an
original GPU probe covering 20 camera orientations and foreground occlusion.
References and provenance:
- Iris public format reference for gbuffers_skybasic:
https://shaders.properties/current/reference/programs/gbuffers/
- Public rendering-uniform and depth-buffer references:
https://shaders.properties/current/reference/uniforms/rendering/https://shaders.properties/current/reference/buffers/depthtex/
- Complementary's public user guidance, changelog and option descriptions.
- Java 26.3 + Iris 1.11.7 and Complementary Reimagined r5.9.3 were run
as comparison inputs. The diagnostic shaders and Rust changes are original;
no Iris or Complementary implementation source was opened or copied.
- After implementation and testing, PR descriptions and closure comments
(including #8753) were consulted to check contribution provenance policy.
No patch from those PRs was opened or reused. This is not a fresh-context
or no-documentation implementation.
Validation: fast game build; 16 depth GPU tests including the new regression
on RX 9070 XT; renderer library Clippy with warnings denied; rustfmt.
Visual comparison used isolated game homes, the same world and camera,
32-chunk render distance, noon and clear weather.
Co-Authored-By: Codex <[email protected]>
Cushion.getCushionItemStackWithData copies the entity's CustomName onto
the dropped item, as the item frame beside it does; the cushion carried
no name at all. Add it, read and write it with the entity, and drop it.
PotionContents.addPotionTooltip appends MobEffectUtil.formatDuration,
which answers the infinite sign for an effect that never ends
(duration -1, from commands or datapacks); brewing's effect_lines only
appended a time past 20 ticks, so such an effect showed none.
🤖 Agent: Queued. Test-only: it pins the server MCP's tool list, input schemas and error replies as a golden, so a renamed tool or field that breaks agents' calls now fails a test. Pinning that is right here, because MCP clients rely on those names.
🤖 Agent: Queued. The new queue drain runs only on split screen's present path and before a new window's surface configures, so single-window play has no extra wait. martipops's two review points are answered in 9fe888b76.
lists.rs promises vanilla's console lines; the new ban/pardon/ban-ip/pardon-ip replies used other words.
Now commands.ban.failed, commands.pardon.failed, commands.banip.success/failed/invalid and
commands.pardonip.success/failed from 26.3's en_us.json; goldens updated to match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CrafterRecipeCraftedTrigger tests a ServerPlayer's bounding box against
AABB.ofSize(center, 17, 17, 17); Horizon tested the eye within 9.5 on
each axis. Use the player's box and 8.5 to each face, so the reach is
the width and height reaching into the box, not a point 9.5 away.
With the rule off, TntBlock.prime leaves the block alone: redstone, fire
and an unstable block breaking go through the level event, which now
refuses like a native mod does (the block comes back); flint and steel
says block.minecraft.tnt.disabled; and a fuse already lit goes out
without exploding, as PrimedTnt.tick does.
ContainerEntity.unpackChestVehicleLootTable rolls the table saved in the
boat's NBT when a player opens it. Horizon read the table nowhere, so a
boat made by a command or a mod stayed empty and kept it. Roll it into
the slots on the open (only when they are empty), clear it from the raw
NBT, and fire player_generates_container_loot for the opener.
Hoglin.addAdditionalSaveData writes putBoolean("CannotBeHunted", ...) every time, so a hoglin that can be hunted saves 0b instead of dropping the tag. save_roundtrip passes again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BaseContainerBlockEntity.canOpen refuses a spectator while the container
still has a loot table to unpack; spectator_open_menu skipped that, so a
spectator could open an unrolled chest. The pending roll lives in the
chunk's saved rolled_loot until the first open consumes it.
The Controller setting is P1's own. A guest copied it, so after joining
from a pad its own pad was dead - on the profile screen it joins into,
with no way to pick one. Enable the controller in the joining player's
settings when the join came from a pad.
OptiFine's shaders.txt puts gtexture on texture unit 0 in gbuffers programs and
colortex0 there in deferred, composite and final; its gbuffers table has no
colortex0 to colortex3. Our probe packs, run in Iris 1.11.7, read the block atlas
in gbuffers_terrain through a sampler name of their own and through each of
colortex0 to colortex3 and gnormal, and the scene in colortex0 in deferred,
composite and final through a name of their own and through gtexture, tex and
texture, also where the program declares no colortex0.
Horizon read its white stand-in for all of these, or, in gbuffers, the buffer.
With this, PotatoShaders' blocks keep their textures and its cave and Nether
come out as bright as in Iris.
Fixes#9763
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Route CLI moderation through file-backed lists when no game is running, pin vanilla-style replies, and document the stopped-server behavior.
Fixes#9753
Co-Authored-By: Codex (GPT-5.6) <[email protected]>
Iris 1.11.7 disables a built-in uniform whose declared type is not the one it gives
("Wrong uniform type ... Disabling that uniform"), and the program reads 0. Our probe
packs saw this for floats declared int or vec2, ints declared float, ivec2s declared
vec2, int or ivec3, vec3s declared vec2 or vec4, a mat4 declared mat3, and uint; ints
and bools declared bool or int were kept. Horizon converted the value instead.
Fixes#9758
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Our probe packs read gtexture in gbuffers_terrain, and the block atlas as a custom
texture in composite, at levels 0.5 and 1.25 in Iris 1.11.7: each read was the two
nearest levels mixed (nearest texels within a level). Horizon's sampler took the
nearest level only. The atlas now gets a sampler of its own with linear mipmap
filtering; normals and specular keep theirs.
Fixes#9760
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
minecraft:wandering_trader joins the zombie and the villager as a living
template. It is the neutral one whose behaviour is all goals, so a native
mod's register_goals can replace every bit of it: a citizen, a guard or an
NPC, which a zombie (hostile, burns in daylight) and a villager (its brain
stays) can't be. Saving and entity data already follow the template's
class (class_id); the tests now cover a trader type too.
Part of #8622.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fishing.rs kept its own triangle on two next_float() calls; vanilla's
RandomSource.triangle draws two doubles, which also advances the random
differently. Use the core method.
Clearing the window and drawing the text straight into its DC shows the blank
frame in between, so the boot screen flickered each time the console's lines
arrived. The paint now goes to a compatible memory bitmap and reaches the
window as one BitBlt, the same frame the text was drawn into.
Co-Authored-By: I114rk's AI agent (OpenCode) <[email protected]>
With shadowDistanceRenderMul set, the shadow pass drew only the chunks
within a circle of ceil(shadowDistance x mul / 16) chunks. Our probe pack's
Iris 1.11.7 shadow maps keep the sections whose box, grown by the same
1.15 blocks as for the casters, meets a cube of shadowDistance x mul around
the camera: out to the square's corners, a section 1 block past a side but
not 1.5, and nothing 70 blocks below a camera flying 50 blocks out. Over
12 scenes at shadowDistance 100 and mul 1, main dropped 33 columns Iris
keeps.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Relands the frames on their submit threads at the top of Split::present:
a reconfigure there (a present mode switch, a new compositor, an
Outdated texture) waits for the device and errors on a submission made
meanwhile, and the compositor then copies views already submitted.
Simpler than gating on will_reconfigure, per martipops's review.
Preserve the existing frame type during id-free live NBT adoption. Compare
count, item and components and remove the old map association before
replacement. Cover the Game command route and immutable-type adoption.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
LocalPlayer.aiStep runs its sprint decisions for a passenger too, but
the client only ran them on foot: `canSprint` and the local authority
stand in for food there. Run them for a vehicle with those conditions,
and give the ridden mount the controller's sprint state instead of the
raw key, so a double-tapped sprint starts and letting the key go keeps
it (Camel.getRiddenSpeed reads isSprinting()).
Read back with a probe pack of ours, Iris 1.11.7's terrain gl_Vertex is
each block's position minus the camera with its fraction rounded to the
nearest 1/65536 block (seven cameras, up to 40000 blocks out), while
cameraPosition and the matrices keep the exact camera. Horizon drew from
the exact camera. Off by up to 2^-17 blocks, fragments on a texel boundary
of the shadow map read the other texel, and depths at the shadow test's
bias fell the other way: with this, our lit probe's depth ties at noon
go from 541 to 1 and its neighbouring-texel reads in y from 154 to 8.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dropping_a_registration_waits_for_its_running_job slept 100 ms and
assumed a worker had taken the held job by then; on a busy runner it
had not, so the drop had nothing to wait for and returned early. Wait
until the job leaves the queue, and fail loudly if no worker picks it up.
Graphics settings move their frame, list and key steps into settings_shell, with no change to how they behave,
and Controller Settings is its second user: General, Bindings and Device pages, search, per-row reset and Undo.
A controller notice is one slot per view rather than the split screen's own. A pad-focused options tooltip hangs
from the widget's bottom left, as the pad's hidden pointer sits mid-widget; mouse and keyboard keep vanilla's. A
focus whose widget goes away on the same screen moves beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Surface::configure waits for the device to come idle and errors when a
submission lands while it waits: with the submit thread on (Linux by
default), adding a player could crash the game in Split::present's
set_vsync, in a new compositor's first configure, or on an Outdated
texture. Compositors now say when a reconfigure is coming or owed, and
Split drains every player's queue at those points only, keeping the
overlap the submit thread is for the rest of the time.
LivingEntity.stopSleeping asks AbstractBedBlock.findStandUpPosition for
the entity's own type. The player's port of that search moves from
game/respawn_anchor.rs to stand_up.rs, generic over the entity type
(dimensions, player, fire immune), and villager::stop_sleeping uses it
with the villager's box and yaw, and vanilla's fallback. Villagers now
step onto shelves, slabs and carpets, pick vanilla's side of the bed,
and fall back to the top of the bed instead of staying lying on it.
WorldBorder.isWithinBounds(AABB) takes 1.0E-5F (a float) off the box.
use_item_on gains villager wake-up cases: 4400/4400 match.
Fixes#9502
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
LocalPlayer.isMovingSlowly is also true while the player is visually
crawling: LivingEntity.isVisuallySwimming counts the fall-flying pose a
glide that just ended still holds. Horizon only counted the swimming
pose, so on the first air tick after a glide its input took the full
0.98 x 0.02 forward acceleration where vanilla's takes 0.3 of it: GrimAC
flagged Simulation 23 times on every elytra landing with jump held, at
an offset of exactly the difference (0.01372). Vanilla's client is clean
on the same input.
Fixes#8295
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #8622 (item 4, structure placement for builders). A new helper,
native_structure_template(id, at, rotation, mirror) (Host::structure_template),
finds a structure template as StructureTemplateManager.get does (the world's
saved structures, then the game's and the content mods' data) and answers
what /place template would place at `at`: the size after the rotation and
each block's position, mirrored then rotated state and block entity data,
in StructureTemplate.placeInWorld's order and from the palette it picks
there. Nothing is placed: a builder places the blocks a tick at a time with
native_set_block. Server only; ids with a . or .. part are refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Save an opt-in pregen switch, apply it to integrated servers immediately, and refresh command completion while preserving --pregen and permissions.
Co-Authored-By: Codex (GPT-6) <[email protected]>
Read back with a probe pack of ours in Iris 1.11.7, a shadowNearPlane or
shadowFarPlane the pack sets is the orthographic projection's plane as it
is, with no 100-block offset; an unset near plane is -100.05 and an unset
far plane 156, each on its own. Horizon offset both by 100 blocks, so a
pack that sets either got another depth range. The projection's bits
(13 settings) fit JOML's setOrtho with zZeroToOne, its depth terms taken to
-1..1, in floats; the inverse is JOML's invertAffine. Before, the default
matrix's [3][2] was 4 ulps off and its inverse's signed zeros differed.
Horizon's own packs keep their depth range, now given as planes.
Adapts JOML 1.10.9's Matrix4f.invertAffine and setOrtho (MIT;
THIRD_PARTY_NOTICES.md).
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read back with a probe pack of ours (25 ticks of day and night,
sunPathRotation 0 and 30, four view directions), Iris 1.11.7's sky
positions fit (0, 100, 0, 0) times gbufferModelView turned by JOML
quaternions in floats: rotationY(-90°), then for a body
rotationZ(sunPathRotation) and rotationX(its sky angle). Turned by
matrices, as Horizon did, the sun read 4e-6 where Iris has an exact 0
(e.g. at sunset), and the rest was 1 to 2 ulps off. All 50 readings of
all three now match, and shadowLightPosition with them.
Adapts JOML 1.10.9's Quaternionf.rotationX/Y/Z, Matrix4f.rotateAffine
and Vector4f.mulAffine (MIT; THIRD_PARTY_NOTICES.md).
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Route the separately modeled stand through HoneyBlock movement after travel. Own current Game/fresh Java gate113/126->126/126, raw exact with unchanged scripts. Canonical travel regression old-fail/fixed-pass; honey suite11pass/1ignored. Broad readiness Rust/doc builds blocked by disk; formatting and Clippy pass.
Co-Authored-By: Codex <[email protected]>
Iris 1.11.7 turns shadowModelView by (shadowAngle - 0.25) * 360 degrees in
floats, plus 360 when negative, not by the light body's own sky angle. Read
back with a probe pack of ours at 25 ticks of day and night (7295 to 7305
one by one), with sunPathRotation 0 and 30, this fits all 50 readings to
the last bit; the sky angle misses 16 of them by 1 to 6 ulps, which moves
the shadow map's texel boundaries.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep same-machine test sockets on loopback and stage launcher-managed games
at a stable executable path per game home. Preserve runtime dependencies and
protect running installed/native versions from replacement and cleanup.
Refs #9685.
🤖 Agent: with no mods the new hook costs one HOOKS.get() and a walk of an empty mod list per mob created, not per tick, and it runs before the float check, so a mod's goals::Float floats the navigation as FloatGoal does. The fragment stamps cleanly against main (test_stamp_native_hooks passes in pr_ready's Python tests). Pushed 9861c8352: living_mod_types now uses a temp folder per call, since parallel tests were deleting each other's folder. Merged with main, pr_ready --quick passes on Linux (4331 tests, launcher included).
set_paused deliberately lets the world run while splitscreen players
play, but pauses_game did not know about them, so the host window's
client ticks stopped behind its own menu: a laggy world and a rush
forward on resume. pauses_game is now false while split players are
joined, mirroring the server; scenario_pause checks both players'
tick counters against a solo game.
🤖 Agent: the sting now lands inside the fish's own tick (Pufferfish.aiStep → touch), so a target that ticks later in the same level tick counts its poison down that tick: 120→119→118, as in vanilla. Pushed 82648da3f, which removes the end-of-tick arm that could no longer run. Merged with main, pr_ready --quick passes on Linux (2863 client tests). The pufferfish contact harness is left to CI's parity run.
🤖 Agent: the close paths (close_container, close_book) run while the fake inventory is still swapped in, so what a menu held goes back to the fake player and not to the server's own state. The ender chest's stand-in is put back afterwards, as a guest's turn does. Merged with main 27b9d342f (690 commits newer than its base), pr_ready --quick passes on Linux and the 4 ignored native_machines tests pass with prepared resources: trader, vehicle chests, shulker and ender lids, the restored state.
🤖 Agent: GPU culling stays off by default and DX12 frames still don't occlude, so only players who switch it on, on Vulkan, get the new passes. The new runs stamps survive a buffer regrowth (reserve_keeping copies the old buffer), so a grown table doesn't send every section back to phase 2 for a frame. On main 27b9d342f: fmt, clippy and 2419 client and render tests pass on Linux. The device tests of the box test itself aren't there, as the body says. Pushed dc3100917: a doc comment moved back onto its test.
Tests run in parallel in one process: with three callers now (lan.rs and
two in mods.rs), one could remove the shared folder while another was
reading it (NotFound in a_mods_register_goals_replaces_its_types_goals).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Convert the saved absolute end at the current level clock when adopting live
wolf data. Keep Bee state and the retained runtime fields unchanged. Cover
actual Game adoption and command writes with a separate NoAI drift diagnostic.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: the 1.15-block margin is applied only to the caster-shape test, and the light frustum still tests the plain box, so the shadow pass can only keep more sections at the wedge's edges, never cull ones the light reaches. It costs two Vec3 ops per section test, only with a shaders/ pack's shadow culling. On main 27b9d342f: fmt, clippy and 1442 render tests pass on Linux.
🤖 Agent: the only outside code is JOML 1.10.9 (MIT, the library vanilla ships), credited in both file headers and THIRD_PARTY_NOTICES.md; Iris was only run as a black box with our own probe packs, so nothing from an LGPL source. The cost is a few float matrix ops per pack frame, and gbufferModelView now takes vanilla's Camera.getViewRotationMatrix path to the bit. On main 27b9d342f: fmt, clippy and 1443 render tests pass on Linux, the two last-bit tests included.
A client mod hears each new toast just before it is queued and may
cancel it: native_toast_add(kind, id, cancel). Kinds are advancement
(the advancement id), recipe (the first recipe id), tutorial (the
hint), system (narrator_toggle, pack_load_failure, pack_download,
low_disk_space, chunk_load_failure, chunk_save_failure) and
now_playing (empty id). Updates to a toast already queued or shown
are not adds. A cancelled toast isn't queued, shown or sounded; a
cancelled tutorial hint is kept off the slots so it isn't asked again
every frame. The startup menu's narrator toast is not covered: it
shows before Game, the hook's host, exists.
Fixes#9710
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BYf41fgTMRfUEXn3afqyTG
🤖 Agent: a menu frame without a controller does no new work: pad_menu_frame returns before the new resize/scroll-follow code unless the pad (or the keyboard focus) is in use, and the only always-on addition is one widgets::scroll_area push per drawn scrollbar. The superflat preset change matches vanilla (focus selects via AbstractSelectionList.setFocused, only Entry.select fills the share box). pr_ready --quick on main+PR on Linux: fmt, clippy, 4396 tests pass (the one Python failure is install.sh needing uname, absent from NixOS's PATH). tools/pad_menu_e2e.py not run: --offscreen still opens a winit window and needs a display.
the_shaders_validate was inserted between that test and its doc comment,
which then documented the new test instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every PufferfishSting comes from a mob's tick (sting_touching_mobs), and
apply_brain_events consumes it right after that tick, so handle_mob_events'
mob_hits_mob arm could no longer run. It joins StartFollowing and SchoolSize.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Configure button's screen was a placeholder (title, MOTD, open/close). It is now
RealmsConfigureWorldScreen: the four-tab bar (Worlds, Players, Subscription,
Settings, Settings off on an expired Realm), the footer's Play and Back, and
the Subscription tab: start date, time left, Extend, and Delete once expired
with its question.
Built on #7725's realms module and realms/client.rs, per the split on #7677.
RealmsClient.subscriptionFor (GET worlds/{id}/subscription) and deleteRealm
(DELETE worlds/{id}) join the client. The Worlds, Players and Settings tabs
follow in their own PRs (slot options, backups, invites, preferred region);
until then they show vanilla's loading line, and the open/close toggle stays
on the footer (vanilla keeps it on the settings tab).
The start date is a fixed local %Y-%m-%d where vanilla asks for the locale's
medium date (the process has no date locale), and the snapshot Realm's
subscription info line is not special-cased.
Co-Authored-By: ZCode <[email protected]>
🤖 Agent: at 2.5 blocks every triangle() throw enters the stand from outside on tick 2; at 3.0 a slower throw started tick 3 inside the margin box, where AABB.clip never hits.
Our probe pack's Iris 1.11.7 shadow maps held 1 to 3 chunk columns at
the edges of the view's wedge that the shadow pass dropped (15 columns
over 8 noon scenes). Growing each section's box by a margin before the
caster test keeps them: the 31 edge columns of 8 noon scenes allow only
a growth of 1.12 to 1.19 blocks (a fit, not Iris's code); with 1.15, 19
of our 20 scenes hold exactly Iris's columns.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Our probe pack read Iris 1.11.7's matrix uniforms back exactly. Horizon's
differed in their last bits (1.0 for 0.99999994, 6e-17 for 0), and a
fragment on a texel boundary of the shadow map then read the texel on
the other side: with nearest filtering, 13,066 pixels of ow_ground_noon
were lit in one game and shadowed in the other.
- gbufferModelView and its inverse: vanilla's Camera.getViewRotationMatrix
(JOML Matrix4f.rotation of the conjugated rotationYXZ quaternion) and
JOML's invertOrthonormal, in floats.
- gbufferProjection and its inverse, shadowModelView and its inverse,
sunAngle and shadowAngle: fits of our probes' readings (JOML's float
operations in a given order, or JOML's formula worked in doubles).
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The guard skips only a step whose value equals the held state, which leaves a finite value unchanged anyway, while NaN still steps because NaN != NaN, so only the inf - inf case changes. It's stacked on #9701 and lands after it; the shaders tests and clippy -D warnings pass on main merged with both.
🤖 Agent: The two parts were already the game's (terrain_parts) and drawn this way for packs with the split programs, so a pack with only gbuffers_terrain now takes the same path. That's one more pipeline switch per terrain pass, not more geometry, and only with a shaders/ pack. 0.5 matches vanilla's ALPHA_CUTOUT for cutout terrain, so foliage gaps now match both Iris and vanilla. horizon-shaders' 298 tests and the ten terrain_parts tests pass on main merged with this head.
GPU frames ran no occlusion culling. On Vulkan, where cull.rs's test runs
with the depth test forced early, they now run the same two-phase test, each
section's part decided on the GPU: `cull` keeps per id the frames its box was
last seen and last tested at, puts in phase 1 the sections whose last test saw
them (retested every 8 frames) and lists the other boxes. The world pass draws
phase 1, then the listed boxes against its depth, each passing fragment
stamping its id (gpu_occlusion.wgsl); count2, scan2 and scatter2 write phase
2's draws, which a second pass draws before the CPU's own. Frames occlude
under cull.rs's gate (`cull::gate`), over the hidden share that comes back
with the statistics, now 12 words copied after the last pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: custom_index refuses only pairs where both sides are float/int/vecN and differ, so bool on either side keeps the old conversion, and so do types outside the list (ivecN, mat). A disabled uniform simply gets no slot, so it reads 0 and shows up in the program's "defaults or zero" notes. pack_post's tests, including custom_type_tests, and clippy -D warnings pass on main merged with this head.
🤖 Agent: Applying the upper bound first and the lower one last only changes the result when min > max, so every in-range clamp a pack uses reads the same as before (the test's two in-range rows pin that). Fitting the order to your probe readings, not to Iris's code, keeps it clean-room. horizon-shaders' 222 tests and clippy -D warnings pass on main merged with this head.
smooth() stepped as val + (state - val) * k, which turns an infinite value it
already holds into NaN (inf - inf). Iris 1.11.7 keeps it: our probe packs read
smooth(1.0 / 0.0, 5, 5) as +inf and smooth(-1.0 / 0.0, 5, 5) as -inf after 3 s
to 30 s in a scene (smooth(0.0 / 0.0, 5, 5) reads NaN in both games). A value
equal to the one held now stays as it is.
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: a group that leaves an objective, forced chunk or gamerule behind now fails tests/test_command_fixture_state.py instead of parity-main's first Java guard.
🤖 Agent: The cap counts bytes as they inflate, not the sizes the zip claims, and it lives in fill(), which import_into and import_mrpack_into both go through, so neither path can skip it. 8 GiB is well above any real .mrpack or shared instance. The launcher's fmt, clippy -D warnings --locked and all its tests (548 + 102, both bomb tests included) pass on main merged with this head.
- Pre-bake full-window blurred background texture on resize to eliminate per-frame CPU bilinear resampling and allocation during list scrolling
- Adjust version card radius from PANEL to ROW (12px) with refined vertical padding and margins to resolve overlapping border stacking
- Add radius::CONTROL and overflow clipping to mods sort dropdown containers
🤖 Agent: Only relays started with RELAY_PROXY_PROTOCOL=1 change, and neither the Dockerfile nor horizon-relay.service sets it, so a default deploy behaves exactly as before. One that has it on now refuses to start until RELAY_PROXY_FROM is set, rather than dropping every connection. fmt, clippy -D warnings and the relay's tests, including proxy_from.rs, pass on main merged with this head.
🤖 Agent: Deploying this changes nothing for Railway's address counting: the Dockerfile names x-real-ip, the header the server already read first. The website fetches /v1/aggregate?days=30 and /v1/benchmarks/summary from the visitor's browser (benchmarks-page.js), so the new 600/h read bucket counts each visitor, not one site address. pr_ready skips this workspace, so I ran it on its own: fmt, clippy -D warnings --locked and every test pass on main merged with this head.
🤖 Agent: This follows Allay.hurtServer, which refuses only a player whose getUUID equals the LIKED_PLAYER memory. owner_damage_uuid passes a UUID only when the acting player is the server owner, so single player on its server thread gets the exact comparison, while LAN guests and dedicated-server players keep today's refuse-any-player filter: narrower than vanilla, but not a regression. The Game-level test runs against prepared resources and passes, and parity checks the corpus.
It left its stone floor at y 199, a crafter, a chest and the player in
survival: the next group's `/setblock 10 199 10 stone` guard found stone
already there and stopped the run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With RELAY_PROXY_PROTOCOL=1 a PROXY line was accepted from any TCP peer,
so a client reaching the port directly could choose the address its
per-address limits count.
- RELAY_PROXY_FROM: addresses or CIDR ranges (IPv4, IPv6, IPv4-mapped
peers handled). A connection from anywhere else is closed unanswered.
- The relay refuses to start (a clear error, exit 1) when the protocol
is on and the list is empty, or when an entry doesn't parse, instead of
silently dropping every connection. Config::from_env returns a Result;
Config::check says why.
- docs/play-with-friends.md lists the setting.
Split from #9129.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
With HORIZON_TELEMETRY_TRUST_PROXY=1 and no
HORIZON_TELEMETRY_CLIENT_IP_HEADER, every request would count as the
proxy's address: one shared bucket for /v1/batch, deletions, the live
count and reads, so intake would stop and players' deletion requests
would be refused, with only a line on stderr to say so (martipops's
review of #9129).
- The Dockerfile sets HORIZON_TELEMETRY_CLIENT_IP_HEADER=x-real-ip, the
header the server read first before the setting existed, so the
address the limits count on Railway stays the same.
- TRUST_PROXY=1 without the header now refuses to start (exit 1), as the
relay does without RELAY_PROXY_FROM: a deployment missing it fails its
/healthz check instead of going live with one bucket.
- README: both settings, and the refusal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
FishingHook(Player, ...) snaps the hook's position (its feet) to the owner's
eye; Horizon put it 0.125 lower. The harness cast at a stand 3 blocks away,
which vanilla's randomised throw reaches only on ~80% of casts: Horizon's own
run missed with the same odds and was never retried. At 2.5 blocks every
throw hooks the stand and the boat on its second tick, so the harness leaves
the known-flaky list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
crafter map extension forced chunk (0,0) and left it, which moved the next
`/forceload add` guard that covers it. The groups the full run leaves out
(reinforcements, recipe overlays) are now named once in the corpus, so the
harness and the fixture-state test skip the same ones.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A trait mod's register_goals hook is asked for every new mob, after its
species added its goals and before its saved data is read. It may clear
the goal and target selectors and add Goal types of its own, which steer
the mob with its navigation (nav_move_to, create_path_to) and controls.
A mod's living type from #9667 thus gets its own AI. The hook runs before
the float check, so a mod's Float goal floats the navigation as
FloatGoal's constructor does.
The hook is a free function in the client library, so it goes through
NativeClientHooks as living_breathe does. mod_api re-exports Mob,
MobWorld, Goal, GoalSelector and Cx; the generator's dispatch tests get
a Mob stub and the catalog test knows mobs/ai.rs.
Part of #8622.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: This matches Minecraft.runTick and tick: the texture clock advances only inside isLevelRunningNormally, while GameRenderer.tick (the lightmap flicker) runs on every unpaused tick, and Horizon only calls tick() when !self.pause, so a paused game still freezes both. The flicker draws from FogState's own random, so moving the call doesn't shift any other draw.
mob partial block suffocation set mob_griefing false and random_tick_speed 0
and left them, so every later group ran without mob griefing: no piglin in
piglin pickups picked anything up on either side, and the gold's fate became
a race. A unit test now checks every group leaves the world rules it found.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pack without gbuffers_terrain_solid/_cutout drew all terrain in one pass
with an alpha test of 0.1, and alphaTestRef read 0.1. Iris 1.11.7, with probe
packs of ours writing one alpha per screen strip, drew solid terrain with no
alpha test (alpha 0.0 kept) and alphaTestRef 0, and cutout terrain keeping
alpha above 0.5 only (0.5 and 0.49 discarded, 0.51 kept) with alphaTestRef
0.5, through gbuffers_terrain alone and through the split programs alike.
Vanilla 26.3 cuts cutout terrain at 0.5 too (RenderPipelines.CUTOUT_TERRAIN).
Every pack now draws solid and cutout terrain apart, as packs with the split
programs already did: without them, both parts draw with gbuffers_terrain's
chain. The cutout part's default test is alpha > 0.5.
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.EntityCallbacks.onDestroyed calls Scoreboard.entityRemoved for
every entity kind. Only dying mobs did here, so an item, an area effect
cloud or a projectile that had a score kept it after it was gone, and
/scoreboard players list kept listing its UUID (command_exec's scores and
social groups after carpet farms and loaded potion clouds). Once a tick,
the UUIDs that hold a score or a team are checked against the live
entities; unloaded and travelling ones leave persist.uuids first, so they
keep theirs, as in vanilla. Nothing runs while no entity holds one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- entities, piglin pickups: their falling-block and item selectors spanned
the whole world. The Rust side block-ticks a ring of terrain (--radius 4)
that the oracle never loads, and at chunk (-5, -2) generated kelp breaks
and gravel falls into a flooded cave there, as vanilla's own does once
that chunk ticks (checked with both forced loaded). Those drops are no
fixture's: the checks now look near the fixture.
- execute: `execute in the_end if block 0 64 0` expected an unloaded chunk.
The oracle's settle keeps its tickets for the rest of the run, so since
gateway passengers settles in the End, 0 64 0 stays loaded on the Java side
only. The check keeps its point (another dimension, an unloaded position)
at 1000 64 1000.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: As in Entity.lavaHurt, the orb draws its pitch from its own random only after a hurt that lands (the early health <= 0 return covers isInvulnerableToBase's isRemoved), so the random order matches vanilla. The sound goes through the same audio.play path as the boats' lava_hurt_sound. Orbs don't track Silent yet, so a /summon'd Silent:1b orb still sizzles; that's a small gap left for later.
🤖 Agent: This follows KeyboardHandler.handleDebugKeys: an open DebugOptionsScreen closes, otherwise canInterruptScreen (shouldCloseOnEsc by default) decides, and the interrupted screen runs its own close first, so a container gives its items back before the debug screen opens. The credits keep their screen, a deliberate gap from vanilla because their close can fail; the doc comment says so.
🤖 Agent: The flag is set only when the last frame before the loss was GPU-culled (old.gpu_frame()), so a device lost in an ordinary CPU-culled frame still gets GPU culling offered after recovery, and the saved setting is never written. It's a Relaxed AtomicBool read once per renderer build, so it costs nothing per frame.
🤖 Agent: In 26.3 recipes are a registry, and RecipeManager.finalizeRecipeLoading builds the stonecutter list and unpackRecipeInfo numbers the displays by walking recipes.values(), so sorting by registration_key in every table (and in the LAN copy the guest's stonecutter picks from) gives the order vanilla shows. The three reads moved to the world's table were the only places still showing the base table's result while handing over the pack's.
🤖 Agent: gl_Fog.density now reads the fogDensity value Horizon already sends, which is 0 everywhere, so a pack's exp-fog maths sees what Iris gives it in the scenes your probe measured. Nothing else in the struct changes, and packs without shaders/ never read it.
A custom uniform of shaders.properties was converted to whatever type the
program's GLSL declared it with. Iris 1.11.7 disables it instead ("Wrong
uniform type for X: Iris is providing FLOAT but the program expects INT.
Disabling that uniform.") and our probe packs read 0: a float declared int,
an int declared float, a vec3 declared vec2 and a vec2 declared vec3. A float,
int, vecN custom uniform now binds only to a GLSL uniform of its own type
among those; pairs involving bool keep the conversion (a float of 2.5
declared bool read true in both games).
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bumpers switch every tabbed screen, and turn the recipe book's pages; its triggers change category.
The right stick only scrolls: the focus stays on its row, no value changes, and nothing scrolls under the
on-screen keyboard. The D-pad walks a list's hidden rows, stops at a list's edge without changing a value, and
a held direction stops at the edge of the slots it's on. The crosshair stays on its target as the screen changes,
A held with the D-pad spreads a stack, and a list's scrollbar says how far its rows moved.
The arrow keys share the focus step, so they walk hidden rows too, and the superflat and single-biome rows
become focus targets for them. Focus on a preset selects it and leaves the share box to a press, as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every Nightly was signed for the constant "horizon release nightly", and
the version a launcher installed it as came from the unsigned GitHub
title, so anyone able to edit the release could serve an older signed
Nightly as tonight's.
- nightly-build.yml signs SHA256SUMS twice with the same key(s):
SHA256SUMS.sig for the channel, as before, for installed launchers;
SHA256SUMS.v2.sig for nightly-<date>-<commit>, the version the title
names (sign_release.sh ALSO_FOR). Both are verified before publishing.
- horizon-release-sign: nightly_title_version() parses the title
strictly (no fallback to published_at); nightly_digest() takes that
version and checks SHA256SUMS.v2.sig for exactly it; is_nightly_build();
SIGNATURE_V2. `verify --signature <file>` checks the v2 file.
- Every installer that checks a Nightly moves in the same step: the
launcher, horizon-server update and packaging/server/install.sh refuse
a Nightly whose title doesn't parse and install it only with a v2
signature for that version. install.ps1 checks no signature (only
GitHub's SHA-256), so it has nothing to move.
- An older Nightly still installs on request as a rollback with a
warning; the launcher's update never moves the selection back to it,
and horizon-server prints a rollback notice for --version.
- Nightlies from before this change have no v2 signature: the refusal
now says so and names `horizon-server rollback`, and docs/security.md
"Nightly signatures" describes the changeover (martipops's review of
#9129). The launcher's new messages go through trf(), in every
language table in English until translated.
- tests/install_sh_signature.sh: a v2 Nightly installs as its version; a
Nightly with only the channel signature, an older Nightly retitled as
a newer one, and an unreadable title are refused (all three installed
before this change).
Split from #9129, as martipops's review suggested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Keep the reinforcement request collection and post-hit drain around the
verified-owner damage adapter, and preserve all upstream corpus groups.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Pass verified owner identity through the existing command and normal attack
adapters while preserving the legacy filter for unresolved damage callers.
Cover live command feedback/state and both adapter and compatibility APIs.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A custom uniform's clamp(x, min, max) applied min first and max last, so with
min > max it gave max. Iris 1.11.7 gives min: our probe packs read
clamp(5.0, 3.0, 1.0), clamp(0.0, 3.0, 1.0) and clamp(2.0, 3.0, 1.0) as 3, and
clamp(vec2(5.0, 0.5), vec2(3.0, 0.0), vec2(1.0, 1.0)) as (3, 0.5), in three
scenes. Apply max first and min last.
Part of #9307
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: This matches Allay.aiStep: the heal sits after super.aiStep with no NoAI gate (only isClientSide and tickCount % 10). The unit test pins both paths: NoAI heals and keeps its hold timers, and normal AI still heals only once.
Since #9482, /execute if loaded reads the ticket levels, which only a tick
recomputed. The harness's settle loads terrain without ticking, so a group
that moved the player and forced chunks (large fireball fire) still saw the
previous group's tickets: chunk column x=5 was resident and lit but not
entity-ticking. The oracle's settle takes its tickets at once (getChunk runs
the DistanceManager updates), so the probe's settle now refreshes them too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemCommands.getContainer rejects any block entity that is not a Container
("Source position … is not a container"). The source check only asked
whether a block entity existed, so a sign, lectern, campfire or beacon got
"The source does not have slot …" instead. Use the same container test as
the target side.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Recording is opt-in (/studio or the control socket), filters before writing and fails closed on unknown packets. With no recording running, the network thread now pays one uncontended lock per packet instead of a copy of every payload.
import.rs inflated every entry of a dropped .mrpack or game-folder zip
with no byte limit, so a few kilobytes of zip bomb could fill the disk
before the half-made instance was removed.
- fill() counts the bytes actually inflated across all entries (a zip's
sizes are its own claim) and stops past MAX_IMPORT (8 GiB); the error
removes the instance as before.
- The cap sits in fill(), which import_any() calls for both
import_into() and main's newer import_mrpack_into() (a Java instance
takes every path a pack carries), as martipops's review of #9129
asked when rebasing.
- The message goes through trf(), in every language table in English
until translated.
- Tests with a 16 KiB limit: a compressed bomb is refused and leaves
nothing, for a shared instance and for a .mrpack; the limit is for all
files together.
Split from #9129.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Once an entry has owner_ids, only an ID in them makes an owner, so adding
a co-owner to `owners` alone, the obvious edit, would silently lock them
out, and the refusal would list their own login among the owners
(martipops's review of #9129).
- Entry::check_fields refuses an entry whose owner_ids is non-empty and
not as long as owners, with how to find the missing ID, so the registry
CI catches it when the change is made.
- docs/api/owner-update-v2.md: the registry repository has no
owner-update workflow yet, so nothing merges on its own today; and the
backfill script runs only once a maintainer has read it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Owners were GitHub logins, and a login can be claimed by someone else once
its owner renames or deletes the account. For a project without a key, a
reclaimed login could publish an owner update that merges on its own.
- Entry gains `owner_ids` (#[serde(default)], omitted when empty). The
form records the lister's ID for new projects (file and repository
listings). The registry review notes any change to it.
- ownership(): when an entry has owner_ids, only an ID in them is an
owner, so a matching login on another account is refused. Without
owner_ids (older entries) the login matches, but the update always waits
for a reviewer and the pull request says so.
- The automatic owner update requires an ID match.
- update_trailer is v2: `owner-update-v2` message with the submitter's
numeric ID, and `format: v2` / `submitter_id:` lines in the trailer.
- tools/registry_backfill_owner_ids.py: one-off, dry run by default,
resolves owners' logins to IDs, cross-checks them against the IDs the
form recorded in the entry's commits, and leaves alone (and prints)
any entry it can't resolve completely.
- docs/api/owner-update-v2.md: what the registry repository's
owner-update workflow must check, and the deployment order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit 526bfcbbe1)
🤖 Agent: Entity occlusion culling stays Off by default. prepare_occlusion_culling only builds a culler when the mode isn't Off, so the default frame skips both the voxel paths and the new section-graph rejection. A budget that runs out still fails open (visible). One push from me: main's new ItemFrame.custom_name field added to the PR's bounds test, which was what broke verify. Desktop: clippy -D warnings is clean (client, client-lib, client-render) and 2835 client tests pass. Benchmarks: the author's stationary and moving runs above.
With TRUST_PROXY=1 the rate limits were keyed on X-Real-IP, else the last
X-Forwarded-For entry, whatever the platform guarantees. Railway's edge
has let clients set X-Real-IP in the past, and puts the CDN's address
there when Fastly is in the path.
- HORIZON_TELEMETRY_CLIENT_IP_HEADER names the one header the proxy
overwrites (x-real-ip on Railway); its last comma-separated entry is
the client. No other header is read.
- TRUST_PROXY=1 without it counts the TCP peer, and the server prints a
warning at start saying every client then shares one bucket; a header
set without TRUST_PROXY is reported as ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit bd420535e0)
The server runs two async workers and did every SQLite call on them, and
GET /v1/aggregate had no rate limit and cached per `days` (366 keys),
while DELETE /v1/installs/<any id> emptied every cache: a cheap way to
stall every handler.
- blocking(): batch storage (zstd inflate included), install deletion,
the totals, the benchmarks summary, the live history, the upkeep and
the live count's minute sample run on tokio's blocking pool.
- /v1/aggregate and /v1/benchmarks/summary take a per-address read
bucket (READS_PER_HOUR, 600); uncached totals builds have a global
budget (AGGREGATE_BUILDS_PER_HOUR, 600) and answer 503 past it.
- AGGREGATE_DAYS (e.g. 7,30,90,366) restricts the windows answered; unset
keeps every value from 1 to 366. It is left unset by default because
the website's own requests (private horizon-site) couldn't be checked.
- A deletion invalidates the cached totals only when it deleted rows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit a29c16a169)
Main added ItemFrame.custom_name (a named frame drops with its name); the test initializer predates it and stopped verify from compiling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The privacy filter ran on every packet before the recorder was looked at, so
with no recording it still copied each payload (chunk data too) and
re-encoded every AddEntity with a fresh random UUID. It now runs only while a
recording is active, and keeps the packets it allows without copying them.
The recordings browser's title, empty state, buttons and server labels go
through the language table (gui.done, menu.singleplayer and
menu.multiplayer are vanilla's). The row's "Edit" hint is removed: this
PR has no editor to open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The NoAI branch now calls only sting_touching_mobs, after super.aiStep's work, as Pufferfish.aiStep does without a NoAI gate, and the unit test checks that ordinary AI still stings exactly once. The 119 vs 118 poison duration your diagnostic keeps is #9672.
🤖 Agent: The name tags now use posed_rotation, which camera_orientation already trusts and which is already turned round for the front view. The only change is that a spectated entity's rotation is kept even when it equals the vanilla view, so the mouse no longer turns the tags.
🤖 Agent: class_id() is the type's own id for every vanilla type, so mob_to_nbt and mob_meta change nothing without content mods. The loot-table fix (ruby:entities/ghoul) matches EntityType.getDefaultLootTable. The 25 slow content-root tests pass with prepared resources.
🤖 Agent: The caster test can only add planes to the light's box, and a plane that doesn't fit the array is dropped, which keeps more, so the shadow pass never loses terrain the old box drew for a view it can shade. It applies only to Iris-format packs with shadow.culling unset. Fitting Iris's maps from screenshots, not its code, keeps this clean-room.
🤖 Agent: The crafter now matches map-extension recipes against the level's live saved maps (MapExtendingRecipe.matches reads the map's scale). An unknown or max-scale map no longer extends, and the lookup goes through the world's recipe table. The author's refresh has the same Rust as my tested merge, where clippy -D warnings is clean and the crafter, menu and command_harness tests pass. Its corpus builds 117 groups with no duplicates.
groups() in command_exec_corpus.py: main added the zombie reinforcement
groups where this branch adds the allay one; keep all of them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game rebuilds the renderer after a device loss, and the new one offered
GPU culling again: a fault in its frames could lose the device after every
recovery, until a second loss within a minute ended the session. A loss while
the last frame was GPU-culled now makes every renderer after it report
GpuCullingOffer::DeviceLost, until Horizon restarts; the switch greys out and
says why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/nightly, the triage commands and /claim decided who may act by the
comment author's login. GitHub hands a renamed or deleted account's login
to whoever claims it next, so a reclaimed reviewer login could pick
unreviewed pull requests into the signed Nightly.
- trusted-reviewers.txt lines become `<id> <login>`; one shared parser
(nightly_set.read_reviewers) for all three commands. Any other line,
bare logins included, grants nothing and is reported on stderr.
`paused` keeps its meaning.
- All three workflows pass AUTHOR_ID (github.event.comment.user.id);
without it the scripts refuse. Ludovic is OWNER_ID, and his threads are
recognised by user.id in triage.
- tools/ci/resolve_reviewer_ids.py fills in the IDs (and OWNER_ID);
--check reports renames and deleted accounts.
No review requirement is added: a Nightly still takes any open pull
request to main, as decided on 2026-10-30 (NoReviewNeededTests).
Split from #9129, as martipops's review suggested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Vanilla 26.3 (KeyboardHandler.handleDebugKeys, the keyDebugDebugOptions branch)
closes an open DebugOptionsScreen, else when Minecraft.canInterruptScreen()
passes closes the current screen (Screen.onClose; canInterruptWithAnotherScreen
defaults to shouldCloseOnEsc) and opens the debug options over it. Horizon only
opened over the game and the bare pause menu (Fixes#9664).
- debug_options_interrupts: the canInterruptScreen classification, one arm per
Screen; the death screen, the level teardown, the text-box screens, the
credits (fallible close) and a playing overlay keep theirs.
- toggle_debug_options closes the interrupted screen the way its Escape does
(a container hands its state back, creative clears pad focus and the carried
stack, a mod's screen hears the close) before opening.
- The F3+F4 switcher passes F3+F6 on to the debug options screen.
- A unit test pins the classification.
Co-Authored-By: ZCode <[email protected]>
- The crafting grid's result, breeding's dye mixing and the campfire's
cooked item read the world's table, as menu_click and the campfire's
placement already did.
- Recipes::from_sources sorts every table (crafting, cooking, mods' own
types) in registration order, the key #9406 sorted crafting by, now
crafting::registration_key; StationRecipes sorts stonecutting and
smithing by it.
- RecipeIndex numbers recipe-book displays in registration order.
- The LAN copy sends the stonecutter list and numbers groups in that
order, so a guest's stonecutter button indexes the host's list.
Fixes#9671.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
command() still passed read_reviewers(reviewers).logins to apply() after
c527d60b7 dropped that parameter, so every /nightly comment that got past
authorize(), status included, ended in TypeError and was never answered.
- Drop the leftover argument.
- Test: command() run as nightly-command.yml runs it, with GitHub and the
state branch stubbed, answers /nightly status. It fails on main with the
TypeError.
Split from #9129, as martipops's review suggested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
The compatibility gl_Fog gave packs a density of 1. Iris 1.11.7 gives 0
there, in gbuffers_terrain and in deferred, in a cave and in the Nether,
where fogDensity also reads 0 in both games (read back with a probe pack
of our own).
Fixes#9674
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Four standalone fixes from #9129, with authorship kept. The control token now comes from getrandom. Token files are written owner-only (write_private). Ban-list dates are checked instead of taken as they are. A relay bridge frees its slot when it ends. None of them is on a hot path. The server-files, relay, server and control tests pass on playhorizon.
🤖 Agent: A zombie hit by /damage now sends its reinforcements before the command returns, as vanilla's hurtServer does inside the command, so a following command sees the reinforcements. The isolated reinforcement groups run only with --only, so the generic corpus is unchanged. The author's refresh onto main keeps both fixture isolations. Clippy -D warnings is clean and the reinforcement tests pass.
Keep H295 active world recipe selection with H298 saved-map matching and system post-process. Preserve all shared corpus groups, datapack fixtures, map copy controls and player observations. Formatting, all-target Clippy, Python and Java compile checks pass; original runtime qualifications remain unchanged.
Co-Authored-By: Codex <[email protected]>
Java's fixture guards read counts: `/scoreboard objectives add` returns how
many objectives exist after the add, and `/forceload add` how many chunks it
newly forces. Four groups left state behind for the next ones:
- mob baby attribute reads kept h227 and allay invalid Brain retention kept
h226, so rewrite_hit, hitstate and audit got 3 instead of 1;
- both advancement loot groups kept chunk (0, 0) forced, so item
stack-size override merges forced 7 chunks instead of 8.
parity-main stopped on "Java fixture control failed" for each of them once
#9514 fixed the /fill floor guard (#9587). Each group now removes what it
added. A unit test replays every group's objectives and forced chunks and
checks each count guard against what earlier groups leave.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep reinforcement and advancement-loot corpus groups, all diagnostic options, and light/player observations. The reinforcement fixture function is byte-identical. Formatting, all-target Clippy, Python compilation/options/registration and Java oracle compilation pass. Original runtime proof remains qualified to its captured base.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: The recipe order now follows ResourceManagerRegistryLoadTask's sort by file id (path, then namespace). It runs once per load, and find_with is unchanged, so lookups cost nothing more. The prepared 26.3 recipe folder has no '-' or '.' names, so vanilla's own recipes keep their order. Main merged in, both fixture sets kept.
The final main checkpoint changes server API tests only; the checked pufferfish source and fixture are unchanged.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: The one memo keeps vanilla's chunk requests (a missing chunk is asked again unless the world promises it stays missing), and may_reach_out, now asked once per non-empty section, is a pure query. An A/B on the desktop (dedicated server, 1500 pigs on the ground, 3 interleaved rounds, main f5565f1 against main plus this PR) gave a 5.30 ms median per tick on main and 5.33 ms with this PR: below the run-to-run noise (±0.3 ms), so your instruction counts are the finer measure. The far-move cost (+16%) is a fair trade for every mob's ground move.
🤖 Agent: With HORIZON_STILL off, every new branch reduces to the old expression and the variable is read once through a OnceLock, so there's no per-frame cost. The main merge keeps the border ticking only while the level runs, as main has it. Checks: clippy -D warnings, shader tests, clock tests.
🤖 Agent: Off by default and in no preset; with the setting off there is no readback buffer and no poll. The device-loss follow-up from the review (turn GPU culling off for the session after a loss in a GPU frame) is worth its own PR. Union build passed.
Upstream changes add frog travel, player equipment visibility and UI/server work; the NoAI pufferfish caller and sting handler are unchanged. Preserve all fixture options and qualify the captured runtime pairs to d158e4473.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: main merged in. The new auth/command goldens test now sits beside main's observability and config golden tests in api.rs, and both sides are kept whole. It drives the real HTTP listener, so missing/wrong token, wrong Host and malformed commands are checked over the wire. horizon-server: 150 tests pass, clippy -D warnings is clean.
Preserve the unchanged canonical 218 and strict 220 scripts. Add an independently validated same-position pig/cod control rather than infer contact from the original cod placement. Keep landed H311 player contact and all current fixture options.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Carries #8918 unchanged, with main merged in. Only the mod_api export list conflicted, and both sides are kept. The depth-tested world text and atlas quads go through the existing native world-draw hook, so a game without mods costs nothing extra. It passed fmt, clippy and tests in union3.
🤖 Agent: Matches TridentItem.use: FAIL (about to break, or Riptide while dry) doesn't start using and lets the off hand try, as Minecraft.startUseItem does. game.rs's single-player use chain now also falls through on FAIL. Union build passed.
🤖 Agent: The boost now starts from the synced DATA_BOOST_TIME (index 18, after AgeableMob's two), as ItemBasedSteering.onSynced does, instead of the rider's client guessing it. Union build passed.
🤖 Agent: Matches ArmorStand.pushEntities: only RIDABLE_MINECARTS within distanceToSqr <= 0.2 are pushed, and the stand riding the cart is skipped as Entity.push skips a shared vehicle. Union build passed.
🤖 Agent: Matches ItemFrame.getFrameItemStackWithData: the dropped frame (glow frames too) takes CUSTOM_NAME from getCustomName, and the name now round-trips through the frame's NBT. Union build passed.
🤖 Agent: Matches Frog.travelInWater: moveRelative(getSpeed()), move, then scale 0.9, with no gravity or water drag. Minor: the test's `let _ = ran;` passes silently without vanilla data. Union build passed.
🤖 Agent: Matches LivingEntity.getVisibilityPercent: each worn stack's mob_visibility applies only when its equippable slot matches and the looker's type is listed, then Mth.clamp(0, 10). The chest/legs/feet fields cost three Arc clones per PlayerView. Union build passed.
🤖 Agent: The non-player victims carry NoGravity so the blast's push is the only motion compared, and Pos/Motion are compared at 9 decimals as server_knockback does. Union build passed.
🤖 Agent: HoneyBlock.fallOn plays the slide sound (the guest's own client plays it for the guest) and broadcasts event 54 to everyone, the lander included. Union build passed.
🤖 Agent: native_health_hud_state hands out &mut HealthHud, so a mod calling layout twice a frame double-advances the heart blink; the docs say once a frame, which is the right contract. Stamp test and union build pass.
🤖 Agent: Matches Mob.checkDespawn: peaceful discards a disallowed type on its own despawn check, even when persistent; Horizon's check_despawn already does that first, so the three instant purges were the only difference. Union build passed.
🤖 Agent: main merged in after #9062. The config golden now lists the new [transfer] section with its defaults (enabled false, expect 10 s, max 60 s, 64 players, freeze 30 s). horizon-server tests: 149 pass.
🤖 Agent: An explicit rain.depth=false now skips the weather depth write; omitted and malformed values keep main's behaviour. Spectrum and Luculent are MIT and disclosed. Union build passed.
🤖 Agent: Each part clips the other's quads in the vertex stage, so packs with gbuffers_terrain_solid/_cutout pay double terrain vertex work (noted in the earlier review); packs without them take the old single draw. Union build with 44 PRs passed, including horizon-shaders and render tests.
🤖 Agent: Mob sounds now return the mob-index state before level_entity runs, so the dragon's metadata Mob can't shadow its Dragon, and silence and removal still come from the Mob. The once-per-tick cache is keyed by id inside one call, and a removed sound never revives when its id is reused.
🤖 Agent: queued; the drop_tool key is the machine's own position, so a neighbour broken in the same call no longer rolls the machine's tool or its silk touch.
#9062 pinned the config reply before this PR added [transfer]; the GET reply now lists it with its defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: entity_event_burst is now the only list, and no other branch of the EntityEvent handler draws for 38 or 42, so nothing draws twice. The cost is the existing mob search, now for every entity event instead of eight ids; entity events are rare enough that this doesn't show.
🤖 Agent: The old verdict is removed before carry mutates content, and an interrupted harness can no longer leave a stale green one. Union build passed.
🤖 Agent: Measured on the desktop: exact::sin + cos takes 110 ns a pair against libm's 10 ns (11x). The 27 jmath sin/cos call sites are per mob or per feature, never per vertex, so that is microseconds a tick, on Windows only. Union build passed.
🤖 Agent: A failed cape no longer drops the skin, a failed download retries after 60 s instead of never, and only bodies that decode reach the disk cache. Union build passed.
🤖 Agent: Matches ServerPlayerGameMode.destroyBlock: a GameMasterBlock stays without canUseGameMasterBlocks. Vanilla also sends a block update there; the guest's break acknowledgement covers that here. Union build passed.
🤖 Agent: The control socket's chat and command go through handle_chat_input and the client mod command path, the same as typed chat, so client mods see them. Union build passed.
🤖 Agent: Entity.load mutates the existing living entity and never reads its damage cooldown or last hurt amount, so a /data merge no longer resets them. The corpus case covers it against Java. Union build passed.
🤖 Agent: if() now evaluates conditions up to the first true one plus that branch only, so a smooth() in an untaken branch keeps its state. Iris was only run with your own probe packs, which our rules allow. Union build passed.
🤖 Agent: Matches EnderDragon.knockback: LivingEntity.knockback only when the phase isn't sitting, power 0.4F from dealDefaultKnockback, and the vertical stays (noPhysics never sets onGround). Arrow hits still don't push, as before. Union build passed.
A content mod's entity type could only copy minecraft:snowball, so a mod
had no way to add a mob. It may now copy minecraft:zombie or
minecraft:villager, as NeoForge's EntityType.Builder.of(Zombie::new, ...)
makes a type of a vanilla class, with its own default attribute values
(EntityAttributeCreationEvent) in an `attributes` map, for attributes the
template has. A thrown type still names its item; a living one has none.
The mod's row in entity-types.json is its template's with `template`
added and its attribute bases in place of the template's, every other
value kept as the template's text. MobTypes::load makes a row naming a
template into that species' MobType under the mod's id, so registry and
network ids, /summon and remote spawns, which already went through the
table and the registry, pick it up. What a class saves and syncs of its
own (a zombie's IsBaby, CanBreakDoors and conversion times, its entity
data, the villager's) follows MobType::class_id, the template's id for a
mod type; the saved and sent id stays the mod's.
A mob's default loot table, and /loot ... kill, now keep the type's
namespace (EntityType.getDefaultLootTable): ruby:ghoul rolled
minecraft:entities/ruby:ghoul, and rolls ruby:entities/ghoul. Nothing
changes without content mods: no row has a template, and class_id is the
type's own id.
First slice of #8622; goals, navigation and structure placement stay
open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep main's world border rule from #9396 (it ticks only while the level
runs, which already covers HORIZON_STILL's hold) instead of the
branch's hold gate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The synced enchantment table is built once per registry sync, not per tooltip; a literal-text description of a data-pack entry now shows instead of the raw id. Union build passed.
🤖 Agent: Matches WitherBoss.hurtServer: a powered wither refuses a direct WindCharge, and a breeze's BreezeWindCharge (a sibling class) still hurts it; the test pins exactly half health. The only parity failure was #9511, fixed by #9514 (merged), so parity runs again.
🤖 Agent: Merged in a 45-PR union build (fmt, clippy, tests pass). Ticking::Native only changes who advances a sound instance; vanilla's SoundEngine.tickInGameSound order is unchanged for every non-native sound.
🤖 Agent: I merged main (wardrobe tests on both sides kept, 95124d4). The session is reused until the service answers 401, then signs in once more; Mojang's join is made once per service session instead of on every refresh. Union build passed.
🤖 Agent: I merged main and carried main's new OreVein positive_y through fold_biomes (3bbcda7); it depends on the rule, not the chunk. The measured win is in the carvers stage (−16 to −18 % ms/chunk), with snapshots identical to main. Union build passed.
🤖 Agent: Matches FireworkRocketItem.use: dropAllLeashConnections(null) while fall-flying, then LEAD_BREAK when something dropped. I fixed the ignored resource test, which no longer compiled after main made the audio capture a struct (86d09bf). Union build passed.
🤖 Agent: The tamable arm was the wrong way round: Wolf/Cat/Parrot.mobInteract answer SUCCESS for sitting down, and for food that heals a hurt tame wolf or cat or tames a cat, so those swing. AbstractHorse.fedFood and Panda answer SUCCESS_SERVER (no client swing). Fixed in a pure mob_swings with a test (0d8d49d), main merged. Union build passed.
🤖 Agent: I merged main. block_entity_nbt now reads a live moving piston first, as #9617 made /data do, so block predicates see the same NBT (de85911). Union build passed.
🤖 Agent: I merged main and changed one thing: a registry whose pack tags fail to load now keeps the game's tags. Before, it stored an empty table, so even vanilla's #tags matched nothing in advancement predicates (167f156). Union build passed.
🤖 Agent: Matches EnderDragon.kill and tickDeath: remove, then gameEvent(ENTITY_DIE). I merged main, which now asks is_fight_dragon before the dragon is taken (0b6621e). Union build passed.
🤖 Agent: I merged main (corpus groups from both sides kept, every def checked present). Mob command names now take the team's prefix, suffix and colour, as Entity.getDisplayName via PlayerTeam.formatNameForTeam. Union build passed.
🤖 Agent: Advancement loot rewards now read the world's data-pack tables, as ServerAdvancementManager's reward resolution does through the reloadable registries. Union build passed.
🤖 Agent: Item bob and end crystal phases come from the entity's own random, as the ItemEntity and EndCrystal constructors draw them, instead of a shared stream. Union build passed.
Our probes' Iris 1.11.7 shadow maps (shadow.culling unset) fit keeping
only the sections whose shadow can fall into the camera's view: under a
noon sun a wedge of chunks in front of the camera, with a low sun ahead
nothing behind it. Horizon's held every section in the light's box.
For Iris-format packs, the shadow pass now also culls sections with the
camera's frustum stretched without end toward the light (the faces turned
toward the light, plus a plane along the light through each silhouette
edge), and tests whole 16-block sections there, a guess that matched our
probe with the camera looking straight up. shadow.culling=false and the
voxel distance of shadow.culling=reversed keep everything, as before.
Horizon's own packs are unchanged.
Part of #8464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Exclude packets that carry player identity or private text, randomize entity UUIDs in recorded AddEntity packets, and fail closed on unrecognized packets. Keep recording metadata free of server addresses and player names. Correct the recordings browser tooltip to match its current behavior.
Co-Authored-By: Codex <[email protected]>
Reuse the existing contact helper from the NoAI Mob.tick branch without changing ordinary-AI order or accepted-hit handling. Add actual Mob.tick and guarded Game/Java contact regressions; retain the independent poison-duration diagnostic.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Ten inactive ticks in a row is longer than any vibration in flight takes to arrive inside the 8-block range, so the walk starts from a quiet sensor. The movement and activation checks on both clients are unchanged, and the three trace tests cover the vibration still travelling, a second vibration and a setup that never settles.
#9598 made far moves cheap by passing empty sections and missing
chunks a row segment at a time, but every cell of a mob's box paid for
it: a second cache search on top of the reach cache, and more state in
the cell loop. On the ground a mob's move cost about 7.5% more
instructions than before #9598.
The scan now keeps one memo, the last cell's chunk with its section's
facts (empty, may reach out), so a cell in the same chunk and section
costs one comparison. Both facts live in one eight-section cache,
asked once per section. Where the world's chunks stay as they are for
the scan (missing_chunks_stay_missing), it also keeps the chunk before,
so a row across a chunk border goes back and forth without asking
again. Worlds without that promise ask as vanilla's BlockCollisions
does, including again after a missing chunk.
Fixes#9653
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The headline is counted from the rows' own status, class and tier, so it can't disagree with the numbers under it, and the launcher's tr() lines are tested against Summary::lines. The per-row plain sentences are still English report data, like `reason`; translating them is #9644.
🤖 Agent: The CurseForge rows now reuse #9583's curseforge_instance_target (minecraftinstance.json first, manifest.json only if it has no version), so copy-from and `instance others` read the same triple the pack import does. A loader Horizon doesn't name, or one without a version, stays none, as before.
Dolphins ran WaterAnimal's air rule, which set their air to 300 every tick
in water: air never fell under 140, so they never surfaced for air nor
drowned, and #9271 now sent that 300 to every viewer. Dolphin's
handleAirSupply is empty and it isn't in #can_breathe_under_water, so it
now takes LivingEntity.baseTick's air from its 4800 (and stops taking
drowning damage on land on top of drying out). The breathing list follows
the tag: copper golems in, ghasts, blazes and snow golems out (no override
keeps their air; the iron golem's decreaseAirSupply does). A saved mob
without an Air tag loads with its species' maximum (Entity.load), not 300.
From martipops's review of #9271.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified on playhorizon merged with main: launcher fmt, clippy -D warnings, 541 lib + 102 binary tests. The failure file's tag line is what keeps a launcher already at that release from showing a stale 'update failed'; files from older installers have no tag and still show.
The idle tests restated the predicate. The furnace, brewing stand and
campfire tests now run the tick the block runs on each idle case and
check nothing changed, and that an awake one changes, so an idle() that
drifts from its tick fails. Unloading chunks used retain, which wakes
every sleeping block entity of the level, so with moving players most
idle machines ticked anyway: unload removes only the gone ones. From
martipops's review of #9239.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: #9298 merged; main merged in (clean, nothing since the verified tree touches net.rs, remote.rs or the wire tools). Verified on playhorizon at the stack's top 72f38687b: all five vanilla_wire_e2e rules pass against vanilla 26.3 with the Login held 1 s, known-packs-as-vanilla and known-data-used included; fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
Also fixes the borrow in drop_resources_now from the machine-tool commit
(the target check is a bool now, so popping can borrow the level).
From martipops's review of #9222.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The failure file now starts with the release that failed, and a launcher
already at it or newer (another copy, the command line or the site put it
in) doesn't say its update failed. Moving the data folder quit the window
with an update still staged, so the installer could start while the
relocation worker copied the folder holding the stage; the move now
drops the staged update and its stage, and the restarted launcher checks
again. From martipops's review of #9311.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The contact now follows Player.aiStep (inflate 1/0.5/1, or the vehicle union inflated 1/0/1) and Pufferfish.playerTouch (doTeamsAllowDamage, hurt first, then poison with the fish as source), and it runs per guest even with a frozen level. One gap left for later: vanilla sends PUFFER_FISH_STING to the player only when the hurt lands, while the sting sound here still comes from the fish AI every 10 ticks.
🤖 Agent: Every place that removes or renames a listed file now goes through Entry::path, which accepts only plain names under the five install folders (mods, resourcepacks, shaderpacks, horizon-mods, horizon-native-mods), and imports from packs or other launchers can no longer bring their own mod-browser.json. Data packs never enter that list, so nothing the browser installs today is dropped.
🤖 Agent: The wrong-guess total and the retired-code count now last for the whole sharing session, and only as many relayed key exchanges run at once as wrong guesses are left, so re-registering no longer resets the relay's chances (at most 50 guesses at 20-bit secrets). Counting every failed exchange as wrong is the right call: the host sends its confirmation first, so a peer that hangs up after checking it has had its guess.
🤖 Agent: #9297 merged; main and its harness fix merged in (clean). Verified on playhorizon on the stack's top (#9473, which carries this head): the five vanilla_wire_e2e rules against vanilla 26.3 with the Login held 1 s pass, including brand-before-information and no-play-information from this PR; fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
Reject an entity whose padded render bounds lie entirely in terrain sections the renderer's visibility graph cannot reach, before any per-entity occlusion query runs; the graph must know the camera's own section, and unknown cells fail open to the existing paths. Add tests for the graph's answer and the bounds-to-section span, and tidy the fully-qualified occlusion calls in the culling tests.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The client forwarded an entity event to the particle code only if its id was
in a hand-written list in remote.rs. The table that says what each event draws
(entity_event_burst) already had rows for villager 42 (raid sweat) and dolphin
38, but the list did not, so neither showed. The list repeated the table and
had drifted from it. It is removed: entity_event_particles already looks the
event up in the table and draws nothing when there is no row.
Fixes#8589
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Mod quads, cubes, items and text come with their colour baked; the scene
shaded them a second time (an east face of a cube at 0.30 against the
block model's 0.6, glowing text dimmed). It now shades only what
entity.wgsl shades (|normal.w| >= 0.5) and culls only what it culls
(CULL_BACK, SHADED_CULL_W), so a flat quad or text seen from behind and a
mirrored part draw as in the world. A degenerate quad takes its vertex
normal. A state the registry doesn't know no longer panics in the face
culling. The docs list the scene's limits. From martipops's review of
#9229.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A ticket past /forceload's 30,000,000-block edge is refused (the ring's
arithmetic overflowed near i32::MAX). pump_chunks kept the workers fed
between ticks for /forceload only; a mod's held chunks get the same. The
docs say where tickets differ from NeoForge's and what they can't do yet
(lazy Nether/End, pausing servers, free owners, cost). From martipops's
review of #9226.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified merged with main (81dad4693, with #9428 on top) on playhorizon: launcher fmt, clippy --all-targets --locked -D warnings, 538 lib tests and 101 of 102 binary tests (the one failure is main's stale docs/launcher-commands.md, fixed on #9428). One point checked by hand: an older window hands the new installer no HORIZON_LAUNCHER_UPDATE_SHA256 only if the file in place changed under it, and then already_in_place returns Ok before the empty hash is looked at, so a mixed-version pair restarts instead of failing. Real placement on macOS and Windows is still unrun.
drop_tool and drop_into applied to every dropResources inside the
machine's with_level, so blocks popping off with the break (grass on the
dirt) dropped with the machine's tool and went into its list. They now
drop as with no tool and pop, as Level.destroyBlock does for neighbours.
A silk touch tool no longer collects ore experience
(EnchantmentHelper.processBlockExperience, after the xpRange draw), which
made an unlimited experience farm. From martipops's review of #9223.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
held() is the acting hand: during the off-hand pass of a use it is the
off hand, so equipment(MainHand) answered the off-hand stack.
LivingEntity.getItemBySlot doesn't depend on the hand acting. From
martipops's review of #9222.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: verified merged with main 81dad4693 on the desktop: clippy -p horizon-client --lib --tests -D warnings, 29 command tests, and the ignored loot_table_tests run against prepared 26.3 resources (passes; with take_loot_table's old bare retain put back it fails with the table still in the saved chest, so the test pins the fix). Matches vanilla: RandomizableContainer.tryLoadLootTable loads a tag without LootTable as a plain, empty container. The run-parity label adds opened_loot as asked.
The reference test lives in the launcher's binary, which --bins now runs in
CI: main's copy missed the game-architecture setting from #8274, so this
PR's own check would fail on it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Insert the new keys before each table's last entry (no comma changes), fold
the instance-name sentence into one key, and give copy-from its --as hint as
plain text after it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
max_bridges compared against bridges already running, so joins waiting
(up to attach_wait) for their host's attach all passed the check and a
burst could exceed RELAY_MAX_BRIDGES.
- BridgeSlot, taken under the lookup lock when a guest is matched with
its host, counts toward max_bridges from then on; dropping it gives it
back, so every way out of a join (no attach, a refused or forged
attach, an error inside bridge(), a panic) releases it.
- counts().bridges now includes matched joins awaiting attach.
- tests/bridge_slots.rs: a waiting join holds its slot (a second guest
gets Full); the count returns to 0 after a timeout, after a forged
attach, and after a bridge closes; refusals never take one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit 91d35fcae0)
parse_date multiplied and added the fields of banned-players.json and
banned-ips.json dates as plain i64, so a huge year, month, day or time
panicked in a debug build and wrapped in a release one, turning a ban's
expiry into an arbitrary date. Every step is now checked, years are bounded
before days_from_civil, and a date out of range is None, like one that
doesn't parse. Lenient month overflow (month 13) still works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit 184ea41194)
write_private (the control API's .horizon/api.json, the game's control
endpoint, the management protocol's generated key) opened the path with
create+truncate and mode 0600, which applies only when the file is new:
an existing, wider-readable file kept its permissions and got the new
token, and a link at the path was followed.
- horizon_server_files::private_file::write_private, shared by
horizon-server and horizon-client: writes a create_new 0600 temporary
file beside the path, syncs it and renames it over the path (a link
there is replaced, not followed); the temporary file goes on failure.
- Tests: an old 0644 file comes out 0600; a link's target is untouched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit 9cb9d76054)
The TCP control endpoint's token (it gates full control of the running
game) was two SipHash outputs of RandomState over the time and the process
ID. RandomState's keys are OS-seeded, but std doesn't promise it is a
cryptographic generator. It now comes from getrandom::fill, as
horizon-server's and the relay's tokens do (getrandom 0.3, already in the
lockfile, added to horizon-client-lib, which compiles control.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit aa89153fc0)
Keep passive healing on the ten-tick entity cadence for NoAI Allays. Reuse the existing heal operation and preserve normal-AI ordering and the independent duplication countdown.
Add guarded command-oracle coverage for both AI modes and health-cap boundaries, plus a reachable Mob tick regression.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Main made the audio capture a struct; the ignored resource test still
compared tuples and no longer compiled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main gave CompiledRule::OreVein the y bounds of its positive density; the
fold copies them, as they depend on the rule and not on the chunk.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: requeued after martipops's harness finding: vanilla_wire_e2e now records the held Login when the client gets it. On playhorizon against vanilla 26.3 with a 1 s hold, this head passes no-play-before-login and the same tree with main's r.joined condition fails it with 27 client_tick_end before Login. Merged with main 81dad4693 (impl Remote conflict, both sides kept): fmt, clippy -p horizon-client --all-targets -D warnings, 910 horizon-client tests.
No version installed: name `horizon-launcher install latest`.
Instance name taken: ask for another name; `instance copy-from` names `--as`.
Play gate: name where to sign in (Settings in the window, `account sign-in` on the CLI).
No change in behaviour; messages go through tr()/trf().
Fixes#9630
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#9617 made /data read a live moving piston's block entity before the ordinary
ones. The shared getter keeps that order, so block predicates see it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A load_tags error used to store an empty table for that registry, so every
#tag of it, vanilla's included, matched nothing in advancement predicates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The coverage report read "0 ported, 52 partial, 264 not yet" with reasons such
as "needs mod API 0.4". A player could not tell what comes over.
coverage.md and the Modpacks page now open with a headline counted from the
report's own rows and tiers: how many of the mods that need porting come over,
how many more Port can carry over, how many need mod support Horizon does not
have yet, and how many are not sorted yet; plus what "partial" means. Each row
gets its reason in plain words (`plain` in coverage.json, a last column in
coverage.md, a line above the technical one on the page). Counts, statuses and
the technical `reason` are unchanged. An older coverage.json without `plain`
still reads.
The three headline lines go through tr() with English values in the language
tables; the per-row sentences are English, like the reasons they sit beside.
Fixes#9629
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified merged with main 81dad4693 on the desktop: fmt, clippy -D warnings on the four client crates (all targets) and the --no-default-features server build, 2750 horizon-client/-lib tests and 1358 render tests pass. One point checked by hand: with no glyph a button's label is centred exactly where it was (x + floor(w/2) equals the old x + 2 + floor((w - 4)/2)), so mouse players see vanilla's buttons unchanged. pad_menu_e2e not rerun here (windowed, no display over ssh).
The CurseForge app's instances were listed with no version and no loader, so
`instance copy-from` demanded --minecraft and the copy lost its loader. They
now use curseforge_instance_target (minecraftinstance.json, else manifest.json).
--minecraft still overrides; the Modrinth App path is unchanged.
Fixes#9628
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Wolf.mobInteract and Cat.mobInteract answer SUCCESS for sitting down, for
food that heals a hurt tame one and for food that tames a cat, before
Animal.mobInteract is reached. AbstractHorse.fedFood and Panda answer
SUCCESS_SERVER, which swings on the server only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve the focused, full inline, named admission and valid-empty diagnostics alongside current shared command fixtures.
Co-Authored-By: Codex <[email protected]>
The proxy wrote the Login's line and then slept, so whatever the client sent
during the hold landed after the Login in the capture, where
no-play-before-login stops looking: a client sending tick ends for the whole
hold could pass. Sleep first, then record (martipops's review on #9297).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Line of sight now clips with the viewer's collision context, as LivingEntity.hasLineOfSight's ClipContext(COLLIDER, entity) does, so scaffolding is open from below and solid from above. I ran the new test against prepared resources too: it builds the world and passes, not vacuously.
Retain map metadata and max-scale controls alongside current fixture groups and explicit boat-use packet actions.
Co-Authored-By: Codex <[email protected]>
Keep current shared oracle and corpus fixtures alongside the unchanged H300 room, light observations and immediate/once diagnostics.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Moving-piston NBT is read from the live tick entry with the existing piston serializer, and writes are still refused before any detached block entity exists. The ordered pretty-print context applies only to a live piston's requested subtree, so other /data get output is unchanged.
🤖 Agent: The per-address host cap mirrors the existing rate limits (off when the relay can't see addresses; IPv6 counted per /64 with a 4x /48 cap). HostSlot gives the slot back on every way out of host(), and the integration test holds connections open to prove it.
🤖 Agent: The tree check follows ClientboundCommandsPacket.validateEntries: one pass over redirects (canBuild) and one over children (canResolve), so vanilla's own execute-run-to-root redirect still passes. It is iterative, so a long chain cannot overflow either.
🤖 Agent: Dialog open_url now goes through the same untrusted_http_url check as chat click events (ExtraCodecs.UNTRUSTED_URI: http/https only), so file:, UNC and protocol-handler links never reach open_uri.
🤖 Agent: The owner's window always joins its integrated server through memory, so refusing the owner's UUID over the network costs the host nothing and closes the offline-name takeover. Splitscreen seats already had this rule.
🤖 Agent: The depth guard is a thread-local counter around read_added and effect_details, released on drop, so an error leaves nothing counted. 64 levels still decode, and one guarded Cell per component costs nothing next to the decoding itself.
🤖 Agent: Along each axis the rounded squares fall and then rise, so the cells inside a sphere form one run per row. Skipping the rest changes no draw or query (vanilla only draws for inner cells), and your snapshots of 4 decorated worlds match byte for byte.
Turn the open questions into three decisions, each with options, a
recommendation, the reason and the step it blocks. State what the HDR
pass does with a shaders/ pack's alpha today, and move the full-screen
peak and light-source info for post stages to the later additions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve all landed fixture routes and one declaration of the shared scratch/reference options. Runtime pair remains qualified to 21c7f4597.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: Boat items now clip like Item.getPlayerPOVHitResult (float view vector, OUTLINE, Fluid.ANY) through the shared ray code instead of 0.05-block fluid sampling; the new boat_item_hit harness runs in CI parity.
Keep accepted-hit gating and attach the actual fish to poison effect criteria, independently of fish AI and frozen world ticks.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: The same hits come in the same order, with the budget kept (both tests compare against the cell-by-cell walk). Measured: +0.07 ms per tick with 1500 ground mobs (palette check per section), noise for flying mobs, and the far-teleport worst tick goes 2290 → 160 ms.
A server dialog's open_url action, from a show_dialog packet or a
show_dialog click in chat, took any string. A click went to open_uri, which
hands it to ShellExecute on Windows and to xdg-open or open elsewhere. A
button naming file:///C:/.../cmd.exe, a UNC path to an .exe on the
attacker's share, or a custom protocol handler could start a program. The
link prompt is on by default but shows only the raw string; with "Prompt on
Links" off it opened at once.
Vanilla's ClickEvent.OpenUrl takes an ExtraCodecs.UNTRUSTED_URI (http and
https only). Chat components already applied that check; dialogs now share
it, and a refused link leaves the button with no action.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Vanilla's trapdoor ladder rule is instanceof TrapDoorBlock, so a tagged block with no open or facing is now just not a trapdoor. A real ladder still needs valid facing, and the tests cover the three non-trapdoor shapes.
🤖 Agent: The client services are owned snapshots with no authoritative writes, and every lifecycle hook is empty without a mod. The menu_background result is clamped to a finite 1..64, so a mod cannot hand the blur a bad radius. I dropped the enable_older_servers helper (see my comment).
Keep upstream corpus groups and opt-in loaded-fixture controls alongside the live-piston contract and full presentation diagnostic.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A host without a Microsoft account opens the world in offline mode, where a
player's UUID is offline_uuid(name). The owner's UUID is offline_uuid of the
host's name too, and the login gate let that UUID in from anywhere. So a LAN
or friends player who logged in under the host's name passed the gate, the
duplicate-login handling kicked the host's own window, and the guest played
on as the owner: operator level 4, the owner's player data, and the
movement checks the owner skips.
Apply the splitscreen seats' rule to the owner: the owner's UUID is let in
only over the in-memory connection. Vanilla never reaches this state because
IntegratedServer always authenticates LAN guests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Item stacks can hold stacks in their components (bundle contents, charged
projectiles, use_remainder, container), and a mob effect can hold a hidden
effect. The decoders called each other with no depth limit. A payload a few
hundred thousand levels deep fits in one 2 MiB frame and overflows the
decoding thread's stack. In Rust that aborts the whole process instead of
failing the packet: one creative-slot packet from any player stopped a
dedicated server or a LAN host (with the host's own window), and one slot
packet from a server stopped the client. Vanilla's decoder turns the
StackOverflowError into a disconnect.
Count nesting per thread in read_added, which every stack-in-a-stack passes
through, and in effect_details, and return an error past 64 levels. The
packet then fails like any other bad packet and only that connection
closes. Survival bundles nest about 16 deep at most.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The relay only rate-limited host registrations (6 a minute per
address) against the global `max_hosts` (1000). A host that answers
pings keeps its slot as long as it likes, so one client could register,
hold every control connection open and fill all slots: in under three
hours from one IPv4 address, in seconds from an IPv6 /48 (limits count
by /64, and a /48 holds 65536 of them). Every real host then got
`Full`.
Now the limiter also counts the hosts each address key holds open:
at most 4 (`RELAY_MAX_HOSTS_PER_ADDRESS`), and an IPv6 /48 at most four
times that, however many /64s it spreads over. A registration past the
cap gets the existing `Full` reply. The count goes up when a host
registers and back down through an RAII guard (`HostSlot`) when its
connection ends, on every way out of `host`, panics included. Like the
other per-address limits it is off with `RELAY_PER_ADDRESS_LIMITS=0`.
Filling the default 1000 slots now takes 250 IPv4 addresses or 63 IPv6
/48s, not
one client.
Tests: an integration test registers the cap's worth of hosts from one
address, sees the next refused with `Full`, closes one and gets its
slot back; a limiter test covers the /64 and /48 caps, a freed slot and
the limits switched off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A short code's secret is 20 bits, and the docs promise the relay can't
get into the world. But the host gave a relay (or anyone driving its
joins) unlimited online guesses:
- after five wrong secrets the code was retired, but the wrong count
was reset on every registration and `run_relay` registered again at
once, so each cycle bought five fresh guesses for one round trip;
- a guess was only counted when the guest's confirmation failed to
verify. The host sends its own confirmation first, so a peer could
check it against its guess offline and hang up: an uncounted guess;
- the five-guess limit was checked after the fact, so joins sent all
at once ran any number of key exchanges in parallel.
Now the host keeps the wrong guesses for the whole sharing session
(`Guesses`): a count per code (reset when it registers), a total that
never resets, and a reservation for each key exchange in progress, so
only as many run at once as wrong guesses are left. Every relayed key
exchange that ends without a confirmation counts. After a retired code
the host waits 1 s, 2 s, 4 s ... (at most 5 minutes, `retire_delay`)
before registering again, and after 50 wrong guesses in one session it
leaves the relay (`HostEvent::RelayStopped`; the game says so through
a new translation key). The full code keeps working directly.
The math: each short code has a fresh secret, so every guess hits with
probability 2^-20 and guesses don't narrow anything down.
- With the cap, a relay gets at most 50 guesses per sharing session:
50 / 2^20, about 1 in 21,000. Spending them takes at least
1+2+...+256 s = 511 s of waits.
- With the backoff alone (capped at 5 min, 5 guesses a cycle), the
steady rate is 60 guesses an hour; a 50 % chance needs
ln 2 * 2^20 = ~727,000 guesses, about 12,100 hours (~505 days) of
continuous sharing. Before: one round trip a guess, hours.
Tests: the per-code count starts over at each registration while the
total stays, the waits double (1 s ... 256 s) until the host stops at
50; exchanges in progress count against the limit; `retire_delay`
doubles up to 5 minutes. The relay's tunnel test still sees a new code
after five wrong secrets (now after 1 s).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OreFeature.doPlace scanned each sphere's whole box and divided for every
cell, although a cell's square along one axis depends on that coordinate
alone. Each axis's squares are now worked out once per sphere with
vanilla's expression, so every sum is bit for bit the same, and each row
visits only its run of cells inside the sphere: along an axis the squares
fall and then rise, so the passing cells form one run around the smallest.
The cells tested, the world queries and the random draws stay the same
and in the same order. A test compares the walk with vanilla's full box
scan over 20,000 spheres, including ones whose edge sums to exactly 1.
Fixes#9609
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three things a server sends were decoded without vanilla's checks, and each
let a server crash or stall the client:
- A light layer shorter than 2048 bytes: the column-top sky light indexes the
layer directly, so an empty layer panicked the main thread. Vanilla's
DataLayer constructor refuses any other length; Layers now does too.
- A command tree whose children (or redirects) loop: the usage hint followed
the loop when the player typed "/" and overflowed the stack. Commands now
runs ClientboundCommandsPacket.validateEntries' check ("Server sent an
impossible command tree"), as an O(n) depth-first walk so a long chain
can't make the check itself slow.
- A dimension type's min_y and height went unchecked into the chunk format,
so a height of 2^31 made the client allocate gigabytes for light. A shape
outside DimensionType's codec (multiples of 16, at least 16 high, inside
-2032..=2031) now keeps the named type's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The older-server adapters are off by default while they are incomplete, and
only the environment turns them on. A Host helper that turns them on for the
whole process would let any installed mod do that for its players; it is
left out of this change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The player predicate now tests stats, advancements, recipes and level, and player_entity_for_any stays &self, so #9494's compound is added only by the &mut self caller.
🤖 Agent: /execute if loaded now also needs the entity-ticking status and saved entities applied, which matches ExecuteCommand.isChunkLoaded (FullChunkStatus.ENTITY_TICKING && areEntitiesLoaded). The simulation permission tracker is untouched.
🤖 Agent: Dawn Client is only one more place to find the player's own 26.3 jar and assets (cache/minecraft under .dawn). The tests check that nothing is fetched from Mojang.
🤖 Agent: A replaced pack becomes a new entry and the old one is unloaded, as ServerPackManager.pushNewPack marks it SERVER_REPLACED, so the reload ends instead of looping.
🤖 Agent: The four seats match the passengerAttachments of HAPPY_GHAST in 26.3's EntityTypes, and the index clamps as EntityAttachments.getClamped does.
🤖 Agent: No-pack worlds keep the existing preload. Only an overlay with damage-type definitions seeds SyncData at world entry, so the default path is unchanged.
The mod browser records what it installed in `mod-browser.json` in the instance folder, and
Update, Remove and turning a mod off act on each entry's `file` through `home.join(file)`.
Nothing checked that path: an absolute path replaces `home`, and `..` climbs out of it. A Java
modpack (`.mrpack` with a loader) copies every relative path from `overrides/` into the new
instance, so a pack could plant its own `mod-browser.json` with an entry such as
`"file": "/home/me/.ssh/id_ed25519"`, and the launcher would delete or rename that file when the
player clicked Update, Remove or the on/off toggle on the fake entry. Copying a Prism/MultiMC
instance folder could plant the same file.
- `Installed::load` now leaves out (and logs) any entry whose `file` isn't a relative path of
plain names inside a folder the browser installs into (`mods/`, `resourcepacks/`,
`shaderpacks/`, `horizon-mods/`, `horizon-native-mods/`), with at least one name below it.
- `Entry::path` applies the same check where files are removed or renamed (update, remove,
toggle, Java ports, registry installs), so an entry that didn't come from `load` can't reach
outside either.
- A modpack import never writes `mod-browser.json` (any case), and the other-launcher copy skips
it.
Tests: a planted list with an absolute path, a `..` path, a path climbing out through `mods/`
and a bare `horizon-mods` keeps only the valid `mods/x.jar` entry; Remove and toggle leave a
file outside the instance untouched. A Java `.mrpack` with `overrides/mod-browser.json` imports
its mods but not the list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: The mod id is read from the mixins.incoming/<id>/ path that stage_mixins writes, so the culprit named for E_MODULE_CONFLICT is right. I removed the Fixes lines for #9016 and #9414, which this PR does not fix.
🤖 Agent: The snapshots now key on a build.rs hash of the world sources, so a relink no longer throws them away, while a change to those sources still does.
🤖 Agent: The launcher's own endpoint still wins when both games answer. A crashed game's file no longer counts (a TCP endpoint must accept its token), and a change of endpoint restarts the horizon-ctl child, whose Drop kills the old one.
🤖 Agent: Animated strips are now cut to their first frame for every atlas directory source, not only item/ and block/, so a pack's own folder (wynn/) animates as vanilla's does.
🤖 Agent: Recovery keeps only whole frames: the crashed .tmcpr.tmp is cut at its last complete packet record before it becomes an .mcpr, and a writer that may still be alive (its pid is checked) is left alone.
🤖 Agent: Applying detail now stops at DETAIL_BUDGET (3 ms) per frame and the rest waits in detail_backlog; with #9515 the widths reduction runs on the worker, so neither the burst nor its first read lands on the frame.
🤖 Agent: Captured server sounds keep their entity, category and excluded player and go out between the entity's add and remove, so a client always knows the emitter when the sound arrives.
🤖 Agent: entity_killed_player fires from the guest's death with the blow read before the death message, as ServerPlayer.die does; the killer view stays on &self on top of #9494.
🤖 Agent: End gateways now keep a same-level vehicle live and teleport it with its passengers. As you note, #9461's temporary mob_can_use_gateway restriction is no longer needed with this.
🤖 Agent: The whole vehicle tree moves through Entity.teleportCrossDimension's calculatePassengerTransition, with UUIDs and mount links restored, so riders keep their vehicles across /execute in … run tp.
🤖 Agent: ModelBakery.DESTROY_STAGES_OIT exists in 26.3. The OIT crack sprites are picked only with improved transparency on, so the default look is unchanged.
🤖 Agent: native-mods.json is refreshed through a fresh hardlink and an atomic rename, so nothing is written through the old link. native-data is a relative symlink inside the app bundle.
🤖 Agent: started_riding now fires for every ServerPlayer the vehicle carries (Entity.getIndirectPassengersStream), not only for the player who boarded.
🤖 Agent: The fight keeps dragon_uuid (vanilla's save key) and ticksSinceDragonSeen, as EnderDragonFight.tick does. A summoned dragon that dies after the fight no longer opens a gateway or gives 12000 XP.
🤖 Agent: renderable_float swaps only a float target the adapter cannot render (to 16-bit float, then Rgba8Unorm). Vulkan, Metal and DX12 keep the pack's declared format.
🤖 Agent: The native pack surface only holds bit patterns, written with blend: None and read with textureLoad. Rgba32Uint keeps the same bits and can be rendered on wgpu's GLES backend.
🤖 Agent: Fluid blocks win the material vote only for their own still and flowing sprites. Voting for every other sprite is unchanged, and the new test covers the shared fluid sprite.
🤖 Agent: The harness now fails on a server that is still up after server/stop (the #8245 regression the issue names), and it runs the same check on Windows paths.
🤖 Agent: At midnight monsters_burn is false, so isSunBurnTick returns before its RNG draw on both sides. The zombie horse reference stops being random, and the comparison itself is unchanged.
🤖 Agent: On a client, entity handles read only what the server synced. Writes (set_velocity, merge_nbt, tags) return false, and a handle from another level's epoch reads None, so a client copy changes only when the server says so.
🤖 Agent: With the default texture budget, map_bytes is still exactly 64 MiB, so nothing changes until a player raises textureBudgetMiB. The unit test pins both cases.
🤖 Agent: The primitives get their own pipeline at the end of the world pass, before the hand. With no subscribing mod the batch is empty and prepare() returns before any upload or draw.
🤖 Agent: native_entity_outline starts from vanilla's own answer (team colour or white) and costs nothing without a listener; masking above 24 bits keeps the colour in range.
🤖 Agent: Language.loadFromJson parses with a lenient Gson reader in 26.3, so packs with comments and unquoted keys now load their lang files instead of being dropped silently.
🤖 Agent: Team joins happen in remember_uuid, which every entity load goes through. An entity loaded without a UUID gets one first, since the scoreboard keys entities by UUID, as Entity.load does.
🤖 Agent: The hoglin reads CannotBeHunted and writes it back only when set (Hoglin.addAdditionalSaveData), so bastion hoglins from the template are no longer hunted by their own piglins.
Keep the upstream piston entity-tick-order group and the independent
live-piston presentation diagnostic.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Read the authoritative tick entry with command metadata and source-aware
pretty ordering. Preserve default output and reject unsupported writes.
Cover the original pulse, a normal sticky pull, and scoped diagnostics.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
🤖 Agent: The named predicate now uses the existing campfire smoke helper (CampfireBlock.isSmokeyPos: lit tagged campfire, first-blocker exception), and the real game command harness covers it.
🤖 Agent: gui_render_post has a read-only host and draws nothing without a subscriber; the fragment is pending, so the API number is assigned at build time.
🤖 Agent: mc_chunkFade is injected only when the renderer supplies it and the pack's vertex program reads it, so packs that don't ask compile as before. The disclosed inputs are an MIT pack's API names only.
🤖 Agent: The values are bit-identical (tabulated perlin corners, 4-column interpolation rows, branch-free range choice), and your noise snapshots compare byte for byte.
🤖 Agent: The surface pass gives the same blocks: positive_y_bounds only skips bands where the vein density is provably <= 0, and the material_condition probe covers the column reads.
🤖 Agent: Tooling only. Clearing the floor to air right before the exact stone fill makes the 256-block guard independent of the scenarios that ran earlier.
🤖 Agent: /data merge and /data modify now reload minecarts (the new Minecart::load) and boats, which matches EntityDataAccessor.setData running Entity.load on every entity but a player.
🤖 Agent: Biome tags now load with the same pack overlay as the other loot tags, and location_check reads biomes through holder_set (#tag, one id or a list), the HolderSet codec LocationPredicate uses.
🤖 Agent: keyed() compares full(key) for the four keys item predicates read, and the potion_contents value is normalised the same way, as the registry codecs behind ItemPredicate do.
🤖 Agent: This matches Chicken.aiStep: the sound and entity_place come only after a drop. One small RNG note: the pitch floats are still drawn before the loot roll, which only differs for a data pack that empties chicken_lay.
🤖 Agent: No vanilla advancement has an nbt or reference predicate, so mob_to_nbt only runs for data packs that ask for it (Tree::index_nbt). player_entity_for now takes &mut self; #9561 and #9562 are stacked on that.
🤖 Agent: riders::tick returns at once for a mob with no mount, so lending the vehicle's navigation costs nothing outside ridden vehicles. #9597's branch also carries this head (their oracles meet in the same switch).
🤖 Agent: Piston block entities now tick after entity travel, the order ServerLevel.tick uses (entities, then blockEntityTicks). The new 6-tick wither skeleton fixture covers both stone and slime, and it matches all 24 observations.
Add FPS-overlay captures from the release OpenGL benchmark for PR #8941. The measured source revision is d87992b.
Co-Authored-By: OpenAI Codex <[email protected]>
ClimbingRegistry::new returned "invalid trapdoor open state" (or "invalid
climbing facing") for any state of a block in minecraft:trapdoors that lacked
a valid open or facing property. Macaw's Bridges adds blocks that are not
trapdoors (rail bridges with facing/powered, a pier with no properties) to
minecraft:wooden_trapdoors, so All the Mods 10 and Craftoria exited before the
window opened.
Vanilla's LivingEntity.onClimbable tests `instanceof TrapDoorBlock`, not the
tag, so such a block is not a trapdoor and never climbs. A tagged state now
counts as an open trapdoor only with facing and open=true; otherwise it is
skipped. The ladder's facing check is unchanged.
Fixes#9585
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Both #9494 and #9562 index advancement triggers (nbt, player facts) and widen
the player's view: kept both. player_entity_for adds the compound on &mut self;
player_entity_for_any (#9562) stays &self for its &self callers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#9494 makes player_entity_for take &mut self to build the player's compound;
guest_entity_view (#9561) is &self, so it reads what the player looks at only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A far move (one move packet thousands of blocks away) made the server's
collision sweep walk every cell of the swept box, one query per cell:
a 2.3 s tick for a hop of 8,000 blocks, growing with the square of the
distance.
Two opt-in provider hooks let the scan pass, after one query, the rest of
a row inside the same chunk (at most 16 cells):
- missing_chunks_stay_missing: a chunk missing once stays missing for the
scan (SceneCollisions, EntityCollisions, EmptyCollisions).
- section_is_empty: no state in the section's palette has a collision
shape (SceneCollisions, cached per section in the scan).
Both default to false, so the parity probe, which records every query,
sees the same calls. Skipped cells yield nothing, so the collisions and
their order are unchanged.
Fixes#9594
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_send_map (API 132) sends a 128 x 128 map picture to a player but was
not in the helper catalog, so a trait mod could not call it. One catalog
fragment adds it to Host as send_map(uuid, map_id, colors) -> bool.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: binding the slot origin to a let first matches vs_section_flat_opaque/cutout; it is compatible with #9584's packed_corner change, which fixes the other X3500 site in the same shader.
🤖 Agent: checked the select() rewrite of packed_corner: rel = (i - axis) mod 3 gives the axis component +side, the next +du, the last +dw, the same values as the old indexed stores, with no dynamic l-value for FXC.
The launcher's game_* tools only looked at the endpoint in its data folder,
so a game the player started, which listens on the game's default endpoint,
answered "not running under agent control". Each call now goes to the first
endpoint where a game answers, the launcher's own first, restarting the
horizon-ctl child when that changes: an installed horizon-ctl keeps the
endpoint it started with.
Whether a game answers is now tested on Windows too, with the endpoint's port
and token, rather than by the file existing: a crashed game leaves the file.
Fixes#9465
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Queued. The home icon and the new ⋯ Edit instance item open the existing edit dialog. In the palette, the instance's saved picture stays offered after another icon is picked, so the player can go back to it without choosing the file again. Both new labels already have translations in launcher/lang.
🤖 Agent: Queued. The invite field's right padding now follows the Join label's width (14 px padding each side, about 8.5 px per character at 14 px), so the longest translations (Bulgarian and Latvian, 15 characters) get 175 px instead of 84 and the button no longer covers the code. In the wide layout the field grows by the same amount (340 to 431 px at most), which comes out of the row's flexible gap. English stays at 84 px.
🤖 Agent: Queued after a small fix on the branch. On main, barrels, shulker boxes, hoppers, dispensers and droppers already load as BlockEntity::Chest, so #9189 already covered them. This PR's real change is that a LootTable id the codec rejects no longer counts as a table, which matches vanilla 26.3's tryLoadLootTable (KEY_CODEC read, then null). I folded the four copied tests into one table test and made roll_loaded_loot read the id the same way, so an invalid table with no items rolls nothing. Desktop run: clippy -D warnings clean, horizon-client-core 63 tests, horizon-client 895 tests, horizon-client-lib 1718 tests.
🤖 Agent: the preset only sets PvP & HUD options and vanilla's toggle-crouch, and turns on the official fps/keystrokes mods only if they are installed. Verify passes at 41c0e4080 (the stack's top: workspace clippy -D warnings clean on the desktop).
The barrel, shulker, hopper, dispenser and dropper copies of the chest test
become one table-driven test. A loaded container whose LootTable the codec
rejects now rolls nothing, as vanilla keeps no table for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: after martipops's review, the chat search puts the typed text aside and gives it back on every exit path, a compact repeat keeps a reader's scroll, and search and copy skip legacy § codes (tests for the last two). Compact chat still never merges signed messages. Verify passes at 4984cc061.
🤖 Agent: the own time of day and weather change only what this client draws (sky, lightmap, rain); the world's day_time and weather, and everything that plays by them, stay the server's, and nothing is sent. Off by default. Verify passes at cd788cd4c.
🤖 Agent: the visual tweaks (low fire, clear water/lava fog) are off by default and stop at the clearest view vanilla itself gives. docs/pvp.md now says they can matter on strict servers (from martipops's review on #9250). Verify passes at d2dafa504.
🤖 Agent: numeric enchantment levels change only the tooltip's displayed numeral (off by default); the enchantment data and the rest of the tooltip stay vanilla. Verify passes at 6eae1c73b.
🤖 Agent: after martipops's review, the combo counts a hit only when the server's damage event names this player as the direct entity (spear jabs included, teammates' hits and fire ticks not), the off server widget no longer allocates each frame, and ping shows nothing before the first latency update. Verify passes at 3842a7aee.
🤖 Agent: the armour, held-item and effects widgets read only what the client already holds, and the effects widget replaces vanilla's icons only while it is on (all off by default). Verify passes at f9f1b5abf, with main 6319cb701 merged in; no harness is touched, so parity-main covers it.
Delegate the predicate view to the existing lit and occlusion-aware smoke
helper. Cover both predicate signs, callback stores, slab exceptions and
positive world guards through the real command runtime.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Importing a CurseForge app instance folder left minecraft, loader and
loader_version empty, because instance() read them only from
mmc-pack.json. Fill them from minecraftinstance.json (gameVersion,
baseModLoader.name), falling back to manifest.json via the existing
curseforge() parser. minecraftinstance.json wins: it records what the
instance runs, while manifest.json is the pack author's copy and goes
stale when the player changes version or loader in the app. A Fabric or
Quilt id's trailing Minecraft version is not part of the loader version.
Missing or broken files never fail the import; mmc-pack.json behaviour
is unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
In vs_section_flat (crates/horizon-client-render/src/terrain.wgsl), passing the nested dynamic storage buffer expression slot_origins[block_slots[s.slot >> 8u]] directly into corner_vertex(s.c, ...) causes Direct3D 12's HLSL compiler (FXC for SM 5.1) to fail during 'Terrain solid (flat)' pipeline creation with: error X3500: array reference cannot be used as an l-value; not natively addressable.
Like vs_section_flat_opaque and vs_section_flat_cutout, bind the sampled origin to an intermediate local variable first, letting naga emit a load into a temporary r-value before the function call.
Co-Authored-By: Antigravity <[email protected]>
LabPBR normal and specular maps had a fixed 64 MiB budget, so with about
1300 block layers they always loaded at 64 px, however high
textureBudgetMiB was set. They now get a quarter of the current budget:
the default budget keeps the old 64 MiB, and 1536 MiB gives 256 px maps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QPo9BpqkYWfY6y5Nwu
native_entity and the entity_*/living_* reads now work against the
entities the server shows the window (own player, other players, mobs,
armor stands with their custom names and equipment, tracked-only
entities by type and position); a fresh epoch per client level keeps
handles from crossing levels or sides. Writes are refused and saved
data and tags read as None on a client. native_menu_stacks gives the
open server menu's stacks, with components, by server slot number.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Skip doomed async work while the camera moves, reduce synchronous voxel-plane lookups, and cull dynamic block entities before generating their geometry. Reuse chest state and remove dye-prefix sorting from the block-entity hot path.
Co-Authored-By: OpenAI Codex <[email protected]>
🤖 Agent: block outline and hit colour stay render-only and off by default; verify and parity pass at 4adfac92b. The composter fix#9508 is merged in. pack_screen's failures came from the playhorizon runner missing 142 vanilla lang files: the files were copied there and it passes now.
native_world_primitives runs once per world frame and hands a client mod a
batch of world-coordinate primitives: lines with a width, box outlines and
filled boxes in ARGB, beacon-style beams and camera-facing text in the game
font (optionally scaling with distance). Each can be seen through walls.
The game tessellates them camera-relative and the renderer draws them with
its own pipelines (gfx/world_primitives) at the end of the world pass,
after the entities and before the first-person hand: the world's reversed
depth, draw resolution and TAA jitter. A GPU test shows depth-tested quads
and lines hidden behind a wall while see-through ones show.
Closes#9555
Co-Authored-By: Claude Opus 5.5 <[email protected]>
native_entity_outline(entity_id, kind, outline) lets a client mod make any
mob, player, dropped item or projectile glow in its own colour, or take the
outline away (Fabric/NeoForge isCurrentlyGlowing and team colour overrides).
Vanilla's answer comes in; only what the client draws changes.
native_visuals no longer shares 8 rims and 256 edges between all mods: each
trait mod is dispatched on its own and gets 256 rims, 4096 rim edges and
8 lights. A panic in a mod dispatched alone still names the right mod.
Closes#9553
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Dawn Client stores its official vanilla Minecraft assets, libraries, and client jar in .dawn/cache/minecraft/ rather than the default .minecraft directory.
Add Dawn Client default folders to known_folders, include cache/minecraft in find(), resolve Dawn sync/instance directories upwards to the launcher root in resolve(), and update troubleshooting advice.
Co-Authored-By: Antigravity <[email protected]>
Read the boat's existing resolved leash state and reuse the holder
conversion already used for mobs. Keep pending saved UUIDs separate.
Cover pending, live and unleashed contexts through the real Game command
path, with typed subjects, callbacks, stores and measured clocks. Retain
movement as a separate raw diagnostic and isolate the harness directory.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The death message takes the combat record's entries and leaves them
empty, so entity_killed_player and kill_mob_near_sculk_catalyst, which run
after it in guest_die, saw no killing blow while show_death_messages was
on: any killing_blow condition failed. ServerPlayer.die reads the
CombatTracker for its triggers before recheckStatus clears it at the end,
so the blow is now taken before the message is built.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Keep the nonempty operand's token metadata when pasting with an empty operand.
Original GCC/Clang controls and the pristine-main control retain the following
call's parentheses; add that regression to the boundary suite.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
A pack's items or blocks atlas can list its own folders (Wynncraft's `wynn/...`). Their animated
strips were packed whole, so a 16x32 two-frame item drew both frames squashed into its slot.
They now keep their first frame and animate, as vanilla does for every sprite of those atlases;
a still texture there stays whole.
Fixes#9540.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.load: a string Team adds the entity's UUID to that scoreboard team
when it exists, so /summon ... {Team:"red"} and structure or data pack
entities join it. Team is only read, as in vanilla, so the kept compound
drops it.
Fixes#8049
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hoglin.readAdditionalSaveData/addAdditionalSaveData keep the CannotBeHunted
flag (a bastion's hoglins are placed with it), and Hoglin.canBeHunted is
false for them: PiglinSpecificSensor never picks them as prey and a hit
from one doesn't rally the piglins around.
Fixes#8048
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Initialize the implicit input per source vertex and isolate it from geometry-stage globals, with original paired and GPU controls.
Co-Authored-By: Codex (GPT-6.1 Sol) <[email protected]>
A Nintendo pad's face buttons are lettered the other way round from an Xbox pad's: A sits on
the right and B at the bottom. Going by position (SDL's layout, the default), B confirms and
jumps and A goes back, while the Switch's own menus and keyboard confirm with A.
PadSettings::nintendo_letters (`nintendoLetters` in horizon-controller.txt; off unless set, on
in a build for the Switch itself) reads a Nintendo pad's face buttons by their letters: A plays
South (confirm, jump), B East (back, drop), X West, Y North (Button::by_letter,
PadState::by_letter). It applies to a player's pad state, read from the pad or fed by
splitscreen's router, before horizon-ctl's virtual pad is merged; splitscreen's join and
lost-controller prompts, which a controller nobody holds yet answers, stay by places. Bindings
still name the part, so they mean the same either way. With Nintendo glyphs, the binds list and
the on-screen keyboard's hints name the button that plays each part (OptionsUi::pad_by_letter).
docs/controllers.md describes the setting.
Test: horizon-client-input gamepad::tests::a_nintendo_pad_by_its_letters_confirms_with_a, and
the settings round trip.
(cherry picked from commit fb2490c02, reworked onto main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the in-bed chat (InBedChatScreen) the pad and the keyboard could only reach the chat box:
Leave Bed was drawn with screen_draw::button, which registers no widget, and focus only lands on
what a frame registers (key_focus::drawn_targets). Game::leave_bed_draws records it as a button
at the bounds the click checks, so the pad's focus starts on it (pad_first_target's first
button), D-pad up from the chat box reaches it, the keyboard's Tab reaches it as vanilla's does,
and A or Enter wakes the player.
Test: game::chat::tests::leave_bed_is_a_focus_target_above_the_chat_box.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sleeper gets InBedChatScreen, a chat screen the game opens on its own. With a pad in use,
pad_auto_keyboard opened the on-screen keyboard over it (Steam's on a Steam Deck) as if the
player had opened chat to type, every time the player lay down. The keyboard now waits for A
on the chat box there; chat the player opens still brings it up. docs/controllers.md says so.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PackedStorage::read and write are the per-block word accesses of section
fills and lookups in horizon-world (BlockSection::set and fill_run, the
terrain fill's solid runs, surface column reads). Without #[inline] each
was an out-of-line call across the crate boundary that recomputed the
slot; inlined, the caller's loop keeps the slot arithmetic and word
access in place. Behavior is unchanged.
(cherry picked from commit 680cabdba245cd9c373fde94164377353c952785)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The in-range/out-of-range merge stored the out-of-range value only where
the selector was outside the range, a conditional store LLVM leaves as a
branch per value (unpredictable for noise selectors). It now writes
every value as a select between the in-range and out-of-range samples,
which vectorizes; the selected values are the same.
(cherry picked from commit 5434c4113f94d6fa98a56f581eea1f8d27ac1352)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block volume's interpolated values come from repeated addition down
each column, one dependent chain per column. A cell row four columns
wide (every row of a 4-wide cell inside the volume) now advances its
four chains together, writing each column's value before adding its
increment, so their additions overlap instead of waiting on each other.
Each column keeps the same operations in the same order. Corner samples
are read through precomputed column offsets instead of a checked 3D
index per corner. Partial rows, and rows whose columns fail validation,
keep the one-column-at-a-time loop and its error order.
(cherry picked from commit 11964a2c3ed6dc8752ef46f3a05ed27d0c36602d)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column's corner XZ dot products and Y gradients depend on the y cell
only through each corner's gradient index. When a volume's y samples
change cell at least four times, each column tabulates the sixteen
gradients' terms once (the same operations as before) and every cell
change reads them, instead of loading three gradient components and
computing two products per corner. The per-y cells, fades and offsets
are kept as separate lanes. High-frequency octaves (the top ten of the
blended noise's limits) gain the most; values are bit-identical.
(cherry picked from commit f3bf17341351a1e02028434765195f5c252b2d24)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Volumes are laid out with y contiguous in each column, so the y-axis
gradient computes the first column and copies it into the others,
instead of storing each value at a stride across every column. The
values, and the error on a zero-period gradient, are as before.
(cherry picked from commit b01ee5606dc23caea2713954a71987aa76c37a00)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column is filled from its sections: an all-air or one-state part at
once, a mixed section from its storage. Beside each id the cache keeps
whether the block is air, fluid or solid, so the walk and its
stone-depth look-ahead no longer look the properties up per read. The
band is stepped down as the walk descends instead of searched per block,
and the column's minimum surface level is read once.
(cherry picked from commit a88b3f32dfd283741de6e149708b5325a90a4386)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An ore vein places nothing where its density is not positive, and
returns before drawing its positional random there. Rule libraries read
from the density graph where that holds (DensityGraph::positive_y_bounds:
a range choice on a clamped y gradient whose out-of-range value is a
constant <= 0, as vanilla's copper and iron veins are), so datapack
ranges are followed. The surface pass splits its y bands at those
bounds, drops each vein from the bands outside them, makes each band's
variant on first use, and does not set up a block whose variant gives
nothing. The veins' up-front densities are sampled over one shared
volume cut at the highest y a vein reaches; cutting a volume's top
leaves the values below it as they were.
(cherry picked from commit 8c101d76d544e84987274b8c01b44a0811430eb9)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A write at or below a heightmap's first available y - 2 leaves it as it
is (Heightmap::update returns early). Each surface column now carries
that bound per stage map, as fill_noise does, refreshes it when a map
changes, and calls Heightmap::update directly above it, without the
repeated registry and ensure_heightmap checks. A missing map still goes
through set_block_worldgen, which primes it.
(cherry picked from commit 2015977dfeb33f38d64be64b9b643a3059544d2a)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPackManager keeps packsToLoad and packsToUnload as ServerPackData
objects; the port kept ids, and a pack pushed again with an active pack's id
was set active again after every reload: an endless resource reload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Three sounds were played under ids that 26.3's sounds.json doesn't have,
so they were silent: block.turtle_egg.break (entity.turtle.egg_break),
block.conduit.ambient_short (block.conduit.ambient.short) and
block.generic.extinguish_fire (entity.generic.extinguish_fire).
Fixes#8055
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerPredicate's stats, advancements (done or per criterion), recipes and
level were read by nothing, so criteria using them were granted to anyone.
They are now tested as PlayerPredicate.matches does. The player's records
are copied into its view only for the triggers whose criteria may ask
(Tree::player_facts_asked) and for the command predicates that name them
(facts_asked_in); a field asked of records not filled fails.
inventory_changed now also tests the criterion's player conditions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKjxbXfo4yzskjMcNq
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
ServerPlayer.die calls awardKillScore on the entity
getKillCredit names, which fires ENTITY_KILLED_PLAYER (KilledTrigger) for
the dying player with that entity and the death's damage source. Nothing
fired it: guest_die, every server-side player death, now does, between
the deathCount scores and the deaths statistic.
The killer lookup and the killing blow the sculk catalyst trigger built
for the same death move into shared helpers, and KilledTrigger's matching
into advancements::killed, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKfuWtefRGy97WTU4a
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Refresh the accepted base before publication. The shader/core/backend inputs
and contribution/build rules are unchanged; rerun required checks on this head.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Entity.startRiding(entityToRide, force, sendEventAndTriggers) runs
CriteriaTriggers.START_RIDING_TRIGGER for every ServerPlayer in
entityToRide.getIndirectPassengersStream() once the rider link is set, not
only for the entity that mounted. Horizon fired the trigger at each mount
site for the acting player alone.
Route every rider-link change through Game::start_riding /
note_started_riding, which queues the vehicle and fires
trigger_started_riding for each server player aboard (directly or through a
mob rider). A guest's turn defers the pass to tick_lan, where every player is
back in place. The per-site trigger_player calls are gone; cushions,
minecarts, /ride on a mob and mobs boarding boats/carts now fire it too.
The new boarding test pins the indirect-passenger set (direct rider, rider of
a rider, and an unrelated vehicle).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Refresh the native build receipt independently of the executable and expose runtime data through a relative directory link. Remove stale companions for vanilla builds and preserve unexpected app-side data on errors.
Fixes#9521
Co-Authored-By: Codex <[email protected]>
The snapshots in .horizon-cache keyed on the executable's modification
time, so every rebuild of the game threw them away and the first start
parsed facts.json and block-behaviours.json again. They now key on a hash
of the code that resolves them (horizon-world and horizon-core sources,
their manifests, Cargo.lock, profile, target and rustflags), computed by
horizon-world's build.rs.
Fixes#9522
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rescan replacement tokens with the remaining source while preserving per-token
recursion hiding through argument substitution and pasting. Recognize calls
continued after expanded names and retain their original source locations.
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Keep the resource-backed physics assertions active when the fixture runs, and follow the existing optional-data convention in ordinary CI.
Co-Authored-By: Codex <[email protected]>
A lang file that is not strict JSON (comments, unquoted or single-quoted
strings, raw control characters...) was dropped without a word, so a
resource pack whose lang files carry comments showed nothing. Strict files
read as before; the others go through the existing Gson lenient JsonReader
port, as Language.loadFromJson is expected to read them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMLmvSqLicupMqfESW8
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
In localized versions of the launcher where the 'Join' button has a longer label (such as French 'Rejoindre', German 'Beitreten', Ukrainian 'Приєднатися'), the static right padding of 84 px caused the button to overlap the placeholder text and entered digits.
Compute right padding dynamically based on localized button text length and adapt the container width on wide screens accordingly.
Co-Authored-By: Antigravity <[email protected]>
Preserve the double operations around vanilla float drag, buoyancy and cap constants. Pin six executed Java method vectors and guard the real Game water and air tick paths.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: the composter regression from #9069 was only the fixture: play_unshared kept the fill sound out of audio.capture, so the probe lost its S line on every fill; the S line now comes from level event 1500 (data > 0 = fill_success, LevelEventHandler case 1500). composter + dispenser harnesses pass on the desktop at this head; pr_ready --quick green except the nix-shell's missing uname.
Clear the floor in loaded-cloud and nautilus fixtures before asserting that filling it changes all 256 blocks. Retain strict controls and cover fresh, partially stone, and fully stone prior states.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Corrects #8815, which made `ReducedColumns`'s levels lazy and said "neither
is the render thread, so no frame can stall on it". `Lod::apply_detail`
runs on the render thread and reads a reduction for every tile over the
chunk, so the first read of a width built it there: #8815 did not remove
that work, it moved it onto the frame.
A `lod_reduce` span inside the build (kept, it is what makes this
visible) separates the reduction from the column copying around it.
Flying over far terrain at lodDistance 256: of 14844 ms that `lod_detail`
spent over 140 slow frames, `lod_reduce` was 14140 -- 95% of it. The
`Arc::make_mut` copies are the other 5%.
The far terrain thread now builds the widths `apply_detail` reads before
it sends a chunk's columns on. Same world, same flight, limiter off:
average FPS 84.0 -> 164.0, 1% low 3.2 -> 77.6, p99 229.8 -> 9.3 ms,
`update` 6.4 -> 0.9 ms, slow frames 140 -> 2, and `lod_reduce` on the
render thread 14140 ms -> 0.
The widths are learned, not derived from the plan: `apply_detail` reads a
tile only where it is in `ready`, which the plan does not say, and taking
every level the plan holds would be the eager behaviour again.
`apply_detail` sets a bit as it reads, so the first chunk of a width
still reduces on the render thread and every chunk after it finds the
work done. Bits are never cleared, so the worst case is all four levels
-- the pre-#8815 cost, off the frame. `OnceLock` stays, so an unwarmed
width is still built on demand and the mask is an optimisation rather
than a correctness requirement.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
A recording writes its packets to `<name>.mcpr.<pid>-<nanos>.tmcpr.tmp`,
but the session metadata only reached `metaData.json` inside the archive at
`finish`, so a crash left a stream nothing could identify. The writer now
writes the metadata to `<stream>.meta.json` when recording starts and flushes
the stream and the sidecar together about once a second of recording.
On the next recording in that folder, `recover_crashed_recordings` packages
each leftover stream into a valid `.mcpr` beside it, cut to its last whole
frame (the same `frame_at` definition the reader uses), stamped with the
sidecar (or defaults when there is none) and dated from the stream when it
has no metadata. A stream whose writer may still be running is skipped (own
pid, `/proc` on Linux, or the exclusive lock a live writer holds), and a
stream with no whole frame is kept with its metadata and logged rather than
deleted.
Part of #9434 (items 2-5 follow).
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Review on #9250 (martipops's agent):
- Ctrl+F puts the box's text aside and gives it back when the search
stops; whichever way the chat closes during a search (Escape, death, a
server-opened screen), the draft is that text, never the query.
- A compact-chat repeat no longer moves a reader scrolled up: only lines
it adds do.
- Search and copy on click read the message without legacy § codes, as
the chat shows it.
- docs/pvp.md says plainly that clear water, clear lava and a fixed time
of day see further than vanilla at that moment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A focused EditBox is the screen's focus, so Space and Enter are its text (charTyped), as in vanilla. The keyboard focus the previous screen left on a widget (the Create New World tab bar, for one) no longer presses it before the key reaches the world name field. The resource-loading title shares this path, so the seed, a server's address and the rename field are fixed with it. Space still jumps in gameplay.
Fixes#9332
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Review on #9246 (martipops's agent):
- The combo counts a hurt report whose direct entity is this player
(melee and spear jabs) and ignores one that names someone else; a
report naming no attacker (Horizon's LAN hub for player victims) still
counts only as the answer to the last swing.
- The server-address widget copies the address only while it is on.
- The ping widget shows nothing while the tab entry still reads 0 (the
server resends latency every 600 ticks).
- own_latency moved below remote_player_attack, whose doc comment it had
taken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reuse the effective stack-limit helper for neighbour merging and keep the
transfer under the separate 64-item cap signed. Preserve age and pickup
delay updates for zero and negative transfers.
Add guarded Game command coverage for component limits, ordinary and
incompatible pairs, counter inheritance and count conservation, plus a
regression for overstacked transfers.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
#9069 plays `ComposterBlock.handleFill`'s sound unshared (the client that
receives event 1500 plays it, `playLocalSound`), so the audio capture no
longer saw it and the composter harness lost its `S … composter.fill*`
line on every fill (use 570, hopper_in 266, dispense 209 differing on
main 1989b1b59). The fixture now adds that line from the level event, as
it already did for the lava fizz (1501): `data > 0` is fill_success, as
in `LevelEventHandler` case 1500.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Use the live holder eligibility and preserve the float lookup, yaw and angular-friction boundaries needed by quad attachments. Keep single-spring arithmetic unchanged.
Co-Authored-By: Codex <[email protected]>
Update the current-overlay marker before a complete wire snapshot exists. Preserve frozen gameplay damage definitions while allowing the first no-pack wire load to be cached after all packs are removed.
Cover reuse through an absent resource root, route the existing pre-hub reload regression through the helper, and require the tenth prepared-resource case in the catalog parity harness.
Co-Authored-By: Codex <[email protected]>
findOrCreateDragon passes over a dying dragon (ServerLevel.getDragons
takes living ones). scanState makes a living dragon the fight's, and
removes it for a new one when there is no active exit portal. The
harness now runs with a player too: arriving in the End beside a dying
dragon, and after the fight is won.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Leave randomizable-container inventories empty when their NBT supplies a valid loot table, allowing chunk loading and command paths to retain the table. Barrels, shulker boxes (and colored variants), hoppers, dispensers and droppers now match chests and vanilla 26.3 RandomizableContainerBlockEntity behavior. Invalid or absent loot-table keys continue to preserve saved items.
Fixes#9438
Managed and reviewed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
EnderDragonFight keeps its dragon's UUID (dragon_uuid) and
setDragonKilled acts only for that dragon, so one summoned in the End
after the fight is won dies with 500 experience and no gateway. The
fight takes the End's dragon, or makes one, as findOrCreateDragon does:
at once without a dragon of its own, else 1200 ticks after it last saw it.
Closes#9464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Require completed saved-entity application and loading status without changing simulation or physical unload policy.
Co-Authored-By: Codex <[email protected]>
Rgba32Float is not a render attachment on wgpu's OpenGL backend without
EXT_color_buffer_float, so enabling a native shader pack crashed there
(#9472). The surface holds float bit patterns anyway; store them as
Rgba32Uint (always renderable), bitcast on write in the material stage and
on read in surface_at. Pack-facing API (set_surface, surface_at) is unchanged.
Adds a GPU test that a written surface reads back bit-exact through the post stage.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: verified: verify green; pr_ready --quick 2677 Rust tests + clippy pass (only Python failure is test_install_sh_signature: no uname on NixOS PATH); 2-client proof on a dedicated server with the SMP mod: ops' clients complete /tempban and /mutelist, non-ops don't; /mute with a duration blocks chat, /msg, /tell, /w, /me but not other commands; /tempbanlist lists timed bans; a 5 s ban ends by itself. Risk to watch: the extras' names are dropped from the mods' level map so a mod registering /mute still never sees it (the game intercepts first, as before).
Freeze only the gameplay damage entries until the first hub needs complete synchronized data. Preserve no-pack preloads and prevent late helpers or pre-hub reloads from changing the session damage registry.
Co-Authored-By: Codex <[email protected]>
`Lod::receive` drained the whole results channel every frame. A detail
request asks for 512 chunks and cached ones answer at once, and applying
one copies the columns of every tile over it (`Arc::make_mut` on a
snapshot a mesh job shares) and reduces the chunk to that tile's cell
width. A burst in one frame cost 273 ms of it on the render thread.
`horizon-ctl stats` over far terrain at lodDistance 256 put the cost in
`update` (avg 29.9 ms, 540.5 on slow frames) and not in rendering (2.6
ms) or on the GPU (avg 9.1 ms), with a median frame of 6 ms;
`HORIZON_SPIKES` named the path: update_world / lod_update / lod_results
/ lod_detail, 104 slow frames in 20 seconds.
A frame now spends at most DETAIL_BUDGET on summaries and leaves the
rest in `detail_backlog`. Same binary, same flight, limiter off:
average FPS 94.4 -> 133.6, 1% low 3.7 -> 42.1, p99 156.2 -> 20.1 ms,
worst frame 739.7 -> 34.2 ms. p50 is 0.6 ms worse, the budget's work
spread out rather than dumped into one frame. The fill did not slow:
settling to fully refined took 19 polls against 45.
`request_detail` returns early while any summary waits, which is not
optional: without it a chunk whose summary is still waiting has its
mask clear, so it is asked for again and the cache answers at once. The
backlog then grew by a request every 250 ms and never drained -- 0.5
FPS, `update` averaging 9.7 s a frame.
`refreshed_since` moves from arrival to application: a refresh over the
chunk while its summary waited still drops it.
Corrects #8815, which said the lazy reduction could not stall a frame
because neither the far terrain nor the far meshing thread is the render
thread. `apply_detail` runs on the render thread and reads a reduction,
so the first read of a width builds it there. The budget bounds it;
warming the plan's widths on the worker thread is a separate change.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
EntityDataAccessor.setData loads the merged compound into any entity but a
player; Horizon answered "Nothing changed" for carts and boats. Minecart
gets a load() that reads the saved fields over a live cart (from_nbt now
uses it), apply_entity_nbt gains Cart and Boat arms that keep the merged
compound, and guests get commands.data.entity.invalid like the player.
The test compound uses the 26.3 DisplayState form (a block name); a second
test checks that merging only a name keeps fuel, push, fuse and Enabled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMK3XzCYMyeKEdamets
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Prefer fluid blocks when resolving material IDs for their still and flow sprites, preventing a cauldron material from being assigned to fluid surfaces. Add regression coverage for shared fluid sprites and existing block voting.
Co-Authored-By: Codex <[email protected]>
Keep main suffocation registration and the world damage catalog together when loading world tables. Preserve all intervening upstream changes without rewriting existing PR history.
Co-Authored-By: Codex <[email protected]>
hidapi's bundled C can crash inside hid_write on Windows once the pad has
gone (Bluetooth sleep or unplug), and flush() only handled a returned
error, not a crash inside the call. Treat the output as lossy:
- probe the pad with a non-blocking read before every write, so a gone
device is noticed before hid_write is entered;
- drop the handle on any hidapi failure (read or write), never writing
through it again;
- schedule a rescan in lost() so the writer goes back through the normal
scan/reopen path instead of keeping a stale handle;
- watch_animation() now drops the pad on a failed read too.
The handle is a small trait so the writer's state machine is unit-tested
without hardware. There is no vanilla reference: this is Horizon-only
hardware output.
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Preserve upstream reload-listener and native API exports beside the world canvas type. No rebase or force-push.
Co-authored-by: Codex <[email protected]>
The regression behind #9088 was #8245: a stop that doesn't come from the
supervisor's stdin or the control API left the supervisor in its stopped
phase, so `horizon-server --dir … run` outlived the management
`server/stop` and the harness's `wait(timeout=180)` raised TimeoutExpired.
#9077 fixed that (`Shared::shut_down`), on the base of this branch, and the
harness passes here again.
What the harness still left open, and what this fixes:
- It could not run on Windows at all. It wrote the absolute keystore path
into vanilla's server.properties with single backslashes, which Java's
properties parser reads as escapes ("C:UserscocoAppData…"), so vanilla
stopped before the first request. Values are now escaped as vanilla writes
them, and the harness runs here.
- A server that stays up after `server/stop` was reported as a bare
TimeoutExpired. `wait_for_exit` waits for the process to end, which is the
condition the issue was about (vanilla's `ServerStateService.stop` submits
`MinecraftServer.halt(false)`, so `runServer` returns and `main` ends the
process), and its failure names the side and the last lines it logged.
The seconds each server took are kept in report.json: vanilla 3.4 s and
Horizon 0.5 s here, against the 180 s budget.
Fixes#9088
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
Lend the vehicle navigation to passenger goals using the vehicle's path
context, while retaining each mob's private navigation tick. Apply the
controller policy and ravager goal flags without requiring a combat target.
Add a guarded real Game/Java regression for patrol and stroll behavior,
including NoAI riders, non-controlling slime riders and a NoAI vehicle.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
An item predicate's `components` and `predicates` are keyed by registry
ids (DataComponentType and DataComponentPredicate.Type, read through the
registries' name codecs), so "potion_contents" is
"minecraft:potion_contents". `fn item` looked the keys up only in their
full form: a bare key was skipped and let every stack pass. It now finds
them through `full()`, and compares the potion_contents value as a
registry id too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Chicken.aiStep rolls the chicken_lay gift table first and plays
entity.chicken.egg and posts GameEvent.ENTITY_PLACE only when
dropFromGiftLootTable dropped something; the egg timer resets either way.
Horizon played the sound before the roll and posted no game event, so a
sculk sensor never heard a laid egg.
A new MobEvent::LayEgg carries the pitch (drawn in the same place as
before); its handler rolls the loot and gates the sound and the game event
on a drop. drop_gift_loot now returns whether it dropped; the other
GiftLoot callers (turtle, panda, sniffer) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A loot `location_check` whose `biomes` named a tag (`#minecraft:is_jungle`)
never matched: the loot data did not load biome tags. `LocationPredicate.biomes`
is a HolderSet, so the arm now reads it with `LootData::holder_set` (a
`#tag`, an id or a list of ids), and the loot data loads the
`worldgen/biome` tags with the world's packs over the game's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Zombie horses are in the burn_in_daylight tag; Mob.isSunBurnTick rolls
every tick, so a horse could catch fire in the two-tick window and the
vanilla reference read 24 Health instead of 25 (#9089).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/tempban, /mute, /tempmute, /mutelist and /unmute worked on dedicated servers but were left out of the
command tree, so operators' clients showed them as unknown commands with no completions, and the
official server's operators believed only permanent bans existed. They now reach the trees of
operators of level 3 (as a native mod's commands do), where the game runs them: the mute commands
when the server filters chat, the ban ones on a dedicated server.
/mute <player> [duration] [reason] takes a duration after the name (/tempmute stays), and
/tempbanlist lists the bans that end by themselves, soonest first, with who banned, the time left,
the end and the reason.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`adv::entity` had no `minecraft:nbt` arm, so every entity predicate with
`nbt` fell to `_ => false`: a villager's profession, a mob's Variant, a
CustomName, the player's Inventory or SelectedItem never matched.
EntityNbtPredicate.matches: the entity's compound from
NbtPredicate.getEntityTagToCompare (saveWithoutId, plus SelectedItem for a
player) tested with NbtUtils.compareNbt(..., true), which
`nbt_path::compare_nbt` already is.
- `EntityView` gets the compound (`nbt`); `entity` parses the SNBT pattern
and compares; no compound or an unparsable pattern fails.
- `Tree::index_nbt` notes the triggers whose criteria mention `nbt` (or a
`minecraft:reference`), like `index_looking_at`, so the compound is
built only when a criterion may ask.
- `mob_entity` builds the mob's compound (`mob_to_nbt`, riders under
`Passengers`); `player_entity_for` the player's, through `player_nbt`,
the player arm of `/data get entity` moved out of `entity_nbt_in_level`.
- A bred child's view, a copy of its parent's, drops the parent's data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
🤖 Agent: a dragon saved or summoned with Silent:1b no longer plays the global 1028 death roar at tick 1 of its death; Silent is read from the compound Horizon keeps for unmodelled fields, and the new dragon_death_roar harness compares the level events against vanilla's. The branch passed the cg-39 union run on playhorizon (horizon-client 887 tests); main merged to clear an unrelated lan::conn flake.
🤖 Agent: follow-up to martipops's #9228 review: kept mod block-entity text is keyed on the font atlas epoch, so a resource reload rebuilds it instead of drawing stale glyphs (KeptKey test covers it); docs say what spin, draw_text and native_block_face_sprite really do. fmt, clippy and 7 tests on Linux; the larger points are #9431.
🤖 Agent: tanks take mods' own fluids; before the helpers get numbers, native_fluid_flow now names its dimension and answers none for an unloaded chunk, and a cauldron reports no room for a mod's fluid so native_move_fluids answers none as documented. Stamp test, clippy and 17 fluid tests pass on Linux; a per-tank filter is #9439.
🤖 Agent: Raider.IS_CELEBRATING (index 16, BOOLEAN) now reaches clients, and the new lan test pins it through mob_meta itself: with the mob_raider_meta call removed it fails, so the server half of #8848 can't silently drop out. Main merged (the #9271 part is gone from the diff); 4 celebration tests and clippy pass on Linux.
🤖 Agent: the .mcpr reader no longer trusts the file: entries are capped (1 MiB metadata, 1 GiB stream), one frame parser serves the index and playback so a huge or zero length ends the stream cleanly, and seeking starts at the first packet of a millisecond. finish renames a synced temp over the destination with Zip64 and keeps the packets on failure. 9 studio tests on Linux, verify green on 56fd928f5; the rest is in #9434.
native_move_fluids promised none when the target has no room for that
fluid, but an empty cauldron reported a bucket of room for any fluid, so
a move drained the source and put it all back, answering 0 and marking
the chunk each time. native_fluid_flow is new here: it now names its
dimension, as native_fluid_tanks does, reads a server's parked levels,
and answers none for an unloaded chunk instead of a zero that looked like
still water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
jmath::sin and jmath::cos used the platform's libm on x86-64, which on
Linux (glibc) rounds like HotSpot's x86-64 intrinsics for all but about
0.2% of arguments. Microsoft's rounds differently for about 3%, so on
Windows item drops, mob goals and worldgen features came out a last bit
off vanilla's, and drops_match_vanillas_motion_bit_for_bit failed there.
On Windows they are now correctly rounded (jmath::exact, double-double
arithmetic with a fast path), which HotSpot's are for all but about 0.1%.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clicking the instance icon on Home or selecting 'Edit instance' in the
folder group more menu now opens the instance dialog. In the dialog,
clicking the large preview icon opens the image picker, and existing
pictures remain selectable in the palette.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
🤖 Agent: merging. Ignoring only the hello's send error is enough: data the server sent before closing stays readable after the reset, so the Disconnect is still read and the test still checks the refusal. Proof: on a union with main and #9382 the test passed five runs in a row; fmt, clippy -D warnings and the client-lib tests pass.
🤖 Agent: merging. The order matches SoundEngine.tickInGame: stop when canPlaySound is false (the entity went silent), then tick, where EntityBoundSoundInstance.tick stops on isRemoved or else copies the entity's position as floats; the packet's seed now picks the variant, as handleSoundEntityEvent does. The per-tick cost is one entity lookup per playing bound sound. Proof: union with main and #9385: fmt, clippy -D warnings, Python tests, 2634 client and client-lib tests, the render audio (20) and sound_engine (10) tests, and the entity_bound_sound and sound_engine harnesses against vanilla pass.
Review of #9176:
- A split part whose program exists but doesn't load, or whose pipeline
can't be built, no longer leaves the other part drawing alone: neither
draws, and the game draws all terrain, as when an unsplit pack's terrain
program fails. Iris 1.11.7 turns the whole pack off for our probe whose
gbuffers_terrain_cutout doesn't compile.
- A part with no program in its chain still draws nothing while the other
draws alone, now with a note: Iris 1.11.7 left the other part undrawn
with our probes having only gbuffers_terrain_solid, or only
gbuffers_terrain_cutout.
- Loader::has honours program.<name>.enabled, as load does: Iris drew all
terrain with gbuffers_terrain for our probe that turns its
gbuffers_terrain_solid off with program.gbuffers_terrain_solid.enabled=false.
- Post::every_program lists terrain_cutout, so buffer_minimums counts the
storage blocks the cutout program binds.
Part of #9137
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The reader caps metaData.json at 1 MiB and recording.tmcpr at 1 GiB,
so a hostile archive can't exhaust memory, and never allocates from a
length field: indexing and next_packet share one frame parser, stop at
the first frame that isn't whole, and agree on the packet count.
- Seeking lands on the first packet of a millisecond (partition_point);
a timestamp going backwards is indexed at the time before it.
- Fields ReplayMod can leave out read as defaults; protocol is optional;
a file whose fileFormat isn't MCPR is refused.
- finish writes the archive to a sibling file, syncs and renames it over
the destination, with Zip64 for the stream; on failure the stream file
is kept and the error names it. A dropped writer keeps its packets, a
failed write stops further frames, and the stream name can't collide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: the SNBT depth cap is now 256 rule frames (about 63 nested compounds or 13 levels of nested container items), measured on playhorizon: 384 already overflows a 2 MiB debug thread stack, the size the server thread runs on. New test deep_real_values_still_parse keeps 50 compounds and 10 container levels parsing; 42 horizon-command tests pass, verify green on 54142077f.
From review: a native mod's chat line was stamped with the client's
ticks while chat follows the world's under the switch, so it could
vanish at once. The switch now logs a warning when it is on, the
docs say to wait a second after a step (the server's time arrives
once a second), and reseed's doc comment no longer sits between
random_glyph and its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: Ludovic allowed Realms with the player's own account (2026-11-04). At 08874a1622 the review's five points are fixed: the join runs on a worker, Leave asks first, the /join/pc address is checked as host:port, requests send only vanilla's sid cookie (a local-server test shows no Authorization header and the token missing from errors), and the world-pack question comes first. Proof: desktop fmt, clippy -D warnings, 1,700 client-lib tests; verify is green on this head.
🤖 Agent: launcher side of the free Founder Set claim where Paddle doesn't sell; the service part is live (horizon-cosmetics#11). Risk: the button only shows when the service sends claim.one_click, so older services and other countries see no change.
KeptKey gains Font::epoch, as a sign's kept text has: a glyph cache reset
(the dynamic region filling, Force Unicode or Japanese Glyphs toggled) moves
glyphs in the atlas, and kept mod text drew the cells' new contents until
its block entity changed. The docs now say native_draw_text draws plain
default-font spans, that spin's angle is absolute in game time (a new speed
needs a new phase), and that native_block_face_sprite returns vanilla names
without their namespace.
From martipops's review of #9228.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Like Lunar Client's: Horizon's mark (two white bars over a violet line, horizon_cosmetics::mark)
before the names of other Horizon players, in the name tag (both views) and the player list. Who
plays Horizon comes from the cosmetics service in the same cached /v1/worn answers ("horizon": a
list of the batch's UUIDs beside "players", which released games ignore): no new request, nothing
on the wire to Minecraft servers.
- Extras > Cosmetics: "Horizon logos" and "Show me as Horizon", both on by default. On, the game
signs in (Mojang join, as for My cosmetics) and sends POST /v1/presence at start and every hour;
off, it sends listed=false and the service forgets the player at once.
- A name tag style's badge moves after the name (the mark goes before it); the launcher's Wardrobe
draws the name tag the same way.
- Name tag mark and style line in the HUD batch's RGBA: the style line drew #a98fff as pink.
- The player list keeps a mark column once anyone listed has it, so names stay aligned.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world's surface rule holds the ore veins, whose densities belong to
each chunk's volume, so every chunk fell back to compiling the rule. The
shared rule now keeps the densities compile binds, in its order, and each
chunk's worker binds them to its own volume the same way: the indices,
and the samples, are compile's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every proto chunk compiled the world's surface rule twice (build_surface
and apply_carving_mask): a deep copy of the rule tree, every biome
condition's names normalised into a new set, its biome ids looked up. A
generator worker now compiles it once, without the chunk's possible
biomes, and keeps it with the noises it bound; each chunk's worker takes
it and folds the biome conditions its possible biomes decide, as compile
folds them, so the tree, its bands and every block are the same. Rules
that bind densities, workers that already hold rules, and possible biomes
the surface doesn't number compile as before.
Fixes#9218
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An open smithing screen cycled its empty-slot icons on the client's
ticks while frozen. And the docs now say to wait a moment after a
/tick step before a capture, as after a turn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#9412. Every launcher process deleted and rewrote the one update folder, and the installer
only checked that the payload existed, so a second launcher could leave a partial file to copy over
the launcher.
- Each stage is a `<pid>-<nonce>` folder. Downloads get their name only after the hash check. Each
process removes its own; stages whose process is gone are cleaned up after an hour, counted from
the hand-over, because an installer may still be using them.
- The installer checks the archive against the signed SHA-256 again and places the launcher from
those bytes. The hash travels in an environment variable: the installer is whatever file is in
place, so the command line stays readable across versions. A window without one can't place.
- Nothing happens when the launcher in place is already that version or newer.
- On Windows a launcher running from the temp folder, however it is spelled, can't replace itself.
- Update no longer stops hosted servers. The new window doesn't stop them when it closes either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
A native pack's `[displacement]` grid takes its quads from the walk's
`visible`, which in a GPU frame lists only the sections the CPU still
draws, so near relief faces went missing: such frames now take the CPU's
path, and docs/graphics.md lists it with the other exceptions.
The GPU Culling switch reads Off wherever it isn't offered, as Translucent
Batching and Smooth Gradients do, so the box, the narrator and the widget
tree no longer report a saved On; the UI test checks the switch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A machine's fake player restored its swapped fields but never ran the close
side of the use. A trader stayed in its session, a vehicle kept its menu's
stand-in, and a shulker box's opener count stayed up. It now runs the real
close paths while the fake inventory is in place, and swaps station,
craft_table, ender_open, hook, experience and the respawn point too.
Closing keeps the use as a player's: the click counts, and the same events
and sounds fire. Refusing to open would need a gate at every open site.
Fixes#9409
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
`shade_sky` records a dampening run's skylight a span per light level, so
a sea 15 blocks deep or more becomes 16 spans to carry a gradient down
its own side. Over real seed-1234 terrain that is 64% to 78% of every
span a far column stores: 18.7, 12.3, 12.0 and 12.6 spans a column at
tile levels 0, 2, 4 and 6, where the runs themselves are 4.1 to 4.5.
A previewed column now spends one span on the run and drains the same
light out of the bottom of it, so every span under the run is lit exactly
as before -- which is what the per-level walk was for ("lit seabeds and
the ground under canopies"). It reaches no face that is drawn: the spans
inside a run have neither a top nor a bottom face (`hides` matches on
state, not light), and a run's side is exposed at its top, where its
light is brightest, with `add_side` raising anything above the
neighbour's top to the neighbour's own sky.
Chunk columns keep the per-level walk. They are what `Column::reduced`
merges, and a reduction takes the brightest light of each slice, so one
bright span for a whole sea would light its bed as brightly as its
surface. A previewed column already stands for a whole far cell and is
never reduced against its neighbours.
Measured on 4448 far tiles at lodDistance 256, Full far detail, two runs
each with the heap census: `Far sampling` 302.9 and 309.6 MiB against
264.5 and 268.2, and the resident set 1386 MiB against 1252 and 1242.
Four pinned views are identical across the change to within the
animation noise floor, while two runs of the same build differ by 7% of
their pixels -- far-tile resolution is not reproducible between
launches, so the controls are in the pull request.
`shade_runs` takes the block-state test as a predicate so the shading
can be tested without prepared resources.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
🤖 Agent: merged main through #9228's merge (mod lines and doc sections both kept). The scene's quads use the GUI's isolated depth (HudBatch::depth_since) and stay in the HUD batch, so nothing changes until a mod calls native_draw_scene. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
CI's launcher job and pr_ready ran only --lib, so the tests in main.rs's
modules first ran in launcher.yml's cargo test at release. Add --bins to
both. The clippy failure at forge_install_tests.rs:4 is already fixed on
main by 812512980, so it needs no change here.
Fixes#9414
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
take_loot_table dropped a held roll without clearing the table from the
chest's source compound. The emptied chest encodes the same as its decoded
source, so the save overlay wrote the source back with its table and the next
load rolled again. It now goes through consume_container_loot, as opening does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
🤖 Agent: the sleep is only as safe as the wake paths, and every mutable path into BlockEntities' map (get_mut, insert, take/put, iter_mut, retain, load_chunk, state_changed) clears it, pinned by a_sleeping_block_entity_wakes_on_any_change. The cost on the tick loop is one FxHashSet lookup per ticker. Hoppers stay awake, so the big win is still ahead. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
🤖 Agent: mod tickets live in Game.mod_chunk_tickets keyed by dimension while streaming.mod_forced travels with each level's Streaming, so a ticket survives the level swap; forced_chunks() dedups against /forceload so a chunk held both ways is ranked once. Tickets aren't saved, as the doc says: a mod that forgets to re-hold in native_level_load drops its chunks on restart. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, client 887 + client-lib 1682 + render 1355 tests green (the 4 clear_budget HDR failures also fail on bare main there).
Fixes the five points from the review of #7725:
- Play runs GetServerDetailsTask on a worker (with vanilla's Retry-After loop and
an abort on Cancel/Escape) while the screen shows "Connecting to the Realm...";
the title polls it once a frame and connects when the address is back. Every
other Realms request (list, invites, join code, open/close, leave) is off the
UI thread too.
- Leave Realm opens RealmsPopups.infoPopupScreen with
mco.configure.world.leave.question.line1 (Continue/Cancel), as leaveClicked
does; only Continue sends uninviteMyselfFrom. Our own Realm is never offered.
- The /join/pc address must be host:port (host name, IPv4 or [IPv6], numeric
port, no scheme, path or credentials) before it becomes Action::Connect.
- Requests carry exactly what RealmsClient.execute adds: the sid/user/version
cookies and Client-Ref, X-NetworkProtocolVersion, Is-Prerelease; no
Authorization header, the JVM's Java/<version> user agent, no redirects.
Errors are typed (RealmsError) and never hold a header or the token.
- A world pack in the join answer asks
mco.configure.world.resourcepack.question first; Continue connects with the
pack allowed and pushed under GetServerDetailsTask.generatePackId.
Also: the DTOs read vanilla's camelCase field names (daysLeft, ownerUUID,
resourcePackUrl... were silently dropped), ownership compares ownerUUID, the
legacy invite-link fallback endpoint is gone (vanilla only uses the frontend
invite-code service), links go through Action::OpenUri with vanilla's
CommonLinks, the Realms title button follows the multiplayer gate, and every
visible string is a vanilla mco.* key (one Horizon key for "not signed in").
No session (offline, expired token) means no request at all.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
128 frames let only 31 nested compounds, 17 levels of a component's
`extra` list and 6 containers in containers parse. 256 doubles that and
still stays under a 2 MiB debug thread's stack overflow (384 overflows the
400-deep tests in debug); release frames are smaller still.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: `weather_check` now compares each present field with `Level.isRaining` (rain level > 0.2) and `isThundering` (rain × thunder > 0.9, dimension-aware), as `WeatherCheck.test` does; an omitted field doesn't constrain. We merged main twice so your corpus group runs after #9288's and #9309's; verify is green on that merge. Nothing else found.
🤖 Agent: on Linux the stream now asks ALSA for a fixed 1024-frame buffer (about 23 ms at 44.1 kHz) instead of cpal's ~100 ms default. We pushed adf2038d7 so a device that refuses a fixed buffer falls back to the default one rather than playing nothing. Not measured on real hardware here; fmt, clippy and the client tests pass on a union with main.
🤖 Agent: the launcher now offers "A new version is available" with Update, and copies that can't replace themselves say so instead of failing silently; Linux end to end on the ThinkPad (Update to 0.2.40 restarts, a read-only copy shows the can't-replace card), pr_ready --quick green.
🤖 Agent: pushed a fix: container_accepts.json listed native_hooks.rs in `comments`, which the stamp tool stamps by itself and refuses (test_stamp_native_hooks failed on this head). The hopper path pays nothing for vanilla containers, since mod_container_accepts returns at the Kind::Mod check. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: use_as_player swaps about 30 Game fields by hand and restores them. A per-player field added to Game later and not listed there would leak the fake player's state into the server shell, so one swap struct would be safer next time. Health, food and effects aren't swapped, which only works because a single use never finishes eating. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: merged main (the mod_api use list now also has NativeReloadListener). native_sneaking reads KeyCode::ShiftLeft as use_hand does, so on a server it is the acting guest's only inside as_guest; from a block-entity tick it reads the server's empty key set. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green, stamp tests OK.
🤖 Agent: merged main (its minecart_hook_targets and pull_hooked tests sit beside the breeze ones, and the test module imports Hook once). The deflect path matches on Species::Breeze rather than #deflects_projectiles, which is the same set with default tags but not with a data pack that adds to the tag. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: checked max_air_supply against the 26.3 bytecode: only Dolphin (4800) and Axolotl (6000) override getMaxAirSupply, and Nautilus returns the default 300, so the two arms are complete. #9220 now carries this branch and shares its mob fixture. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
🤖 Agent: shulker_support costs one block-tag lookup per sturdy/rigid/center query before the cached path, the same shape as moving_block_supports there. Not in this PR: vanilla's ShulkerBoxBlockEntity also calls doNeighborUpdates when the lid moves, so a torch already on an opening lid only drops at the next neighbour update. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green, the new level tests included.
🤖 Agent: checked every new trigger id against the trigger it guards; damage_view's two KilledTrigger ids are right, since its only callers (creatures.rs last_blow, dragon.rs) feed trigger_killed. Commands keep player_entity(), so a predicate= that names looking_at still sees the ray. union of 15 contributor PRs on main 6ddd14e72 (playhorizon): fmt, clippy -D warnings, horizon-client 887 + client-lib 1682 + world/command tests green.
KnownPacksManager.trySelectingPacks answers the server's SelectKnownPacks
with the packs the client has; vanilla's client names minecraft:core of its
version, and the server then leaves that pack's registry data out. Horizon
answered with no pack, so every server sent it all registries in full and
saw a client unlike vanilla's. Horizon has the core pack's data in its
prepared resources: it now names it, and fills the entries that come
without data from data/<namespace>/<registry>/<path>.json, as
RegistryDataCollector does from the client's own pack.
tools/vanilla_wire_e2e.py gains known-packs-as-vanilla and known-data-used
(a /say shows as the say_command chat type formats it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: merging. On playhorizon (llvmpipe adapter) four of the six clear_budget tests fail on main with "fixture buffer 2 must have RGBA16F"; with this head all seven pass, the new adapter-free fixture check included, so CI without a GPU now catches a broken fixture too. Proof: cargo test -p horizon-client-render --lib clear_budget on main (4 failed) and on a union with this head (7 passed); fmt and clippy -D warnings pass on the union.
🤖 Agent: merging. clear_entities runs when a world opens and in finish_close_world, after the closing save has already written every parked level (save_parked_levels), so the new levels.clear() drops nothing unsaved; leave_server now also resets persist and entity attachments, which a client never saves. Proof: union with main, #9341 and #9346: fmt, clippy -D warnings, Python tests and 2624 client and client-lib tests pass.
🤖 Agent: merging. The 26.3 bytecode of EnderDragon.aiStep checks isNoAi right after Mth.wrapDegrees(getYRot()), sets flapTime to 0.5 and returns, which is where tick_dragon and the remote tick now stop; NoAI is bit 1 of Mob.DATA_MOB_FLAGS_ID (15), the index the host sends. Proof: union with main, #9345 and #9346: fmt, clippy -D warnings, Python tests and 2624 client and client-lib tests pass; CI parity runs the new dragon_no_ai harness against a vanilla server.
ClientLevel.tick fades the sky flash on every tick, and moves the world
border's size change only while the level runs normally. The client did
it the other way round: after a /tick freeze, a flash that had just
struck lit the sky for as long as the level stayed frozen, and a border
change went on shrinking or growing on this side while the server's
held.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dev switch for before/after pixel diffs (#8750). The world starts frozen
before its first tick, and what the frame shows of time follows the
world's ticks: animated textures, the glint and world border scroll, the
darkness pulse, the dragon's crystal beam, the player model's idle swing,
the HUD (hearts, chat, titles, the locator bar, the tab list), a shader
pack's clock and randoms (smoothed uniforms at their targets, custom
uniforms' random, cave mood, obfuscated text, a spinning clock or compass)
and the camera's partial tick. While frozen, the client's own entity
effects and countdowns wait for the world too, as a frozen level's
entities don't tick. `/tick step N` moves the scene on by whole ticks.
Off by default; nothing changes without it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemEntity's bobOffs and EndCrystal's time come from the entity's own
random in vanilla. The client drew them from the level's random, shared
with splashes, eating and other client effects, so the same dropped item
could bob and spin from a different phase from run to run. Each now draws
from a random seeded from its id and position, as a client mob's is
(Mob::new, now mobs::entity_random).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Query the active world table from crafters and retain first-match path/namespace priority after recipe replacement and filtering.
Co-Authored-By: Codex <[email protected]>
Keep the internal damage cooldown and previous-hit amount when a data
command reconstructs a mob, matching an in-place vanilla entity load.
Continue loading explicitly edited Health and HurtTime values from NBT.
Add guarded command regression cases for equal hits, stronger hits,
unchanged writes, fresh actors and measured cooldown expiry.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Players in countries Paddle doesn't serve (Russia, Belarus...) can't use the
shop's 0 CHF checkout. The cosmetics service now says so per request
(`claim.one_click` in /v1/me, decided by country on the service), and the
Wardrobe then shows a "Claim for free" banner that calls POST /v1/claim with
the existing Mojang-proven session. Everyone else keeps the shop link.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The control socket's chat line went straight to the server on a connected world, skipping the client mod commands, the Chat option's restrictions and the recent list that typed chat goes through. It now uses handle_chat_input like the chat screen, so /modcmd opens the mod's screen on a server and the rest of the line is sent only when no mod owns it.
The control socket's command line also tries a client mod's command before the server console: the console cannot see a mod that is compiled into the client, so hudplus or another client mod's command failed there.
AI agent: oxi-does-dev via OpenCode, GLM 5.3.
One click on the PvP & HUD page (and on Horizon Extras) turns on the custom
crosshair, the armour and effects widgets, vanilla's toggle sprint and toggle
sneak, lower fire, and the official FPS counter and keystrokes mods when they
are installed. It keeps each option's own settings.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two options on the PvP & HUD page, off by default: a time of day (morning,
noon, sunset, midnight) and a weather (clear, rain, thunder) that only this
window draws. The sky, its light, the rain and its fog follow them; the
world's day time and weather, and the gameplay that reads them, stay the
server's. Nothing is sent to the server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Visual tweaks on the PvP & HUD page, each off by default:
- Lower fire moves the first-person fire overlay down (0-80% of its height).
- Shield size (50-100%) and totem animation size (25-100%).
- Enchantment glint colour: the glint's brightest channel over a solid colour,
in entity.wgsl and hud.wgsl, carried in free uniform lanes.
- Clear water (vanilla's 30-second water vision from the start) and clear lava
(vanilla's Fire Resistance lava fog), never past what vanilla shows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An option on the PvP & HUD page, off by default: enchantment names show their level
as a number ("Sharpness 5", and past vanilla's numerals, "Sharpness 255") wherever
the client names one: tooltips and the enchanting table's hint. A one-level
enchantment still has none. Enchantment.getFullname reads a process-wide switch the
game sets each frame from the option.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three HUD widgets on the PvP & HUD page, each off by default, in a corner of its own
beside the minimap and mods' boxes (docs/pvp.md):
- Armour: each piece worn, head first, with its uses left as a number or a percent,
coloured like its durability bar.
- Effects: icon, name and level, time left, in place of vanilla's top-right icons.
- Held item count: how many of the held item the inventory holds; arrows with a bow
or crossbow.
pvp_hud::Widget keeps each widget's lines and rebuilds them only when what it shows
changes, so a frame that changes nothing allocates nothing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two more groups on the PvP & HUD page, off by default (docs/pvp.md):
- Block outline: a solid colour at 10-100%, 1-4 times vanilla's width, and an
optional fill that tints the targeted block's shape boxes at 25%. Its first
settings are vanilla's 0x66000000 stroke. With high contrast on, the colour
replaces the bright stroke.
- Hit colour: the hurt overlay's colour and how much of it covers the entity.
Its first settings give OverlayTexture's 0xB2FF0000 texel. The texel rides in
colored_count.y of the frame uniforms, so it needs no pipeline rebuild.
FrameInputs gets one pvp::Frame field the later PvP options extend.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four chat options on the PvP & HUD page, off by default:
- Timestamps: the local time before each message.
- Compact chat: a message repeated in a row shows once with its count (x3).
- Copy on click: a click on a message with no click action copies its text.
- Chat search: Ctrl+F in the open chat filters the messages by the box's text.
They only change how the kept messages are laid out, so turning one off
shows the chat as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four more HUD widgets on the PvP & HUD page, off by default (docs/pvp.md):
- Ping: the latency the server's player list gives this player. Multiplayer only.
- Coordinates: block position and facing (Direction.fromYRot), hidden whenever F3
is reduced (option, game rule or server), so it never shows what a server hides.
- Server address: as net::shown_address shows it (invite codes hidden).
- Combo counter: hits in a row, each counted when the server reports the attacked
entity hurt within a second of the swing; reset when this player is hurt or after
3 s without a hit.
Each widget rebuilds its text only when its value changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With transfers off the server turns a transfer away at the handshake and
closes (ServerHandshakePacketListenerImpl.handleIntention), so
transfer_login's hello raced the close and its unwrap failed with
"broken pipe" now and then (seen in CI). The hello's send error is now
ignored; the disconnect is still read. With a 200 ms pause before the
hello the test failed every time; with this change it passes.
Fixes#9376
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game hits through hurt_part_from, so the no-attacker wrappers were dead code without cfg(test).
Co-authored-by: Cursor <[email protected]>
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Keep the packet seed and category, update the bound position each tick,
and stop the old object when an entity is removed or its id is reused.
Compare the sound-engine lifecycle against the owned vanilla client.
Co-Authored-By: Codex <[email protected]>
Custom uniforms evaluated every argument of if(), so a smooth() in a
branch not taken still moved toward its value each frame. Our probe pack
in Iris 1.11.7 (if(fmod(frameTimeCounter, 4) < 2, smooth(sq, 10), 0.5)
over a 16 s square wave) reads values that fit "frozen while the branch
isn't taken" within 0.008, and miss "updated every frame" by up to
0.14. if() now evaluates only the conditions up to the first true one
and that branch.
The same probes also time smooth() over a square wave from if(),
eyeBrightness, worldTime and rainStrength, fades 0.5 to 100 and the
default: their Iris values fit a half-life of 0.1 s per fade unit, as
custom.rs already has (a fit to our probes, not Iris's code). A test
pins worldTime steps and the default fade.
Fixes#9371
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 0.2 gate is already checked by whether the cart is shoved.
Co-authored-by: Cursor <[email protected]>
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Reuse map post-processing before dispensing and register the derived saved map. Cover actual map use, scheduled crafting, and backed/missing/max-scale controls.
Co-Authored-By: Codex <[email protected]>
EnderDragon.knockback skips that push while the dragon is sitting, including a hit that makes it take off.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
The rider's client simulates the mount, and ItemBasedSteering.onSynced starts that timer from the synced int. Without it the boost factor stays 1.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A firework used while fall-flying has to drop every lead on the player and play the snap, or the mob stays tied and nobody hears it.
Co-authored-by: Cursor <[email protected]>
Co-Authored-By: Grok <grok-4.7> <[email protected]>
ArmorStand.pushEntities moves a plain minecart inside the stand when it is within 0.2, and the stand tick never did, so the cart stayed put.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
ItemFrame.getFrameItemStackWithData copies the entity name onto the frame item before dropItem spawns it. Glow item frames use that same stack.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A frog in water was using LivingEntity.travelInFluid, so its speed and drag did not match vanilla.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
A skeleton skull, zombie head, creeper head or piglin head should halve how far that mob notices the player. getVisibilityPercent multiplies by the item's component, and the targeting paths only scaled sneaking and invisibility.
Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
Reuse the existing mob team lookup and team formatter when command feedback
names a mob. Preserve its custom name or translated type and the player paths.
Extend the real command corpus with guarded zero-tick join, custom-name,
prefix/suffix clearing, color-only and leave controls against Java 26.3.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
When disconnecting or leaving a server/single player world, leave_server() in remote.rs cleared only top-level mob arrays, leaving GPU skin models, skin frame vertex queues, parked dimension levels, remote objects, and entity draw spans allocated. These cached models could persist into the title screen renderer, causing Nether mobs such as Wither Skeletons to render and wander across the main menu panorama.
- Delegate leave_server() entity cleanup to clear_entities().
- Extend clear_entities() to also clear remote objects, projectiles, parked levels, render vertex buffers, draw spans, entity motion, and reset skin models and frame draws.
- Implement Levels::clear().
- Expose SkinModels::restart() and reset skin draw frames on entity clear.
Fixes#9074.
Managed by Axxio.
Co-Authored-By: Antigravity <[email protected]>
`tools/test_bed_explosion_parity.py` compared one victim: Alice's health after
the blast and the push her client is sent. What an explosion does to anything
else went unobserved, and `game/explosion.rs` is the one place in the game
where the entity side of `ServerExplosion` lives.
The scene now puts four things that are not the player in front of the bed
head, where no placement puts Alice or the obsidian wall, and gives each of
them `NoGravity` so the only motion any of them can get is the blast's own:
- `stand`: an armour stand two blocks from the head,
- `invisible`: an armour stand with `Invisible:1b` three blocks away
(`ArmorStand.ignoreExplosion` asks `isInvisible`),
- `item`: a stone item entity four blocks away,
- `boat`: an oak boat two blocks to the side.
After the blast each one is read back with `/execute if entity` (did it
survive), and with `Pos` and `Motion` where it did. `compared()` keeps those
answers and gives the lists nine decimals the way
`test_server_knockback_parity.py` reads its mob: a blast writes arbitrary bits
into `Pos` and `Motion`, and NBT prints the shortest form that round-trips.
What the first corpus says in vanilla 26.3, in all twelve cases: the visible
armour stand, the item and the boat are destroyed, and the invisible armour
stand survives untouched at its summon position with zero motion - the
`ArmorStand.ignoreExplosion` rule, seen from the outside.
#9220's own MobType literal lacked main's item_stack_limits field, so its
tests no longer built; #9271 carries the same 50-line fixture with it.
Both now use one fixture_mob that knows the air and the raider species.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's glyph callback returns a font::Glyph (its own box from the pen):
text_vertices takes it, and native_draw_text sums its advance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob.DATA_MOB_FLAGS_ID (15) carries NoAI as its first bit: the LAN and
dedicated host now sends it with the dragon, and a Horizon client reads
it and stops the dragon's tick where EnderDragon.aiStep does on a client
(no flap, phase or parts). The harness also checks what a vanilla-codec
client is paired with.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The index loop pushed a packet whose length ran past the end of the
stream (a recording that stopped mid-write), so seeking to it could
only fail. Metadata's mcversion follows VANILLA_VERSION instead of a
literal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HoneyBlock.fallOn plays the slide (and the block's fall sound when the landing hurts) and broadcasts entity event 54, ten honey crumbs. The guest turn dropped both, and clients only drew the five crumbs of a slide.
Co-authored-by: Cursor <[email protected]>
Co-Authored-By: Grok <grok-4.7> <[email protected]>
The stamp tool stamps native_hooks.rs itself and refuses it in `comments`
(test_stamp_native_hooks failed on this branch).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A fixed ALSA period that a device does not support made build_output_stream
fail, which left the game with no sound on that device. Retry with the
default configuration then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`BlockPredicate`'s `nbt` was skipped, so a predicate with it matched any
block of its `blocks` (and an `inverted` one never matched). As
`BlockPredicate.matchesBlockEntity` does, the pattern now has to be in
the block entity's `saveWithFullMetadata` (`NbtUtils.compareNbt`), and a
block without a block entity fails. The predicates' `World` asks for the
data `/data get block` builds, moved into `Game::block_entity_nbt`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A `#tag` in an advancement predicate never matched a tag a world's data
pack defines: `View::in_tag` read the game's own item, block, entity
type, biome and damage type tags. The world's tables now also keep the
entity type, biome and damage type tags with the packs' overlay
(`TagLoader.loadTagsForExistingRegistries` binds every registry's tags
from the pack stack), and `in_tag` reads them, and the item and block
tags through `Game::items()`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A mod answering native_health_hud can now reach Horizon's HealthHud
(native_health_hud_state): its left_height/right_height place the selected
item name and the action bar (vanilla Hud.leftHeight/rightHeight), and its
layout keeps Horizon's own rows. native_local_attribute(name) reads an
attribute of this client's player (LivingEntity.getAttributeValue), such
as minecraft:armor_toughness. Both through a helper-only API fragment, so
mods no longer read Game's private fields.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BYf41fgTMRfUEXn3afqyTG
The split decided per vertex, and read lava from the high nibble of the
vertex colour's alpha. With separateAo=true that byte holds each vertex's
own ambient occlusion, so the corners of one leaf quad landed in different
parts: each part pushed its other corners off screen, and in our separateAo
probe the leaves drew as speckled slivers of both parts. Lava had no flag
there and went to the cutout part.
The test now reads only the vertex's sprite layer, which all four vertices
of a quad share: OPAQUE_LAYER, or one of lava's two sprite layers, which
the game hands the renderer. In Iris 1.11.7 our separateAo probe draws
leaves whole with gbuffers_terrain_cutout and lava with
gbuffers_terrain_solid; the branch now does the same.
Part of #9137
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GPU culling becomes a setting players can switch: GPU Culling
(horizon-graphics.txt's gpuCulling, off by default and in no preset) sits in
the Performance page's Culling group right after Cave Culling, and applies at
once: every settings apply calls set_gpu_culling, which does nothing when
nothing changes. Where it isn't offered the switch is greyed out, reads Off
and says why: the device can't (no count draws the GPU wrote, compute shaders
or working first instances, or vanilla frames drawn flat), DX12 with flat
page groups, whose count draws lose each draw's base vertex (not offered there
since #9142: "try the Vulkan graphics API"), a software adapter (WARP,
lavapipe), whose pass would run on the CPU beside the frame, or its buffers
stopped fitting in video memory, until the next renderer. macOS
doesn't show it. The control word gpu-culling on|off now sets the setting in
memory, unsaved, as lod-distance does, so the switch, the word and rebuilt
renderers agree.
The statistics are exact again. Each GPU frame copies the pass's two counters
(the sections it kept, their draws) into one of three small readbacks, with
the CPU's part of the same frame: the sections the walk listed less those
whose solid quads the pass drew and counted, its draws, and every call. The
readback is mapped from the frame's after-submit hook, on the submit thread
too, and taken in a frame or a few later; nothing waits for it. So
drawn_sections, section_draws and section_draw_calls always describe one
real frame, the newest whose numbers are known: a CPU frame's at once, a GPU
frame's when its counts come back, never an older frame's over a newer one's.
The status says how old they are (stats_lag) and what is offered
(gpu_culling_offer). A readback whose mapping fails, or whose buffer the
device lost, goes FAILED and is never copied into or unmapped again, as the
GPU timer's readbacks since #6354.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderDragon.aiStep returns after the flap and checkCrystals when
isNoAi() (flapTime = 0.5): no flight history, phase, steering or parts.
Horizon's dragon never read NoAI: a NoAI dragon flew off, out of a
force-loaded chunk, and a save could then lose it. The dragon now loads
and saves NoAI, and its tick stops there.
tools/test_dragon_no_ai_parity.py compares the dragon's position and NoAI
after 100 ticks and after a restart with vanilla's dedicated server.
Fixes#9316
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Switching to peaceful removed every hostile mob in the same tick with
`mobs.retain(|m| !m.hostile())` in the difficulty command, the LAN/menu
difficulty change, and the dedicated-server start. Vanilla only sets the
difficulty there; each hostile mob is discarded by its own Mob.checkDespawn
on the next tick when the level is peaceful and the type is not allowed in
peaceful, so mobs in unloaded chunks persist until loaded and the wither and
ender dragon follow their own exceptions.
Drop the three retain calls: the per-tick check_despawn already discards the
same mobs (difficulty.effective == 0.0 && !allowed_in_peaceful), with vanilla's
timing and exceptions.
Closes#9283
Launcher 0.2.0 reaches 0.2.40 by itself wherever it can write its own folder (checked with the real
0.2.0 builds on macOS and Linux: tags, signing key, legacy list and asset names all still work). A copy
in a read-only place (the disk image, macOS's translocated copy of an app never moved out of
Downloads, a .zip opened in Explorer) fails at every check and said so only on Settings' Launcher
line, so the player never saw it.
- The window downloads and checks the new launcher (backend::stage_update), then the sidebar says
"A new version is available" with Update: the window closes, the launcher's own file runs as the
installer (`apply-update`, update_handoff.rs), waits for the window's process to exit, puts the new
launcher in place with the same code `horizon-launcher update` uses (place_staged) and starts it.
Closing the window with an update downloaded installs it too, so launchers still update themselves.
- A copy that can't replace itself (backend::cannot_replace_itself) says so with Download again (the
website's install page); a failed install starts the old launcher, which says why.
- Docs: launcher/README.md, the "download again once" row in docs/versioning.md; README's release
link no longer filters on the old launcher-v tags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The row described it as missing because the prediction only restored the block.
It is game/remote.rs::Game::block_meets_player now, and end_predictions acts on
the answer.
Entity.isColliding on a full block at the player's feet meets a 0.6 x 1.8 box
standing in it, and a block above its head, the one beside it or an empty shape
does not. The snap takes the remembered position while the teleport guard allows
it, and nothing when the block came back clear of the player or the player was
teleported at the same sequence.
ClientLevel.syncBlockState is what BlockStatePredictionHandler.endPredictionsUpTo
calls once the server acknowledges a sequence: when the block takes a state it
did not have, the level restores it, and then, if the prediction remembered a
player position and the player was not teleported since
(lastTeleportSequence < sequence), it asks Entity.isColliding(pos, state) -- the
state's own collision shape at the player's pose against the player's box -- and
calls Player.absSnapTo with the remembered point when they meet.
Horizon's end_predictions restored the block and stopped there, so a break the
server refused under the player, or a placement it pulled back, left the player
standing inside it until its own physics pushed it out.
A prediction now remembers what vanilla's ServerVerifiedState does -- the
sequence, the state the server last gave the block, and the player's position
when the change was first predicted -- and keeps all three through a
retainKnownServerState that only bumps the sequence. end_predictions restores the
block, tests the shape against the player and snaps to the remembered point when
the guard allows it. remote_teleport takes BlockStatePredictionHandler.onTeleport,
so a prediction made before a teleport keeps its block but not its snap.
Keep the independent inline parser and its fixture definitions alongside the named weather regression.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Give the entity-only probe its dry weather context and keep the new fixture beside stable command groups. Cite the predicate read context without treating abstract reads as complete Level method ports.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
A on a focused button now presses it when let go, if the focus is still on it.
Moving the focus, a cancel, a scroll, a dialog opening or another pad taking over
drops the press. On the title, X opens Options and B focuses Quit Game; on the
pause menu, Y focuses the exit button, and A confirms.
A screen says what a button is for (`ui::action_button`, `widgets::declare_action`)
and its pad button and glyph follow: Done, Back, Cancel, Apply, Options and Exit.
Nothing else names the screen. What a screen declared stops counting once another
is drawn over it or replaces it, and `pad status` lists what is declared, what
isn't and the glyphs drawn. tools/pad_menu_e2e.py checks these with the virtual pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82xRaic2UQfeBTnyhW
The command SNBT parser (horizon-command) recursed on every nested
compound/list with no depth limit, so a crafted deeply-nested argument
(stack overflow: thread 'Server thread' has overflowed its stack). Both
ArgType::Nbt (summon/data/give NBT) and ArgType::Component (tellraw/text
components) route through this parser, so one guard covers both.
Add a MAX_DEPTH (128) recursion counter to the packrat State, checked in
State::parse: over-deep input now fails to parse (the argument is
rejected) instead of recursing until the stack overflows. 128 is far
below the measured ~300-level overflow on the smallest parsing thread and
far above any real command or data value. Adds tests that deep compound
and deep extra-style list input are rejected while shallow values still
parse.
Closes#9236Fixes#9261
The test sent a chat, then an ack of offset 0, which validates in either
order. It now sends an ack of 1 and then a chat acknowledging the message
the ack brought into the window, which fails validation (and kicks) when
the ack is applied after the chat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderDragon.tickDeath (at 200 death ticks) and EnderDragon.kill (/kill)
both end with gameEvent(ENTITY_DIE) at the dragon; Horizon posted none.
Sculk sensors now hear the death, a catalyst within reach blooms (no
charge: the dragon's xpReward is 0), and on /kill a warden can hear it
too (after the death flight Warden.canTargetEntity refuses a dragon with
no health left).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
martipops's second review of #8700.
The export closes the inventory's own 2x2 crafting grid too
(InventoryMenu.removed): guest_close_all empties it only on a real
ContainerClose(0), and a frozen client's is dropped, so its items were
in no record and lost with `player-release moved`.
A frozen player's chat acknowledgements are handled in the same pass as
the last-seen updates of its chat packets, in the order they came, so an
ack followed by a message in one tick still validates.
A login that used up an expectation and fails before it reaches play
(its connection drops in configuration, or the client refuses a
required pack) gives it back with its own deadline, so the proxy's next
attempt within that time is still expected.
The export's reply gives `freeze_ticks_left` beside the record while the
player is frozen, so a proxy does not commit on the target too late for
its release to come in time.
Refs #7484
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Route fall and stalagmite hurt through the world catalog. Retain the identical no-pack preload, atomically replace it for world packs, and protect the frozen snapshot from late vanilla readers. Select all prepared-resource hurt, landing, registry and preload regressions from the damage catalog parity harness.
Co-Authored-By: Codex <[email protected]>
Vanilla's ClientHandshakePacketListenerImpl.handleLoginFinished sends the
brand payload, then ServerboundClientInformationPacket; Horizon sent them the
other way round. ClientPacketListener.handleLogin sends no client
information either: the configuration phase already did, and
broadcastClientInformation runs only when an option changes. Horizon sent a
second, identical ClientInformation in play right after Login.
tools/vanilla_wire_e2e.py gains the rules brand-before-information and
no-play-information.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read the existing dimension-aware weather levels in the predicate context and honor both optional weather fields. Add a guarded Game/Java command regression and a resource-free evaluator test without changing weather simulation.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Tooltips read the enchantment table built from the game's own files, so an
enchantment a server's data pack adds fell back to its raw id
("enchex:drill II"). The server sends that entry's definition in the
registry sync; join the entries the game lacks to the table the tooltip and
the enchanting clue read (Enchantment.getFullname), as client_biomes does
for biomes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
Vanilla asks the box's block entity for its support shape because a shulker box is a dynamic shape: an open box supports only through the one-pixel plate opposite FACING, while the exported state facts always see a closed full cube. Route sturdy, canSupportRigidBlock and canSupportCenter through the level's lid state when the block is a shulker box, keeping the cached path for every other block.
Keep the successful falling-block hurt advancement, passenger frostbite tick and general world-pack tag synchronization.
Co-Authored-By: Codex <[email protected]>
Send the celebration flag and its false default for all raiders, then apply it on clients. Exercise real tracking and packet decoding against native dirty metadata without loading shared resource state.
Fixes#8848
Co-Authored-By: Codex <[email protected]>
Minecraft.tick sends ServerboundClientTickEndPacket only while it has a
level, which ClientPacketListener.handleLogin makes and
handleConfigurationStart clears. Horizon sent one every tick from the end
of configuration, so a client waiting for a busy server's Login (Paper
with plugins: seconds) sent dozens of play packets vanilla never sends.
tools/vanilla_wire_e2e.py joins a vanilla 26.3 server through
net_capture.py with the server's Login held back 1 s, and checks the
capture against rules of vanilla's client; no-play-before-login is the
first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A brewing stand that isn't brewing, has no ingredient, has nothing to
refuel from and whose bottles already match its block state, a lit
campfire with nothing on it, and an unlit one with no cooking progress do
nothing in their server tick; they now sleep on the same terms as idle
furnaces. Lighting or putting out a campfire changes its block state, which
wakes it.
16,384 idle brewing stands cost 2.01 ms a tick instead of 8.14, and as
many unlit campfires 2.05 instead of 7.52.
Part of #8558
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An unlit furnace, blast furnace or smoker with no cooking progress and no
fuel and input pair to light does nothing in AbstractFurnaceBlockEntity's
server tick. Such a furnace now leaves the tick loop: BlockEntities keeps a
set of sleeping positions, and any change to one (get_mut, insert, take and
put, remove, a block state change, a chunk load, a bulk edit) wakes it at
once, so it ticks on the same game tick as before.
16,384 idle furnaces cost 2.07 ms a tick instead of 8.76 (2.11 for as many
chests, which have no ticker).
Part of #8558
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The dragon and the crystals were saved over the compound they were loaded
with only while they stood where they were loaded (anon_raw looks it up
by position), and never over the fields remember_tags keeps for them.
A dragon always flies off, so its Silent, Tags or name were lost at the
next save. Both now save over persist.raw, as /data get already reads
them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WitherBoss.hurtServer refuses a direct AbstractArrow or WindCharge once
isPowered. A wind charge hurts mobs as DamageKind::Thrown, which
wither::accepts let through, so every charge took a point off the armor.
The check is on the projectile, not the damage kind. A breeze's
BreezeWindCharge deals the same damage but is not a WindCharge (both only
extend AbstractWindCharge), so it still hurts, unlike what the issue asks.
/damage with the wind_charge type has no direct charge and goes through too.
tools/test_wither_wind_charge_parity.py compares the three cases against a
vanilla dedicated server.
Fixes#8779
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderDragon.tickDeath sends globalLevelEvent(1028) at the death's first
tick only when !isSilent(). Horizon roared for every dragon. The dragon's
Silent is kept with the saved fields Horizon models nowhere
(remember_tags), so the death reads it from there.
tools/test_dragon_death_roar_parity.py compares the 1028 events a client
is sent for a dying loud and a dying silent dragon with vanilla's
dedicated server (VanillaProtocolClient's new level-events mode,
LevelEventTranscript).
Fixes#8739
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerPredicate.matches looks for the entity under the player's view
only when the predicate has looking_at. #8687 skipped the ray for tick
and location only; every other trigger that builds the player's view
still cast it, against every entity, on each event.
Each of those call sites now passes its trigger id (player_entity_for),
so the ray runs only when one of that trigger's criteria may ask. A view
that serves two triggers (the thrown-item pickup, the KilledTrigger
damage source) checks both. Commands keep player_entity(), which always
looks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
A pack with either program now draws terrain in two parts over the same
draws: the quads over opaque texels (and lava) through
gbuffers_terrain_solid, the cutout ones through gbuffers_terrain_cutout,
each falling back to gbuffers_terrain's chain. Solid terrain has no
default alpha test; cutout terrain keeps the 0.1 one. Packs with neither
program keep the single draw.
This is what Iris 1.11.7 drew with our probe packs (both programs, only
one of them, alpha 0.05 outputs, renderStage).
Fixes#9137
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The message had a run of 18 spaces in the middle of the string (a line
break that was not a string continuation); a backslash continuation keeps
the source wrapped and the message on one line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bind hidden proof reuse to the exact camera and use render-complete bounds for non-mob entities. Keep the async worker active independently of the mob precheck and preserve shader-pack shadow spans.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Kacper's review of #8700 and martipops's on #8700 and #8730.
The module is off by default. `server.toml` gets a `[transfer]` section
(enabled = false, expect_seconds = 10, max_expect_seconds = 60,
max_expected = 64, max_freeze_seconds = 30) in place of the constants in
lan/arrivals.rs. horizon-server passes it to the game as `--transfers`,
beside `--control-socket`, not as a server.properties key. While it is off
every player-* request is answered "transfers are off on this server",
the hub has no table and a tick does nothing for transfers. With it on,
the game refuses to start without Velocity forwarding, so a player can
only come in through the proxy. The section is kept apart from vanilla's
accepts-transfers.
Record version 1 gets a `handover` id. An export with `freeze` starts a
hand-over: a random id, and the record kept with the player. A repeat
export while it lasts answers with the same id and the same bytes and
does not restart the freeze, so a proxy that lost the reply can ask
again. An export without `freeze` is a read, with a null id. The record
also carries the player's statistics (`stats`), as the stats file has
them.
The export now closes the player's menu in the player's own level, as
the close of every guest does on leaving, and refuses when that level
cannot be opened. It copies the live mod attachments into the player
first, so the compound is what a save would write. A player who is
leaving cannot be exported.
An expectation is used up when the login starts, on the network thread,
once the bans, the whitelist and the player limit let it in. A login
still in configuration when the time runs out keeps it.
A frozen player is not kicked for floating, and its chat and signed
commands are dropped after the messages they say the client has seen are
applied, so its chat still validates after the freeze; chat
acknowledgements are kept.
docs/server.md gives the section, who can ask (the endpoint's limits,
the token being all or nothing) and how a server reads a record: another
version or data version is refused, the NBT in `player` wins over the
plain fields, unknown fields are ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What Create's Ponder shows its machines in: native_draw_scene(batch,
rect, blocks, view) draws a few block states, at positions of the scene's
own (NativeSceneBlock), into a box of a screen, seen as NativeSceneView
says (centre, yaw, pitch, pixels per block). Each block's model is drawn
shaded by face, its faces against solid neighbours left out; a mod block
entity given with it is drawn through native_block_entity_submit, its
spin parts turning. The quads go in with the GUI's isolated depth and
scissor (HudBatch::depth_since), as the smithing screen's armour stand.
The view's matrix is a point reflection, so the unturned scene is seen
from the north as a player at yaw 0 sees it, unmirrored; the tests pin
which way yaw and pitch turn it.
Builds on the block entity drawing change (spin and place_turning).
Pending fragment: scenes_in_screens.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- native_draw_text(draw, text, at, color, glowing, outline): a Text under
a block entity's pose as a sign draws its lines (Font.drawInBatch in a
BlockEntityRenderer), each span in its colour, glowing text full bright,
an optional outline; it returns the width. The sign's glyph quads are
shared (block_entity_render::text_vertices).
- BlockEntityDraw::spin(axis, pivot, degrees_per_tick, phase) and still():
parts that turn by themselves. The game turns them each frame from the
game time and partial tick, so with keep() a shaft at a steady speed is
drawn again with no call of native_block_entity_submit until its block
entity changes (Flywheel's rotating instances, on the CPU).
- native_block_face_sprite(state, side): the texture a block state shows
on a side, by the name native_draw_cube takes, for copycat blocks.
Connected textures need no code: a content mod ships OptiFine CTM rules
under assets/<id>/optifine/ctm/, as the docs now say.
Pending fragments: draw_text.json, spinning_geometry.json,
block_face_sprite.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- native_fluid_flow(pos): FluidState.getFlow(level, pos) in the level in
place, the normalised current an item in the fluid is pushed by
(entities::fluid_flow, through the same flow_at items and boats use).
A water wheel reads its speed from it.
- Tanks take a mod's own fluids (create:chocolate): native_fluid_insert
now accepts any id in a namespace other than minecraft, besides water
and lava; extract and move already carried names. Cauldrons still take
only water and lava. NativeFluidTank becomes public and
native_fluid_tanks joins Host.
The world itself still has only vanilla's fluids: no source or flowing
blocks or buckets of a mod's fluid.
Pending fragments: fluid_flow.json, mod_fluids.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's TicketController.forceChunk: native_force_chunk(owner, chunk,
hold) holds a chunk of the level in place for a namespaced owner
(create:train/<uuid>), and native_forced_chunks(owner) lists what it holds.
A held chunk is kept as a /forceload'ed one is: Streaming has
mod_forced beside forced, Ranges reads both, the ticket levels give them
the FORCED level, and a level with any stays open. /forceload neither
lists nor releases them and they aren't saved with forced chunks: a mod
holds them again in native_level_load.
Pending fragment: chunk_tickets.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
NeoForge's IItemHandler.isItemValid for a mod's ModContainer: hoppers,
droppers and native_insert_item ask native_container_accepts(pos, entity,
slot, stack, face, cancel) before an item goes into a slot the container's
inputs and faces allow. `cancel` refuses it and the mover tries the next
slot, so a millstone takes only what it has a recipe for and a wrong item
can't clog it. tryMoveInItem now carries the face it goes in by.
Pending fragment: container_accepts.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What Create's drill, saw, harvester and deployer need, as Host helpers:
- destroy_block(pos, tool, collect): Level.destroyBlock(pos, true) with the
loot rolled for `tool` and no player (Block.dropResources(state, level,
pos, blockEntity, null, tool)). Particles, the block entity's contents
in the drops, ore experience and the fluid left behind are vanilla's
own path: GameLevel carries the tool and, with `collect`, keeps the
drops instead of popping them. The mods' say on drops applies.
- set_break_progress(breaker, pos, progress): ServerLevel.destroyBlockProgress,
the same broadcast a mob breaking a door uses (now shared,
server_destroy_block_progress).
- use_as_player(stack, eye, yaw, pitch, target, sneaking): NeoForge's
FakePlayer. useItemOn on a block, useItem in the air or interactOn with
an entity, through the guest use path (forced_hit + guest_use), in
survival with only `stack` in hand. The server's empty player shell is
filled for the call and everything it touched is put back; it answers
None while a player acts. NativeUseTarget and NativeFakeUse are new in
mod_api.
Pending fragments: machine_breaking.json, fake_player.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Trait mods couldn't reach several documented helpers through `dyn Host`:
their signatures were generic or used gameplay-private types. Host now has:
- draw_model (standalone models in block_entity_submit)
- mod_block_entity(pos) and change_mod_block_entity(pos, &mut change)
(native_with_mod_block_entity's form; on a client mod_block_entity
returns what the server sent through block_entity_update_tag)
- item_entities, level_random(dimension)
- play_sound and send_particles for the level in place, from any server
hook (until now only on LevelWriteHost)
- block_name_in_tag and server_item_in_tag
- component, set_component, remove_component
- trigger_criterion with a &dyn Fn test
- equipment(slot): the player's own on a client, the acting player's on
a server
- sneaking(): whether the acting player holds sneak
(UseOnContext.isSecondaryUseActive in a use hook)
NativeItemEntity, NativeBlockReadError, NativeComponentError and
NativeCriterion become public and are re-exported from mod_api.
Pending fragment: tools/native-hook-fragments/host_bridge.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The read side of a seamless transfer (#7484, step 2). A proxy moving a
player between two servers tells the target who is coming and reads the
player out of the source.
`player-expect <uuid> [seconds]` records that the player will log in
within that time (10 s by default, 60 at most, 64 players at once). It
refuses a bad id, a time that is not a whole number above zero, a player
already connected and a full table. The login uses the expectation up;
one no login used is reported when it runs out. The player still logs in
as anyone else: holding them is a later step.
`player-export <uuid> [freeze]` closes the player's menu as vanilla does
(AbstractContainerMenu.removed puts the carried stack back into a
connected player's inventory) and answers with record version 1: the
player compound as a save writes it (base64 NBT), the advancements JSON
and a few plain fields. It refuses a player who is not connected, has not
finished joining, is riding, asleep or dead. With `freeze`, the player's
own packets are dropped and its acknowledgements still handled, until the
freeze is let go after 600 ticks with a warning.
docs/server.md gives both requests and the record's shape.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add JSON and Prometheus golden cases for status, logs, sampled metrics, cursor handling, and stopped or running health output.
Fixes#9083
Co-Authored-By: Codex (GPT-5.6) <[email protected]>
Keep removed definitions frozen in the current session and rebuild the next catalog from current resources. Cover resource removal, reload and fresh-world behavior.
Co-Authored-By: Codex <[email protected]>
Preserve ordinary player damage, enchantment predicates and synchronized registry data for enabled resource/data packs. Validate definitions before publication and keep world definitions stable through reload.
Co-Authored-By: Codex <[email protected]>
Add conservative AABB prechecks, a non-blocking two-stage async ray and plane pipeline, and reusable voxel/proof caches for dense scenes. Keep synchronous culling on the exact mesh path, expose an async-only threshold, and add focused tests plus release/OpenGL benchmark evidence for issue #7549.
Co-Authored-By: OpenAI Codex <[email protected]>
Add the world canvas fixture type to workspace dispatch tests and verify ordered mutation through the generated event.
Co-authored-by: Codex <[email protected]>
Apply owner-updated gain, pitch and position through ordinary sound-engine channels without attaching vanilla entity or music behavior. Verify paused updates, stop and release with the channel backend.
Co-authored-by: Codex <[email protected]>
Submit camera-relative world overlays independently of block entities, using normal depth-tested atlas and font rendering. Preserve RGBA quads and draw eight-offset outlined text with the existing polygon-offset front pass.
Co-Authored-By: Codex <[email protected]>
Keep the latest main particle culling and graphics settings alongside the shared synchronous and asynchronous entity occlusion path.\n\nCo-Authored-By: OpenAI Codex <[email protected]>
Key retained ray and plane traversal grids by the snapshot lifetime, not only world revisions, so refreshed chunk visibility cannot reuse stale opaque cells. Add a regression test for a wall becoming transparent.
Co-Authored-By: OpenAI Codex <[email protected]>
Keep DDA worker scratch and result vectors across jobs, reset voxel state on snapshot changes, and move culling tests into standard feature test files.
Co-Authored-By: OpenAI Codex <[email protected]>
Read conventional Linux trigger axes when no trigger button is mapped.
Preserve mapped button input and exclude the 8BitDo stick-axis layout.
Cover pull values, absent input and mapping precedence.
Co-Authored-By: Codex <[email protected]>
- Enable Minecraft Realms button on main menu and handle navigation
- Implement dedicated Realms browser screen supporting world list, status badges, selection and refreshing
- Implement Realms HTTPS client querying pc.realms.minecraft.net using AuthSession access token
- Add join endpoint resolution and direct server connection via Action::Connect
- Update agent mode serialization and port map ledger
Isolate the entity occlusion cache and worker from particle visibility. Keep entity modes in the existing settings, submit inflated entity AABBs before model generation, and refine visible entities with their emitted bounds. Preserve fail-open handling and add async culling telemetry.
Co-Authored-By: OpenAI Codex <[email protected]>
Stub the new input trace types and no-op recorder so the focused device-event fixture continues to compile when it extracts Game::device_event.
Co-Authored-By: OpenAI Codex <[email protected]>
Record both mode orders on the merged head and make the single-scene and cache-counter comparison limits explicit.
Co-Authored-By: OpenAI Codex <[email protected]>
Replace the collision-prone four-way visibility cache with a bounded sorted cache and validate repeated occlusion proofs through frame-local ordered lookup. Keep entity visibility conservative and off block entities, and add batched particle culling modes sharing the occlusion worker.
Add a reproducible benchmark note and screenshots for Sync and Async modes.
Fixes#6684
Co-Authored-By: OpenAI Codex <[email protected]>
Keep block entity frustum checks and section cache rendering independent of the ordinary entity occlusion culler. Show mode-specific culling help in the graphics UI.
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-02 08:04:05 +03:30
1624 changed files with 184360 additions and 15632 deletions
`tools/check_public_tree.py` rejects them; `git config core.hooksPath .githooks` runs it on commit.
- **Keep your runs away from real player data.** Any game or launcher you start gets fresh `mktemp -d`
folders in `HORIZON_HOME` (the game's data), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`, and, for the
folders in `HORIZON_HOME` (logs, crash reports, telemetry consent, recordings; not saves), `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME` and
`XDG_CONFIG_HOME`, plus `HORIZON_LAUNCHER_NO_SELF_UPDATE=1` and `HORIZON_NO_AUDIO=1`. Saves, `options.txt` and
caches go in the game folder, by default the working directory: start the game from a scratch folder with
`--game-dir` pointing at it, or they land in the repo root, hidden by `.gitignore`. For the
launcher, `HORIZON_LAUNCHER_TRASH`, `HORIZON_LAUNCHER_CLIPBOARD` and `HORIZON_LAUNCHER_DESKTOP` pointing into the scratch home. Never write to
`~/.local/share/{applications,horizon,horizon-launcher}` or `~/.local/bin`.
- **Mark what an agent posts.** A PR body starts with a line naming the person whose agent opened it and
@@ -32,7 +34,10 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- 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: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same terrain, checked against the Java game by more than 300 differential harnesses. Free, open source and unofficial.
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
> Not an official Minecraft product, nor approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own copy of Java Edition 26.3.
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher-v&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Home page: the selected instance ready to play, the version picker, Join a friend and the instance's tabs"></td>
<td width="50%"><img src="docs/media/launcher-settings.webp" alt="The launcher's Settings page: Microsoft account, offline name, updates, appearance and accent colour"></td>
</tr>
</table>
<p align="center">
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
</p>
## What works today
- **The game:** vanilla world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on vanilla saves. Worlds from 1.8 to 26.2 are upgraded. [Status](#project-status).
- **Extras:** new menus, graphics presets, distant terrain up to 1024 chunks, controllers, zoom, minimap and more, off by default where they change the vanilla look. [Settings](https://playhorizon.dev/docs/settings).
- **Packs:** resource packs, and data packs with functions, predicates, item modifiers and world generation.
- **Multiplayer:** invite friends with a code, Open to LAN, join servers (online mode included). [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** content mods (blocks, items, data) and native Rust mods, built by the launcher with nothing to install. Java mod jars do not run. [Mods](https://playhorizon.dev/docs/mods).
Everything below is in the current release.
In the Rust client, with vanilla assets and no mods or shaders:
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
</tr>
</table>
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
## Download
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
| Platform | File | Needs |
| --- | --- | --- |
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
<table>
<tr>
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
</tr>
</table>
## Mods and shader packs
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
## Host a server
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and needs no Java. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Modpacks
The launcher imports Modrinth and CurseForge packs and plays the mods Horizon can carry over: today, content mods made of data and assets, not Java code. [Modpacks](https://playhorizon.dev/docs/modpacks).
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
## Agent control
@@ -80,13 +90,11 @@ AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, re
```sh
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
horizon-ctl state # position, health, food, target, screen...
horizon-ctl find-block birch_log # the nearest logs
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
horizon-ctl break -1 65 -3 # mines it; replies with the drops
horizon-ctl open-inventory && horizon-ctl click-slot 0# menus: slots, buttons by label, text fields
```
Commands block until done and reply with the result; `horizon-ctl help` lists them all. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
## Build from source
@@ -105,13 +113,9 @@ cargo run --release -p horizon-client
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
## Roadmap
What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md), or [playhorizon.dev/hub](https://playhorizon.dev/hub) with the work board and what shipped.
## Contributing
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
```sh
git config core.hooksPath .githooks
@@ -124,7 +128,7 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
**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 harness results below say that what is tested matches.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
### Java parity: 100%
@@ -147,14 +151,8 @@ Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
### Known issues
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
## Licence
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
## Author
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
@@ -206,7 +206,7 @@ Manage a server from a console, ban and pardon players, and drive it from other
<details><summary>Technical</summary>
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
`horizon-server console` (a text interface), `ban`, `pardon`, `ban-ip` and `mcp` over stdio. Admin API additions: the player list, kick, ban, op and whitelist, an SSE stream of logs and events, a text metrics endpoint, config GET and PATCH, backup delete and download. The last pieces merged: bans edited while the server is stopped ([#9767](https://github.com/horizon-rust/horizon/issues/9767)) and the MCP tools' contracts pinned by a test ([#9748](https://github.com/horizon-rust/horizon/issues/9748)). The panel's later features and vanilla's management protocol are their own item (server-panel-management). [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
</details>
@@ -266,7 +266,7 @@ Shader packs were removed and are being rebuilt from scratch. They come back, of
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/horizon-rust/horizon/issues/1584), [#1585](https://github.com/horizon-rust/horizon/issues/1585), [#1586](https://github.com/horizon-rust/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/horizon-rust/horizon/issues/1698) to [#1801](https://github.com/horizon-rust/horizon/issues/1801)).
@@ -298,6 +298,8 @@ Far terrain drawn from the world you really built and explored, caves and builds
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/horizon-rust/horizon/issues/718).
@@ -308,7 +310,7 @@ Several smaller speed-ups, each measured: idle block entities that sleep, cheape
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/horizon-rust/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
@@ -322,6 +324,8 @@ Recognise mods built for versions before 26.1 in the mod browser, so they can be
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
@@ -396,6 +400,16 @@ A Datapacks tab beside Mods and Resource packs: install into a chosen world's da
### Servers and multiplayer
#### Server panel extras and vanilla's management protocol
More of a server managed from its web panel (bans, worlds, logs and crash reports, modpacks), and vanilla's own server-management tools working with a Horizon server.
<details><summary>Technical</summary>
The panel's later features (bans, worlds, server.properties import, logs and crash reports, modpacks) and vanilla's management protocol (the JSON-RPC over WebSocket that the `management-server-*` keys configure), so vanilla's tools can manage a Horizon server. Split from server-admin. [docs/server.md](docs/server.md), [docs/api/server.md](docs/api/server.md).
</details>
#### Ely.by and other sign-in providers
Sign in with other account providers, such as Ely.by or any Yggdrasil-compatible one, beside Microsoft.
@@ -526,6 +540,8 @@ Contraptions and physics like the Create and Aeronautics mods, as a mod for Hori
Issue [#805](https://github.com/horizon-rust/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod.
@@ -548,6 +564,8 @@ Every past and future Minecraft version prepared for Horizon automatically. When
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
#### Controllers: tested on real pads, and in the launcher
@@ -558,6 +576,8 @@ Make controller support solid: tested with real PlayStation and Xbox pads and a
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
@@ -600,16 +620,6 @@ An output device list in Music & Sounds, falling back to the system default. [#1
</details>
#### Play in your language
Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English.
<details><summary>Technical</summary>
Translatable components resolved through the selected language for items, containers and server text ([#1806](https://github.com/horizon-rust/horizon/issues/1806)), on top of the bundled languages the prepared resources carry ([#1594](https://github.com/horizon-rust/horizon/issues/1594)). Left: the client's own screens ([#1672](https://github.com/horizon-rust/horizon/issues/1672)) and the main menu with zh_cn.
</details>
### Rendering
#### A light default shader pack
@@ -668,6 +678,16 @@ A shared resource store keyed by the preparer's digest, no prepare intermediates
</details>
#### A launcher screen readers can read
Screen readers read the launcher's buttons, lists and settings, and every part of it works from the keyboard.
<details><summary>Technical</summary>
Issue [#4537](https://github.com/horizon-rust/horizon/issues/4537): the launcher is GPUI (not Electron), so it exposes nothing to UI Automation, NSAccessibility or AT-SPI yet. Needs an accessibility tree from GPUI (an AccessKit bridge) and a role and label on each widget.
</details>
#### A visible beta label
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
On a system Horizon has no download for, such as FreeBSD, the launcher offers to build the game on your computer, as it already does for native mods.
<details><summary>Technical</summary>
Issue [#4354](https://github.com/horizon-rust/horizon/issues/4354): when no release asset matches the platform, the launcher fetches the tagged source and runs the same pinned-toolchain `cargo build --locked` its native-mod builds use, with a warning about the time it takes. The build is unsigned, so the launcher marks the version as built locally.
More: [docs/native-mods.md](docs/native-mods.md)
</details>
#### Horizon on Windows ARM64
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
@@ -710,6 +742,8 @@ Versions of Horizon for Android, iOS and web browsers. Nothing official yet; con
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/horizon-rust/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/horizon-rust/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/horizon-rust/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
@@ -752,6 +786,8 @@ Record, replay and edit videos without leaving the game: record in a keypress, r
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
@@ -842,6 +878,8 @@ Every download Horizon makes is checked against the project's signature, includi
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/horizon-rust/horizon/issues/1011), [#1014](https://github.com/horizon-rust/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
Banners in End cities show as plain magenta instead of with their pattern.
<details><summary>Technical</summary>
Compare the banner block entities the End city templates place with vanilla's (`EndCityPieces`, the templates' block entity NBT, `BannerBlockEntity`'s patterns), in `crates/horizon-world/src/worldgen/structure/`. Issue [#1438](https://github.com/horizon-rust/horizon/issues/1438); possibly related: [#526](https://github.com/horizon-rust/horizon/issues/526). The templates' block entities now keep their patterns ([#1928](https://github.com/horizon-rust/horizon/issues/1928)).
</details>
#### Enchanted loot in End cities
Armour and tools in End city chests should come enchanted, as in vanilla.
<details><summary>Technical</summary>
`loot.rs` implements `enchant_with_levels`: find why the End city chest table's rolls come out bare, and compare them with vanilla's for a few seeds. Issues [#1437](https://github.com/horizon-rust/horizon/issues/1437), [#1660](https://github.com/horizon-rust/horizon/issues/1660). The loot's enchantment functions now reach every table that rolls ([#1911](https://github.com/horizon-rust/horizon/issues/1911), [#2037](https://github.com/horizon-rust/horizon/issues/2037)).
</details>
## Recently shipped
- **Enchanted loot in End cities** (v0.2.73): Armour and tools in End city chests should come enchanted, as in vanilla. [#1437](https://github.com/horizon-rust/horizon/issues/1437) · [#1660](https://github.com/horizon-rust/horizon/issues/1660) · [#1911](https://github.com/horizon-rust/horizon/issues/1911) · [#2037](https://github.com/horizon-rust/horizon/issues/2037)
- **End city banners keep their pattern** (v0.2.67): Banners in End cities show as plain magenta instead of with their pattern. [#1438](https://github.com/horizon-rust/horizon/issues/1438) · [#526](https://github.com/horizon-rust/horizon/issues/526) · [#1928](https://github.com/horizon-rust/horizon/issues/1928)
- **Play in your language** (v0.2.65): Item names, container titles and server messages follow the language you pick, as in vanilla. Today parts of the game stay in English. [#1806](https://github.com/horizon-rust/horizon/issues/1806) · [#1594](https://github.com/horizon-rust/horizon/issues/1594) · [#1672](https://github.com/horizon-rust/horizon/issues/1672)
- **Chickens flap while they fall** (v0.2.67): Chickens should flap their wings while they fall, not stand still in the air. [#1706](https://github.com/horizon-rust/horizon/issues/1706) · [#1892](https://github.com/horizon-rust/horizon/issues/1892)
- **Ctrl+Backspace and scroll bars in menus** (v0.2.67): Ctrl+Backspace should delete a whole word in the creative search, and every list's scroll bar should be draggable. [#1764](https://github.com/horizon-rust/horizon/issues/1764) · [#1782](https://github.com/horizon-rust/horizon/issues/1782) · [#1884](https://github.com/horizon-rust/horizon/issues/1884) · [#1931](https://github.com/horizon-rust/horizon/issues/1931)
- **A pointing hand over buttons** (v0.2.67): The mouse pointer should turn into a pointing hand over menu buttons, as in vanilla 26.3. [#1347](https://github.com/horizon-rust/horizon/issues/1347) · [#1897](https://github.com/horizon-rust/horizon/issues/1897)
These measurements came from the pre-split integration branch for PR #7054. That build contained the entity-occlusion rewrite and particle light lookup cache; Particle Culling was Off. They are useful comparison evidence, but they are not a benchmark of this isolated entity-only commit. The cache-only run in PR #7391 measured effectively the same CPU time on the ordinary scene, but the combined result still cannot isolate interactions.
Host: Intel Core i7-3770S, Intel HD Graphics 4000, Vulkan/Mesa, 675×727, render distance 4, VSync off, 260 FPS cap. Two opposite-order passes measured for 8 seconds after warmup. Values are means. CPU is harness average CPU ms per frame; p99 is frame time. The driver supplied no usable GPU timing.
## Ordinary scene
The scene had 24 mobs, roughly 9,400–10,100 rain particles and campfire smoke.
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
This normal-scene run does not show a large FPS change; its CPU differences are small relative to frame time and not attributable to the entity-only change by themselves.
## Opaque-wall scene
A 32×32 platform at Y=300, a 7-block wall with a one-block vertical opening, and 192 pigs spawned behind the wall (216 mobs in the captured scene). The player looked toward the opening from the near side. The same world and camera path were used for the main and integration builds.
| Build and mode | FPS | CPU ms/frame | Frame p99 ms |
The asynchronous mode reduced measured CPU time slightly in this wall scene but did not improve FPS beyond run-to-run spread. This is a synthetic occlusion-heavy scene, not a broad performance claim.
## Captures
The HUD performance overlay is visible in each capture. The integration-build screenshots were recorded with the same wall and camera position in Off, Synchronous, and Asynchronous modes.
# Entity occlusion pipeline — release/OpenGL run (2026-11-04)
This is the final local smoke benchmark for the entity-occlusion pipeline before opening the PR. It was run from the branch after the release build completed, with the OpenGL backend selected explicitly. The same process created each scene, switched the culling mode, flew the camera for two seconds, collected frame/CPU telemetry, and captured the screenshot after the scene settled.
The values below are branch-only measurements. They are useful for comparing the three modes under identical geometry and for checking that the worker is doing useful work. A fresh main binary was not built in this short pass, so these numbers do not claim a controlled main-to-branch delta.
| Scene | Off FPS / CPU p50 | Sync FPS / CPU p50 | Async FPS / CPU p50 | Async entities culled |
| --- | ---: | ---: | ---: | ---: |
| `entity_normal` (24 visible) | 240.5 / 0.586 ms | 233.5 / 0.762 ms | 239.3 / 0.686 ms | 0 |
Async telemetry confirmed that the render thread stayed non-blocking (`try_recv`/`try_send`, `worker_blocking_wait_ms: 0`). The latest wall/slit run completed 562 worker jobs, queued 3,982 queries, avoided about 4.0 million entity bytes, and had 379 ns recent p50 worker-job time. The solid 512-pig run culled 15,552 generated vertices; the slit run culled 120,290 generated vertices. The normal scene recorded no pre-model hides and kept the opt-in path close to Off.
## Captures
Every image includes the FPS performance overlay. The names are `<mode>-<scene>.png`:
From the repository root, with prepared 26.3 resources available:
```sh
python3 tools/entity_occlusion_bench.py
```
The script uses fresh temporary game data, the release binary in `target/release`, OpenGL, the FPS overlay, and writes JSON plus captures under `/tmp/horizon-entity-occlusion-release-quick/`.
# `keys.contains(&KeyCode::ShiftLeft)` reads a mapping by its default key: ask by `Action`, so the
# mapping is named and every device that can hold it counts.
disallowed-methods=[
{path="horizon_client_input::held::HeldKeys::contains",reason="use `held(Action::..)`, or `raw` for a key that is no mapping's default",allow-invalid=true},
/// The English description: the fallback of [`Item::description_text`].
pubfndescription(self)-> &'staticstr{
matchself{
Item::Pregen=>{
"Enables /pregen in your single-player and LAN worlds. With cheats enabled, use /pregen start to generate an area before exploring it. Does not enable the command on servers you join."
}
Item::Fullbright=>"Show blocks and entities at full light, even in dark areas.",
Item::DynamicLights=>{
"Torches and other light sources held, dropped or on fire light the blocks around them."
@@ -788,7 +859,18 @@ impl Item {
Item::CosmeticsOthers=>{
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsAuras=>"Auras and trails other players wear. Off by default.",
Item::CosmeticsHideFx=>{
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
}
Item::CosmeticsSolo=>{
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
}
Item::HorizonMarks=>{
"Show the Horizon logo next to Horizon players: a small logo before the names of other players who use Horizon, above their heads and in the player list. Only Horizon players see it: servers receive nothing."
}
Item::HorizonMe=>{
"Show me as a Horizon player: other Horizon players see the logo by your name. On, the service answers anyone who asks about you that you played Horizon in the last 3 hours; off, it never reports you."
}
Item::Pvp(o)=>o.description(),
Item::WheelCyclesSettings=>{
"The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list."
Loaded 100 of 1624 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.