Compare commits

..
Author SHA1 Message Date
Ludovic Marie 11102a5651 Merge pull request #12516 from RINNECODER/up5/port-armor-tags
🤖 Agent: The declaration passes horizon-mods' item check: max_stack_size 1 is the field (1..=99), and minecraft:equippable with only slot is a valid 26.3 Equippable. The #tag walk stops at 8 levels and reads only the carried jar's tags, so the oracle path and untagged items are unchanged.
2026-11-08 14:28:04 +01:00
Ludovic Marie 0cf6116d8d Merge pull request #12505 from sluisr/sec/protocol-recursion-and-threads-soundness
🤖 Agent: Same diff as #12490, which I reviewed before its author closed it (its verify failure was a linker crash on a full-disk runner). A server's registry ids can no longer walk out of the resources folder (no ., .., empty or upper-case segments), predicate components get the same nesting guard as potions, and banner layer counts are bounded by the bytes left (2 per layer).
2026-11-08 14:28:00 +01:00
Ludovic Marie bf53e59fae Merge pull request #12511 from MSedgeMC/port_map/debug-profile
🤖 Agent: DebugEntries has load, set_status, toggle_status, enabled and save, and the F3+F6 editor is game/debug_options.rs, so the 'no editor' skip note was out of date.
2026-11-08 14:27:57 +01:00
Ludovic Marie affb4cbc98 Merge pull request #12510 from MSedgeMC/port_map/multiplayer-allowed
🤖 Agent: AccountStatus::allows_multiplayer in ban_notice.rs is the account check the title screen reads; a skip:service with no note was stale.
2026-11-08 14:27:52 +01:00
Ludovic Marie ab6fe1802d Merge pull request #12509 from MSedgeMC/port_map/world-dimensions-stable
🤖 Agent: dimension_is_vanilla_like in save.rs holds the three stable arms, so the isStable* rows are inline there and isVanillaLike is the port.
2026-11-08 14:27:47 +01:00
Ludovic Marie c23c62df76 Merge pull request #12508 from MSedgeMC/port_map/equipment-is-armor
🤖 Agent: Slot::is_armor exists in mobs/equip.rs (head, chest, legs, feet, not the hands), so the stale skip:other becomes the port.
2026-11-08 14:27:42 +01:00
Ludovic Marie 67093685db Merge pull request #12458 from glitchsnake/fix/11807-summon-duplicate-uuid
🤖 Agent: PersistentEntitySectionManager refuses a UUID already in the level, so a duplicate inside a summoned tree stays a saved passenger under Passengers. Cost to watch: target_with_uuid scans every known UUID, now once per loaded entity with a UUID. I pushed #[ignore] on its second test, which unwraps HORIZON_TEST_RESOURCES like its ignored sibling (verify failed on it).
2026-11-08 14:27:39 +01:00
Ludovic Marie 012391f4aa Merge pull request #12405 from TaquitoSuabe/perf/cheap-in-order-chunks
🤖 Agent: Columns decode on up to 4 workers and are delivered strictly in arrival order (seq). I pushed a fix: the attachments retain runs after the column's block entities load again, so a resent column keeps its block entities' attachments.
2026-11-08 14:23:38 +01:00
Ludovic Marie 4757e2ddca Merge pull request #12507 from MSedgeMC/port_map/weather-layers
🤖 Agent: Builder::weather_layers and weather_layer are in horizon-world's environment_system.rs on main; environment.rs no longer has apply_weather, so the two rows point at the code that runs.
2026-11-08 14:23:35 +01:00
Ludovic Marie c1e8fc5d4b Merge pull request #12495 from MSedgeMC/fix/non-tame-prey-recheck
🤖 Agent: NonTameRandomTargetGoal: canUse needs an untamed mob, and the target keeps matching the selector (Turtle.BABY_ON_LAND_SELECTOR now checks !isInWater), so a baby turtle that reaches water is let go.
2026-11-08 14:22:01 +01:00
Ludovic Marie aa3d35ebc7 Merge pull request #12492 from sluisr/perf/entity-pool-and-meshing-allocs
🤖 Agent: pack_lighting reuses one stack array (every row below h is fully written, the rows above refilled), the server's object states reuse the pooled States, and the guest turn skips the 5-stack clone when the gear is unchanged.
2026-11-08 14:21:58 +01:00
Ludovic Marie d178e80ea5 Merge pull request #12484 from TheMrZZ/fix/h182-trader-llama-discard
🤖 Agent: TraderLlama.maybeDespawn discards in aiStep, so a removed trader llama leaves the mob list in that tick (tick returns false); tame or persistent ones stay.
2026-11-08 14:21:54 +01:00
Ludovic Marie 7f117f484e Merge pull request #12465 from TheMrZZ/fix/armor-stand-lodestone-inventory-tick
🤖 Agent: EntityEquipment.tick runs each equipped item's inventoryTick, so a stand's lodestone compass loses its target when the lodestone goes; only compass stacks pay the lookup.
2026-11-08 14:21:50 +01:00
Ludovic Marie d3910293e0 Merge pull request #12457 from Joansitoh/fix/11886-trident-falls
🤖 Agent: AbstractArrow.tick: a stuck trident falls when the block at its position differs from lastState (the hit block) and nothing is within 0.06; startFalling's three nextFloat()*0.2F draws scale x, y, z in that order.
2026-11-08 14:21:47 +01:00
Ludovic Marie c3d6c640ac Merge pull request #12383 from TheMrZZ/fix/h259-unused-feature-startup
🤖 Agent: An unused invalid fill_layer in a selected pack now fails world preparation through the existing leaf codec, as vanilla's registry load does, instead of the dedicated server falling back to a flat generator; its capture harness (--kind/--output) runs by name only (pushed).
2026-11-08 14:21:44 +01:00
Ludovic Marie 37e9e82dfd Merge pull request #12501 from MSedgeMC/fix/spider-vehicle-attack
🤖 Agent: SpiderAttackGoal.canUse is super.canUse() && !isVehicle(): Mob.rider (a player on its back, set by mounts.rs and the remote/LAN vehicle paths) now counts beside extra.rider (a mob jockey).
2026-11-08 14:21:40 +01:00
Ludovic Marie f4eebc417e Merge pull request #12500 from MSedgeMC/fix/hurt-by-universal-anger
🤖 Agent: HurtByTargetGoal.canUse returns false for a player's hit when universal_anger is on (checked after the timestamp, before the ignore classes, as vanilla); Who has only Player/Mob, so guests' hits take the same path.
2026-11-08 14:21:36 +01:00
Ludovic Marie 0e83631f9d Merge pull request #12499 from TheMrZZ/fix/entity-predicate-movement
🤖 Agent: MovementPredicate.matches as vanilla: per-tick velocity × 20, speed and horizontal_speed against squared bounds (a negative bound squares, as MinMaxBounds.Doubles does), vertical_speed on |y|, exact bounds with no tolerance; only an unmounted, unridden mob gets the snapshot, so other subjects keep today's false.
2026-11-08 14:21:33 +01:00
Ludovic Marie a2140a9ef6 Merge pull request #12502 from TheMrZZ/fix/golem-defense-reputation
🤖 Agent: DefendVillage reads the villager's gossips by the selected player's own UUID instead of level.player_uuid, so a hated guest on a server is attacked and a hated host no longer turns the golem on a guest. I pushed a commit dropping a doc paragraph and a constant copied from another test file.
2026-11-08 14:21:30 +01:00
Ludovic Marie 35b2938c47 Merge pull request #12503 from vnsmoreira/packs/polytone-particle-runtime
🤖 Agent: Merged main in (game.rs frame block and the add/add on horizon-polytone particles.rs, which matches main plus tick_source); the merged diff equals the last commit. Without a Polytone pack, polytone_animate/polytone_entity/make return at the first `let Some(registry)`, so the animate tick and spawn paths pay nothing.
2026-11-08 14:21:26 +01:00
Ludovic Marie 236b13f290 Merge pull request #12389 from Vaspyyy/armor-stand-entity-collisions
🤖 Agent: Stands collide with shulkers, boats and a still adult happy ghast through Entity.collide's getEntityCollisions, with an obstacle grid built only when a stand moves, and Shulker.makeBoundingBox's lid lifts what rests on it.
2026-11-08 14:21:22 +01:00
Ludovic Marie 838ae17615 Merge pull request #12504 from horizon-rust/cg72/grindstone-default-max-damage
🤖 Agent: Grindstone max_damage fix for main's menu_click 7998/8000 (#11434): writes the merged MAX_DAMAGE only when it differs from the item's own, as PatchedDataComponentMap.set does; unit test with the failing case
2026-11-08 14:21:18 +01:00
RINNECODERandClaude Opus 5.5 bdc8a23f21 Port pack: without the oracle, armour the vanilla armour tags name is worn in its slot
A carry without the oracle's facts declared every item that has no block as
a stick, so a mod's helmets and chestplates could not be worn. Items the
carried minecraft:{head,chest,leg,foot}_armor tags name (directly or through
the mod's own tags) now get minecraft:equippable with that slot and a stack
size of 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:11:06 -06:00
Horizon agentandClaude Opus 5.5 087ce1708b Merge origin/main into armor-stand-entity-collisions
ObstacleGrid keeps main's reused scratch list (near takes &mut self) with
this branch's roots, near_except and huge-area path; the shulker keeps one
progress_aabb with a scale and bullet_origin is the centre of bounds_at;
the shulker shell tick and main's trader llama tick both run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 14:03:16 +01:00
MSedgeMC 61b7fa5b26 port_map: the three stable-dimension helpers are arms of dimension_is_vanilla_like 2026-11-08 13:01:34 +00:00
MSedgeMC 1df93f9739 port_map: the rest of the debug profile editor rows are ports 2026-11-08 13:00:39 +00:00
MSedgeMC 17e2982614 port_map: the debug profile load and save rows are ports
Both methods are implemented in debug_entries.rs and used by the F3+F6
DebugOptionsScreen; the rows' "no editor" note is stale.
2026-11-08 12:59:18 +00:00
Horizon agentandClaude Opus 5.5 eb74ad2a63 Merge origin/main into packs/polytone-particle-runtime
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:59:14 +01:00
MSedgeMC 34aa927de4 port_map: Minecraft.allowsMultiplayer is allows_multiplayer
The row skipped the account check as a service. AccountStatus::allows_multiplayer
is documented as the method and the title screen enables its Multiplayer and
Realms buttons from it.
2026-11-08 12:58:48 +00:00
MSedgeMC 491403897f port_map: WorldDimensions.isVanillaLike is dimension_is_vanilla_like
The row skipped the predicate, but save::dimension_is_vanilla_like is
documented as the method and its caller world_gen_unstable drives the
experimental-settings warning the note mentions.
2026-11-08 12:58:14 +00:00
MSedgeMC 3f7e6970ae port_map: EquipmentSlot.isArmor is Slot::is_armor
The row skipped the predicate as a plain constant, but Slot::is_armor is
documented as EquipmentSlot.isArmor and read by slot_for and mob loot
replacement.
2026-11-08 12:57:49 +00:00
Horizon agentandClaude Opus 5.5 d609f56d42 golem_defense_tests: drop a doc paragraph and a constant copied from another test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:57:20 +01:00
MSedgeMC 37ed06402d port_map: the weather layer rows name environment_system
apply_weather is gone; the weather layers live in horizon-world's
environment_system: Builder::weather_layers adds one per attribute rain or
thunder changes, and Builder::weather_layer registers the entries weather
runs.
2026-11-08 12:57:19 +00:00
sluisr e5043bb090 fix(security): bound item predicate recursion, validate registry paths, and fix threads UB
- Guard recursive `Component::read` calls in `skip::component_matchers`,
  `gameplay::block_predicate`, and `v1_20_3::from_tag` with `Nested::enter()`
  to prevent stack overflow DoS via deeply nested `CAN_PLACE_ON` / `CAN_BREAK`
  exact component matchers, and tighten `banner_layers` pre-allocation cap.
- Validate registry and entry resource location segments in
  `net::fill_known_entries` before joining paths and reading local files on
  `Clientbound::RegistryData`.
- Fix data race UB in `horizon_threads::heap_census` (`slot` / `slot_name`)
  by publishing `NAME_PTR` with `Release` and loading with `Acquire`, and
  guard `CensusAllocator::alloc` / `realloc` against size addition overflow.
- Fix provenance UB in `horizon_threads::boot_paint` where an immutable
  reference `&count` was passed as Win32 `AddFontMemResourceEx`'s mutable
  `pNumFonts` out-pointer, and require `'static` font byte slices.
2026-11-08 13:55:29 +01:00
Horizon agentandClaude Opus 5.5 d035060e66 Grindstone: a merged max damage equal to the item's own writes no component
GrindstoneMenu.mergeItems sets MAX_DAMAGE to the larger of the two, and
PatchedDataComponentMap.set drops a value equal to the prototype's. Since
#11434 Horizon always wrote it, so merging two plain diamond pickaxes gave
one carrying max_damage:1561 (menu_click 7998/8000 on main).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:50:25 +01:00
MSedgeMC afb2ce0f1e A spider carrying a player does not start its attack
`SpiderAttackGoal.canUse` is `super.canUse() && !isVehicle()`, and
`isVehicle()` is true for any rider. Horizon's `SpiderAttack` checked only
mob riders (`extra.rider`), so a spider with a player on its back began
the melee goal.
2026-11-08 12:48:48 +00:00
MSedgeMC 2ffc8b5f56 Universal anger leaves a player's hit to the anger goals
`HurtByTargetGoal.canUse` refuses a player's hit when the
`universal_anger` game rule is on, leaving that hit to the anger target
goals. Horizon's `HurtByTarget` always retaliated against the player, so
the rule changed the anger bookkeeping but not the direct retaliation.
2026-11-08 12:48:16 +00:00
Ludovic Marie 447e9d277d Merge pull request #12477 from vnsmoreira/packs/polytone-particles
🤖 Agent: Includes #12475 and #12476. Particle and emitter files are parsed with every expression compiled at load (a typo fails the load); nothing spawns them yet, so no runtime cost.
2026-11-08 13:42:09 +01:00
TheMrZZandCodex d0d9e050d3 Evaluate movement predicates for supported Mob query snapshots
Preserve global ranges and unsupported provider contexts.

Co-Authored-By: Codex <[email protected]>
2026-11-08 13:41:10 +01:00
MSedgeMC 678dfde14b A non-tame predator stops hunting prey its selector no longer matches
`NonTameRandomTargetGoal.canContinueToUse` re-tests the goal's
`targetConditions` while hunting, so the `Turtle.BABY_ON_LAND_SELECTOR`
(a baby turtle out of water) drops out as soon as the turtle enters
water. Horizon's `NearestTarget` only ran the base continue rules, and its
turtle filter never checked the water, so a wolf or cat kept hunting the
turtle.
2026-11-08 12:37:25 +00:00
Ludovic Marie 1597853de7 Merge pull request #12486 from echo-ilovech3ss/fix/12287-windows-save-roundtrip-paths
🤖 Agent: Tooling only: save_roundtrip compares relative .dat paths as POSIX on every OS, so Windows' backslashes no longer make every file look renamed.
2026-11-08 13:36:22 +01:00
Ludovic Marie 7d0f866a47 Merge pull request #12481 from RINNECODER/up4/upgrade-surface-rule-biomes
🤖 Agent: A surface rule's biome_is keeps vanilla's biomes and the pack's own; others (a "comment" entry, another mod's biome) are dropped and reported as lossy, since 26.3 refuses the pack over them.
2026-11-08 13:36:18 +01:00
Vinícius MoreiraandClaude Opus 5.5 600fd5f04a Polytone: run custom particles and block and entity particle emitters
Resource packs' custom particle types now run in the particle engine: the
initializer sets the particle up, the ticker moves, recolours and removes it,
its emitters spawn more, emitter values seed the particles they spawn, and
`limit` caps how many live at once. Block modifiers' emitters run from the
block animate tick, the way torches spawn flames; entity modifiers' emitters
run on the player each client tick (fog, dust and weather around them).
Expressions see the particle, the blocks and biome around it, the player,
the camera, the time and weather.

Model and GPU particles aren't drawn yet (model ones still tick and emit);
additive blending and rotation modes fall back to the plain translucent
billboard; entity emitters ignore bones.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:33:09 -03:00
Vinícius MoreiraandClaude Opus 5.5 2145f26929 Polytone: read custom particles and particle emitters
Reads what a pack's particles do (wiki "Custom Particle Types", "Block
Properties Modifiers", "Entity Modifiers"): a custom particle type's kind
(custom, semi custom copying a vanilla one, GPU), render type, liquid
affinity, physics and despawn flags, limit, tick interval, exclusion
radius, light level and rotation mode; its initializer and ticker
expressions with their named variables; and the particle emitters that
particles, blocks and entity bones carry (the particle by id or with its
options, chance, count, position, motion, colour, size and customs, the
state predicate, biomes, spawn location, bone, max distance). Every
expression is compiled when the pack loads, so a typo is a load error.
Nothing spawns them yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:33:09 -03:00
Vinícius MoreiraandClaude Opus 5.5 36e962a58c Polytone: colormap values in attribute layers
An attribute value or argument can name a Polytone colormap (wiki
"Colormaps"): an image read at the column and row two expressions pick,
each 0 to 1 with 1 the top-left corner, rounded to the nearest pixel by
default, the row scaled by the column when triangular. Without a .json
the axes are the biome's temperature and downfall, which the frame now
passes along. The colour is the modifier's argument whenever the layer
applies; a transparent pixel falls back to the value before the pack (the
entry the modifier replaced), as fallback_colormap's default "original".

Colormaps load once per pack reload, shared by the three dimensions'
stacks, and only those a modifier names, decoded to RGBA ints as on
Java. The biome_id axis isn't supported yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:33:09 -03:00
Vinícius MoreiraandClaude Opus 5.5 3ce27e636b Polytone: an expression value out of range is clamped, not dropped
A number an expression gives outside its attribute's range goes through the range as the sampled
value does (AttributeRange.sanitize) instead of failing validation and leaving the value as it
was. A pack whose fog end goes below zero deep underground (14 * view distance - 128 under the
surface) now gets its thickest fog there, as on the real mod, where the cave was left with no fog.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:33:09 -03:00
Ludovic Marie db2b5ff76b Merge pull request #12479 from NikitaMGrimm/hdr-resolve-gone
🤖 Agent: HDR output now takes its gain from Hdr::draw_params instead of the removed mark-resolve lane; the change stays behind HDR, which is off by default, and frame_identity's test covers the no-pack path.
2026-11-08 13:29:59 +01:00
Florian ErnstandClaude Opus 5.5 06fe2cc687 An iron golem reads the reputation of the player it selects
DefendVillageTargetGoal asks the villagers in the golem's box for the
reputation of the player (Villager.getPlayerReputation: the gossips
about that player's UUID). Horizon read one reputation per villager,
keyed by the level's own player, while each golem's goal looks at the
player it selects, often a nearby guest: on a server, a hated guest was
never attacked, and a hated host could turn the golem on a guest it
did not hate. The goal now reads the villager's gossips about the
selected player's UUID.

Fixes #12487

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:27:39 +01:00
Ludovic Marie a9033e8c41 Merge pull request #12483 from RINNECODER/up4/upgrade-biome-effects
🤖 Agent: 1.21.1 biome effects move to 26.3's visual/audio attributes (colours as #rrggbb), carvers become the air list, and liquid carvers are reported as lossy.
2026-11-08 13:26:43 +01:00
Ludovic Marie c3a1e1293f Merge pull request #12478 from glitchsnake/fix/command-components-expected-equal
🤖 Agent: A bare component in an item argument ([custom_data]) now fails with expected '=' as ItemParser's reader.expect does, instead of being ignored; !removals are unchanged.
2026-11-08 13:26:40 +01:00
Ludovic Marie 1e3e77270d Merge pull request #12470 from oloion/perf/client-culling-and-chunk-streaming
🤖 Agent: Frustum select picks the same corner as the branches did (-0.0 >= 0 both ways) and the resident bounds are origin()'s arithmetic inline. I pushed a fix: upload order measured the camera section as floor(y/16) while keys count from the world's bottom, four sections off in the overworld.
2026-11-08 13:26:36 +01:00
Ludovic Marie 138616afdb Merge pull request #12475 from vnsmoreira/packs/polytone-expression-values
🤖 Agent: Pack expressions are compiled at load and evaluated against the frame only when a pack's environment stack is loaded (pack.environments), so the vanilla path sets no frame and keeps folding constant layers.
2026-11-08 13:23:43 +01:00
Florian ErnstandClaude Opus 5.5 1863a04353 Measure a golem's defence against the player it selects (test checkpoint)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:22:56 +01:00
Shreyas 8634b05990 save_roundtrip: normalize relative dat paths to POSIX slashes on Windows (#12287) 2026-11-08 17:51:06 +05:30
Ludovic Marie bf928658ef Merge pull request #12474 from horizon-rust/cg72/loot-mine-named-only
🤖 Agent: Tooling only: loot_mine_location_block asserts its .local/h079/capture is new, so it fails at startup on the Mac runner after one run (#12405); it joins the other capture harnesses in named_only.
2026-11-08 13:20:28 +01:00
Ludovic Marie 8c58505b3c Merge pull request #12456 from martipops/ui/hud-room
🤖 Agent: The room is rebuilt each frame from the boss bars (BossHealthOverlay: y 12, step 19); with 0-1 bar the notice stays at y 18, and HudBatch::new there allocates nothing. I merged main (client_lib re-export list: hud_kit beside main's social).
2026-11-08 13:18:14 +01:00
Ludovic Marie d3151e0cc2 Merge pull request #12418 from RINNECODER/up4/port-item-place-block
🤖 Agent: A block item whose block has another name (seeds) is carried with place_block; one whose block is left out stays in items_unknown. I merged main (both branches added a test at the same spot; both kept).
2026-11-08 13:18:10 +01:00
Vinícius MoreiraandClaude Opus 5.5 2017a3b977 Polytone: read custom particles and particle emitters
Reads what a pack's particles do (wiki "Custom Particle Types", "Block
Properties Modifiers", "Entity Modifiers"): a custom particle type's kind
(custom, semi custom copying a vanilla one, GPU), render type, liquid
affinity, physics and despawn flags, limit, tick interval, exclusion
radius, light level and rotation mode; its initializer and ticker
expressions with their named variables; and the particle emitters that
particles, blocks and entity bones carry (the particle by id or with its
options, chance, count, position, motion, colour, size and customs, the
state predicate, biomes, spawn location, bone, max distance). Every
expression is compiled when the pack loads, so a typo is a load error.
Nothing spawns them yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:16:21 -03:00
Vinícius MoreiraandClaude Opus 5.5 a3b56d6218 Polytone: colormap values in attribute layers
An attribute value or argument can name a Polytone colormap (wiki
"Colormaps"): an image read at the column and row two expressions pick,
each 0 to 1 with 1 the top-left corner, rounded to the nearest pixel by
default, the row scaled by the column when triangular. Without a .json
the axes are the biome's temperature and downfall, which the frame now
passes along. The colour is the modifier's argument whenever the layer
applies; a transparent pixel falls back to the value before the pack (the
entry the modifier replaced), as fallback_colormap's default "original".

Colormaps load once per pack reload, shared by the three dimensions'
stacks, and only those a modifier names, decoded to RGBA ints as on
Java. The biome_id axis isn't supported yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:16:21 -03:00
Vinícius MoreiraandClaude Opus 5.5 30aee13f1d Polytone: an expression value out of range is clamped, not dropped
A number an expression gives outside its attribute's range goes through the range as the sampled
value does (AttributeRange.sanitize) instead of failing validation and leaving the value as it
was. A pack whose fog end goes below zero deep underground (14 * view distance - 128 under the
surface) now gets its thickest fog there, as on the real mod, where the cave was left with no fog.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:16:20 -03:00
sluisr fec26a0044 perf: reuse server States tracking pool and reduce meshing/lighting allocations
- Reuse the pooled EntityState entries in States (`out.add`) across all 21
  object and projectile loops in `lan_entities.rs`, and push vehicle, armor
  stand pose, and arrow metadata directly into `e.meta`.
- Look up vehicle driver mobs via `self.mob_index` in `lan_riding` instead
  of a linear scan over `self.mobs`.
- Compare `play.equipped` by reference before cloning 5 `ItemStack`s on
  every `enter_guest` turn, and reuse `play.craft_grid` via `std::mem::swap`.
- Eliminate per-fluid-cell `Vec::new()` allocations in `mesher/fluid.rs` and
  deduplicate animated fluid layers directly into `out.sprite_layers`.
- Reuse `SectionMesh::packed` capacity and a thread-local scratch buffer in
  `mesher/packed.rs` during `sort_solid`.
- Pack uniform/single-channel lighting from a stack buffer in `chunk.rs`
  (`Light::from_slice`) instead of allocating and dropping a 4 KiB `Box`.
- Check `!self.saves_beside_final` and neighbor readiness before cloning
  `Chunk` in `Streaming::update`.
2026-11-08 13:15:43 +01:00
LokiandAntigravity 9fd2c8c339 items: require '=' for non-removal command components
Java 26.3 `ItemParser.State.readComponents` expects '=' after a component
type unless it begins with '!'. Return an error for bare components rather
than silently ignoring them.

Co-Authored-By: Antigravity <[email protected]>
2026-11-08 21:08:01 +09:00
Ludovic Marie 3f58a9511b Merge pull request #12461 from MSedgeMC/port_map/creative-screen
🤖 Agent: draw_creative, click_creative and creative_tooltip_tabs moved to game/creative_screen.rs on main; the rows follow them.
2026-11-08 13:06:07 +01:00
Ludovic Marie 2d087e4826 Merge pull request #12460 from MSedgeMC/port_map/shear-saddle
🤖 Agent: Game::shear_saddle exists on main and takes a saddle off any saddleable mob, so attemptToShearEquipment is a port row now.
2026-11-08 13:06:03 +01:00
Horizon agentandClaude Opus 5.5 d426f98ae2 parity: run loot_mine_location_block by name only
It writes .local/h079/capture and asserts the capture is new, so every automatic
run on a checkout that ran it once fails at startup (the Mac mini runner).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 13:04:57 +01:00
RINNECODERandClaude Opus 5.5 48f02115ed Port pack: 1.21.1 biomes upgrade their effects to 26.3 attributes and their carvers to a list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:02:45 -06:00
RINNECODERandClaude Opus 5.5 06b5147e56 Port pack: surface rules drop the biomes nothing defines, as 1.21.1 never matched them
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:02:32 -06:00
Ludovic Marie 9fc6abb08b Merge pull request #12468 from kacpherek/fix/server-sword-blocking
🤖 Agent: A consumable or blocks_attacks the server's patch adds (components 24 and 37) now starts a use like the item's own (getUseDuration reads the stack); items with their own consumable are unchanged.
2026-11-08 13:01:32 +01:00
Horizon agentandClaude Opus 5.5 1a9663f21a uploads: measure the camera's section from the world's bottom
Pending upload keys count sections from min_y, so floor(y / 16) put the camera
four sections low in the overworld.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:58:30 +01:00
Ludovic Marie eaca6fe37a Merge pull request #12467 from masa-masa-0314/fix/realms-terms
🤖 Agent: Realms answers 6002 for a new account: RealmsTermsScreen shows (Agree posts /mco/tos/agreed, then the join resumes; Disagree goes back), with vanilla aka.ms/MinecraftRealmsTerms and mco.terms.* keys.
2026-11-08 12:53:12 +01:00
Vinícius MoreiraandClaude Opus 5.5 c52446638a Polytone: expression values in attribute layers
A pack's attribute value or argument can be a scripting expression
(wiki "Environment Attributes"): fog that closes in with the rain and
with depth, a cloud height scaled from the value the layers before gave.
Each one is compiled when the pack loads and evaluated whenever its layer
applies, against what the frame sets: g (rain, time, day time, sky type,
dimension, render distance, moon phase), p (position, in water), c
(position, view distance in blocks), o (the camera's position, sky and
block light), r (random), v (the value so far, colours packed ARGB) and
the pack's config(id). Its result is the modifier's argument; one that
fails leaves the value as it was. A dynamic entry is never folded into
the base value, since it changes every frame.

Expressions are compiled with Arc instead of Rc so a pack's stacks can be
built on the loading worker.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:51:58 -03:00
Ludovic Marie e8acedb5c2 Merge pull request #12401 from Web-mc1/studio-screen-bridge
🤖 Agent: Screen::Studio is opened only by `studio screen open` and draws nothing itself (no chat behind it); Escape closes it. I pushed the rustfmt fix verify failed on.
2026-11-08 12:50:37 +01:00
Horizon agentandClaude Opus 5.5 a0d1a9f0e3 Merge origin/main into up4/port-item-place-block
Both branches added a test before the Windows-separator one: both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:48:19 +01:00
Ludovic Marie 4b2cba530a Merge pull request #12451 from apeirs/fix/fxc-entity-written-color
🤖 Agent: FXC (DX12) failed on discards one call below the entry point; fs_entity now computes entity_color itself and hands it on, same colour on every backend.
2026-11-08 12:48:16 +01:00
Ludovic Marie 4b99e9a701 Merge pull request #12441 from RINNECODER/up3/far-tile-walk
🤖 Agent: Boxing the far tile meshes shrinks Tile for the per-frame walk; release paths unbox each mesh once, nothing double-released.
2026-11-08 12:48:12 +01:00
Horizon agentandClaude Opus 5.5 4497f18b31 Merge origin/main into ui/hud-room
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:47:26 +01:00
Ludovic Marie e761e6394a Merge pull request #12463 from MSedgeMC/port_map/edge-walk
🤖 Agent: VoxelShape::edges exists in horizon-core's shape.rs, so forAllEdges is a port now rather than skip:data.
2026-11-08 12:45:18 +01:00
Ludovic Marie bced9e4ecd Merge pull request #12462 from MSedgeMC/port_map/watcher-home
🤖 Agent: PackSelectionScreen.Watcher spans datapack.rs::folder_stamp and options_ui.rs's watch, so a home row fits better than a single port target.
2026-11-08 12:43:01 +01:00
Ludovic Marie 4dd22ecd6a Merge pull request #12459 from MSedgeMC/port_map/infested-silverfish
🤖 Agent: spawn_infested, player_infested and mobs.rs::infested_silverfish all exist on main; the rows now name where the roll and the spawn happen.
2026-11-08 12:38:53 +01:00
Ludovic Marie ba041dc3bb Merge pull request #12439 from RINNECODER/up3/pack-halflives
🤖 Agent: Pack half-lives replace OptiFine's defaults in the same units (600/200/10/1). I pushed a scripted-clock version of its test: backdating u.last was the wall-clock flake #12354 removed.
2026-11-08 12:34:49 +01:00
Ludovic Marie 706aad56c0 Merge pull request #12455 from Evan-lhm/fix/release-sums-windows-test
🤖 Agent: Git Bash's sha256sum writes `*name` (binary mode); the test strips the star, nothing else changes.
2026-11-08 12:29:15 +01:00
Ludovic Marie 564f031294 Merge pull request #12445 from RINNECODER/up3/lod-dispatch-incremental
🤖 Agent: Mesh jobs' samples are kept per 8x8 shard and synced at every active/ready change; debug builds assert the copy matches active ∩ ready.
2026-11-08 12:29:12 +01:00
Ludovic Marie f18c761edc Merge pull request #12443 from RINNECODER/up3/mod-content-copy
🤖 Agent: Plain files are copied with the same size and path checks read() made; tags, atlases, declarations, biome modifiers, worldgen and structures are still read and parsed.
2026-11-08 12:29:08 +01:00
Ludovic Marie c2661a333d Merge pull request #12410 from masa-masa-0314/feat/friends-service
🤖 Agent: The friends worker never holds the lock across HTTP (reads the etag, unlocks, fetches, relocks), and nothing starts unless the account's FRIENDS_ENABLED flag is set.
2026-11-08 12:29:05 +01:00
Ludovic Marie c09321478d Merge pull request #12408 from m-uvex/pluto/add-flake
🤖 Agent: The flake builds the launcher from the repo itself, so it follows main, unlike #11200's pinned tarball; both outputs evaluate on NixOS (horizon-launcher-0.2.0.drv, nix-shell.drv); CI does not build it.
2026-11-08 12:29:01 +01:00
Horizon agentandClaude Opus 5.5 519d9b6d97 persist: ignore the second summon UUID test without prepared resources
It unwraps HORIZON_TEST_RESOURCES like its sibling, which is already #[ignore]d.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:27:29 +01:00
TheMrZZandCodex gpt-6.1-sol fe3ebc3b8d Tick lodestone compasses equipped by armor stands
Reuse the existing loaded-target predicate for actual stand equipment inside
the entity-ticking guard. Preserve player behavior and bounded tracker rules.

Fixes #12395

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-08 12:26:24 +01:00
MSedgeMC 98e37bbb84 port_map: the edge-walk rows name VoxelShape::edges
forAllEdges and forAllAxisEdges were skipped as data, but the walk is in
crates/horizon-core/src/geometry/shape.rs: VoxelShape::edges is documented
as forAllEdges, and its inner axis scan is forAllAxisEdges.
2026-11-08 11:24:22 +00:00
Kacper 7965bc432e Merge pull request #12447 from RINNECODER/up3/mount-tick-order
Riding: the driven mount ticks before its rider, on the input the rider's last tick applied
2026-11-08 12:24:13 +01:00
Kacper 50f53a5a2b Merge pull request #12437 from RINNECODER/up3/mount-air-speed
Mounts: a player-steered mount moves through the air at a tenth of its speed (LivingEntity.getFlyingSpeed)
2026-11-08 12:24:06 +01:00
Kacper 113ac28e79 Merge pull request #12426 from RINNECODER/up4/port-food
Port pack: food items carry over, as 26.3's food, consumable and use_remainder
2026-11-08 12:23:59 +01:00
Kacper 509deae34e Merge pull request #12424 from RINNECODER/up4/upgrade-weird-scaled-sampler
upgrade-datapack: weird_scaled_sampler becomes 26.3's interval_select of scaled noises
2026-11-08 12:23:52 +01:00
Kacper 156f335169 Merge pull request #12422 from RINNECODER/up4/upgrade-nether-vegetation
upgrade-datapack: nether_forest_vegetation becomes 26.3's spread simple_block
2026-11-08 12:23:45 +01:00
Kacper f4cbc6311e Merge pull request #12416 from RINNECODER/up4/port-farmland-template
Port pack: carried models on 1.21.1's farmland template use 26.3's indented cube
2026-11-08 12:23:38 +01:00
MSedgeMC 7a4012d00f port_map: PackSelectionScreen.Watcher is a home row
The class row was written with a `port` verdict and the function that
represents it. Class rows take `home` with the files that represent the
class; list datapack.rs (folder_stamp) and options_ui.rs (watch_data_packs).
2026-11-08 11:23:32 +00:00
Kacper dca8dbddd1 Merge pull request #12414 from RINNECODER/up4/port-recipe-conditions
Port pack: recipes with NeoForge conditions are upgraded when NeoForge loaded them
2026-11-08 12:23:31 +01:00
Kacper 6410d3f446 Merge pull request #12412 from RINNECODER/up4/port-tag-namespaces
Port pack: tags alone claim no namespace
2026-11-08 12:23:24 +01:00
Kacper 059eb482c4 Merge pull request #12400 from Solenopsisbot/perf/obstacle-grid-scratch
perf: reuse obstacle grid query scratch
2026-11-08 12:23:13 +01:00
MSedgeMC 3835921949 port_map: the creative screen rows name game/creative_screen.rs
The creative inventory screen moved out of game.rs into its own module.
draw_creative, creative_tooltip_tabs and click_creative are unchanged; only
the module path in the six rows that name them was left behind.
2026-11-08 11:23:10 +00:00
Kacper f8be508301 Merge pull request #12397 from Solenopsisbot/perf/creature-path-writes
perf: move creature path writes without copying
2026-11-08 12:23:06 +01:00
Kacper 8b06e4ba1e Merge pull request #12386 from TheMrZZ/fix/stand-command-hurt
/damage breaks, lights or burns an armor stand as its hurt does
2026-11-08 12:22:59 +01:00
Kacper 197a6ef7dc Merge pull request #12381 from svetch/discovery-texture-uris
parity: take valid texture URIs from the services discovery document
2026-11-08 12:22:52 +01:00
Kacper ba54ecff05 Merge pull request #12375 from TheMrZZ/fix/test-run-cross-dimension-coordinate-feedback
fix: send completed test coordinate feedback to its command source
2026-11-08 12:22:45 +01:00
MSedgeMC 44a27421ca port_map: the equipment shearing rows name shear_saddle
`Game::shear_pig_saddle` was renamed and generalized to `Game::shear_saddle`
when shears began taking the saddle off any saddleable mob. Its doc names
`Mob.attemptToShearEquipment` and `Mob.shearItem`; re-point the two rows.
2026-11-08 11:22:40 +00:00
Kacper eb8f8e38b5 Merge pull request #12374 from TheMrZZ/fix/h064-vibration-source-uuid
Save the source and projectile owner UUIDs of a vibration
2026-11-08 12:22:38 +01:00
Kacper d276c60075 Merge pull request #12371 from glitchsnake/fix/11382-vehicle-entity-random
game: use entity random for boat, minecart and item frame operations (fixes #11382)
2026-11-08 12:22:31 +01:00
Kacper ee91d5907a Merge pull request #12366 from vnsmoreira/packs/polytone-environment
Polytone: biome and dimension modifiers as attribute layers
2026-11-08 12:22:24 +01:00
Kacper 1cec3b3751 Merge pull request #12365 from vnsmoreira/packs/polytone-overlays
Polytone: pack configs and conditional overlays
2026-11-08 12:22:17 +01:00
MSedgeMC 04ead01cb4 port_map: the infested hurt rows name spawn_infested and player_infested
`Game::infested_hurt` was split. `Game::spawn_infested` spawns a silverfish
per drawn velocity at half the bearer's height with the hurt sound, and
`Game::player_infested` is the player's roll and spawn. Re-point the two
rows that still name the old function.
2026-11-08 11:22:13 +00:00
Kacper 447341b6a4 Merge pull request #12364 from vnsmoreira/attributes/integration
Environment: sample the sky through the vanilla attribute stack
2026-11-08 12:22:10 +01:00
Kacper 2bb7df1bf9 Merge pull request #12352 from k3yh/potion-contents-names
A potion or tipped arrow is named by its contents
2026-11-08 12:22:02 +01:00
Kacper 13c56dd065 Merge pull request #12345 from Vaspyyy/title-icon-tooltips
Show the title screen icon buttons' tooltips while hovered like vanilla
2026-11-08 12:21:55 +01:00
Kacper cceab3b2b5 Merge pull request #12344 from Vaspyyy/shulker-bullet-parity
Fire shulker bullets from the box centre and turn them at standable blocks
2026-11-08 12:21:48 +01:00
Kacper 6e00e1bbf1 Merge pull request #12341 from Vaspyyy/vehicle-container-scatter
A destroyed chest boat or container minecart scatters its contents like vanilla
2026-11-08 12:21:40 +01:00
Kacper fc14c6fc28 Merge pull request #12336 from k3yh/loot-give-full-inventory
/loot give to a full inventory throws nothing out
2026-11-08 12:21:33 +01:00
Kacper d03fbd593a Merge pull request #12260 from Akirabane/fix/9335-passengers
Riders turn like vanilla's: vehicle body only where vanilla copies it
2026-11-08 12:21:26 +01:00
Kacper 0335218910 Merge pull request #11682 from TheMrZZ/fix/loot-mine-location-block-state
Evaluate loaded block-state offsets in loot-mine location checks
2026-11-08 12:21:19 +01:00
Kacper 3b76fcb951 Merge pull request #11642 from TheMrZZ/fix/inline-armor-trim-holders
Preserve fully inline armor-trim holders in item stacks
2026-11-08 12:21:11 +01:00
masa-masa-0314andZCode a7f89f1c62 Realms: the terms screen before a new account's first join
A new account's first join gets error 6002; GetServerDetailsTask shows
RealmsTermsScreen instead of the generic error. Port it:

- The title, the first sentence and the underlined terms link at
  row(5), with the hover colours and the link's measured hit box
  (extractRenderState's onLink). A click opens
  CommonLinks.REALMS_TERMS (https://aka.ms/MinecraftRealmsTerms).
- Agree (width/4) and Disagree (width/2 + 4), each width/4 - 2 wide at
  row(12). Agree posts mco/tos/agreed (RealmsClient.agreeToTos) on a
  worker, then runs GetServerDetailsTask again from the same screen;
  a failure logs and stays. Disagree and Escape return to the screen
  that started the join (GetServerDetailsTask.lastScreen, now carried
  by Connecting).

Co-Authored-By: ZCode <[email protected]>
2026-11-08 19:21:06 +08:00
Kacper 73b2d7434f Merge pull request #11588 from TheMrZZ/fix/named-predicate-context
Resolve named predicate references through the Game context
2026-11-08 12:21:04 +01:00
Kacper e19018da1c Merge pull request #11564 from TheMrZZ/fix/h157-player-save-projection
fix: project live player motion fields into saved data
2026-11-08 12:20:57 +01:00
Kacper 5a94e4c188 Merge pull request #11488 from TheMrZZ/fix/h181-noai-trader-llama-delay
Advance eligible TraderLlama despawn delay with NoAI
2026-11-08 12:20:51 +01:00
Kacper 6db9351111 Merge pull request #11301 from TheMrZZ/fix/h052-vehicle-container-save
Respect class-owned container fields in vehicle saves
2026-11-08 12:20:43 +01:00
Horizon agentandClaude Opus 5.5 7d2f225c85 pack uniforms: drive the half-life test with a scripted clock
Backdating u.last measured the gap with the wall clock; a few ms of runner delay
moved wetness past the 1e-3 tolerance (the flake #12354 fixed in the Iris test).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:12:58 +01:00
Horizon agentandClaude Opus 5.5 f15f153836 remote chunks: drop block-entity attachments after the column's block entities load
Moving the retain before the block entities loaded dropped every attachment of a
resent column, and skipping it for new columns kept those of forgotten ones.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:12:19 +01:00
JoansitohandClaude Sonnet 5.5 c9f62a1572 Trident: a stuck trident falls when its block is destroyed
AbstractArrow.tick: if lastState != blockState && shouldFall() startFalling().

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-08 12:11:48 +01:00
LokiandAntigravity 067a6a1db1 game: skip in-tree duplicate UUIDs in summoned passenger trees (fixes #11807)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 20:11:37 +09:00
Horizon agentandClaude Opus 5.5 7ba6ef0912 studio screen: rustfmt the control replies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 12:11:35 +01:00
Kacpherek 34c91f0c34 Hold a server-patched consumable or blocks_attacks use on a server
A server that adds blocks_attacks and a long consumable to a sword (1.8
style sword blocking, #11817) sends both components in the stack patch,
which the client reads past. The client never started the use itself and
remote_tick_using dropped the use the server's living flags started on
the next tick, so the right click kept restarting the use and the input
slowdown flickered (GrimAC NoSlow).

Read the added components from the wire patch, as Item.use does from the
stack's components: Consumable.startConsuming / startUsingItem start the
use, Item.getUseDuration takes the consumable's consumeTicks (else 72000
for blocks_attacks), and the use is held until the button lets go or the
server stops it, as LivingEntity.updateUsingItem never completes a use on
the client (LocalPlayer.onSyncedDataUpdated keeps it running).
2026-11-08 12:10:32 +01:00
Evan-lhmandClaude Opus 5.5 0f36e33ccf Tests: read SHA256SUMS names with Git Bash's binary-mode * (#12454)
On Windows, Git Bash's sha256sum writes `<digest> *<name>`, so the
optional release assets test kept the `*` in every name and failed for
all four masks. Both forms are valid sha256sum output; accept them.

Fixes #12454

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 11:59:54 +01:00
apeirs 94e2918668 DX12: keep entity_color's discards one call below fs_entity, so FXC compiles it
Since #12282, fs_entity called entity_written_color, which called entity_color.
FXC (every DX12 game: Horizon ships no dxcompiler.dll) fails on that with
"internal error: argument pulled into unrelated predicate" while creating the
first entity pipeline, and the game crashes at start. entity_written_color now
takes entity_color's result instead of calling it.
2026-11-08 11:46:09 +01:00
m_uvexandPluto ecc9a21e18 packaging: build Horizon launcher from source in flake
Addresses review feedback from #11200:
- Builds horizon-launcher from repository source via rustPlatform.buildRustPackage
  instead of fetching a pinned binary tarball by hash.
- Native build support for both x86_64-linux and aarch64-linux.
- Added essential POSIX tools to devShells.default for pr_ready test suite.

Co-Authored-By: Pluto <[email protected]>
2026-11-08 13:41:26 +03:00
TaquitoSuabe 00127a51c7 remote_chunks: decode chunk packets concurrently with strict in-order retirement
- Parallelize chunk decompression across a bounded worker pool while enforcing sequential retirement (FIFO arrival order) via monotonic sequence ordering.
- Filter resident neighbors and batch section dirtying with column masks.
- Skip attachment and block entity cleanup scans for fresh (non-resident) chunks.
- Enable TCP_QUICKACK on Linux streams and reset empty reader buffers to prevent drift.
- Elevate network worker thread QoS on supported platforms.
2026-11-08 03:40:59 -07:00
Ludovic Marie 92fd45976d Merge pull request #12393 from Solenopsisbot/perf/worldgen-decoration-biomes
🤖 Agent: Same fixed [u64; 128] bitset with spill as the generator's (#12346): spilled words keep index 128 + offset, so the decorated chunk's biomes stay ascending, and the exact reserve avoids regrowth.
2026-11-08 11:39:18 +01:00
SolenopsisbotandCodex 3bf71f9926 perf: reuse obstacle grid query scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 21:33:40 +11:00
Web-mc1 df92a7cbc5 Open and close Screen::Studio from horizon via studio screen, with test 2026-11-08 23:29:06 +13:00
Web-mc1 b7215560ba Add Screen::Studio so Studio windows don't borrow the chat slot 2026-11-08 23:28:48 +13:00
Ludovic Marie 79bd301973 Merge pull request #12388 from kacpherek/fix/remote-push-tick-order
🤖 Agent: ClientLevel.tickEntities ticks in add order: after login (and after a respawn re-adds the player last) a mob added later pushes from where both ended the tick, so the push reaches the player's next move, which is what GrimAC's Simulation check expects.
2026-11-08 11:27:31 +01:00
SolenopsisbotandCodex ec73a1af84 perf: move creature path writes without copying
Co-Authored-By: Codex <[email protected]>
2026-11-08 21:24:37 +11:00
Horizon agentandClaude Opus 5.5 bbbbfa6dd2 Parity: unused_feature_startup runs by name only
It needs --kind, --output and --binary, which an automatic parity run
doesn't pass (#12372).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 11:23:29 +01:00
Ludovic Marie 33facd006a Merge pull request #12361 from adiraj66132/security/audit-hardening
🤖 Agent: client_mod_command is the chat-side mod hook; the SMP and other server mods use native_perform_command, so gating guests (lan_acting) out of it removes a level-0 path without touching server commands. The 4 GiB zip budget is per opened archive, and every Zip lives for one load.
2026-11-08 11:22:13 +01:00
Ludovic Marie 0ad6f93762 Merge pull request #12280 from NikitaMGrimm/portal-ticket-persistence
🤖 Agent: PORTAL tickets are saved in chunk_tickets.dat with level and ticks left, reload on restart (prepareLevels), and do not expire while their chunk is still loading (canTicketExpire). I fixed the main merge: #12232's ticket fast path compared (chunk, level) pairs with this PR's PortalTicket (LAND-STOP: E0308); it compares (ticket.chunk, ticket.level) now.
2026-11-08 11:22:09 +01:00
SolenopsisbotandCodex 1467568676 perf: reuse decorated biome discovery scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 21:18:11 +11:00
masa-masa-0314andZCode da428abd4d Friends: use ? for the presence request's missing session and endpoint
Co-Authored-By: ZCode <[email protected]>
2026-11-08 18:15:47 +08:00
Ludovic Marie 02deae8774 Merge pull request #12377 from Solenopsisbot/perf/station-defaults-reuse
🤖 Agent: Smithing results and banner defaults now share the shared() snapshot StationData loads once; the standalone loaders keep reading their own.
2026-11-08 11:14:56 +01:00
Ludovic Marie 90267f9863 Merge pull request #12359 from Solenopsisbot/perf/items-enchant-defaults
🤖 Agent: item_defaults::shared is the cached item_defaults::read, so the enchantment properties read the same snapshot Items already decoded; the standalone loader still reads its own.
2026-11-08 11:12:04 +01:00
martipopsandClaude Opus 5.5 88dd9e8d1e Document the HUD kit's Room and Plate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:09:02 -05:00
martipopsandClaude Opus 5.5 8ce5ba0e52 Keep the controller notice and the splitscreen join strip off the boss bars (#12342)
Game::hud_room gathers the boss bars the HUD shows each frame; hud empties the room every
frame first. The notice and the join strip go below the bars when they would cover them,
and stay at y 18 otherwise.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:09:02 -05:00
martipopsandClaude Opus 5.5 9fb2965c3a HUD kit: Room keeps Horizon's overlays below the boss bars, and Plate
hud_kit::room holds a view's keep-outs, today the boss bars, and tells a centred overlay
where it goes: where it wanted unless the bars reach past it. boss_bars_bottom follows
BossHealthOverlay's layout. hud_kit::plate draws the notice's box.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:09:01 -05:00
Kacpherek 5c616363b1 Push the local player by mobs in ClientLevel's tick order
Vanilla's ClientLevel ticks entities in the order they were added: after
login the player comes first, so a mob added later pushes it from where
both ended the tick, and the push reaches the player's next move. After a
respawn the new player is added after what the level holds, so those
entities tick before it. Horizon applied every mob's push before the
player's move, from where the mobs had just moved: walking into a mob
the server is pushing away tripped anticheats (GrimAC Simulation 0.02-0.04
on every push).
2026-11-08 11:01:33 +01:00
SolenopsisbotandCodex 58140be02b fix: satisfy station defaults lint
Co-Authored-By: Codex <[email protected]>
2026-11-08 21:00:32 +11:00
SolenopsisbotandCodex 4fab87e3ce perf: reuse station item defaults
Co-Authored-By: Codex <[email protected]>
2026-11-08 21:00:32 +11:00
Ludovic Marie d61235cb39 Merge pull request #12370 from Solenopsisbot/perf/stream-decoration-scratch
🤖 Agent: The 9-key array and Box<[Proto; 9]> build the same region in the same order; the removed try_into fallback could never fail (always 9 keys).
2026-11-08 10:59:28 +01:00
Ludovic Marie 38d670c0ac Merge pull request #12379 from Solenopsisbot/perf/block-tick-scratch
🤖 Agent: The spawner and trial-spawner lists keep their tick order and are taken around the client effects, so a client tick can't see a half-filled list.
2026-11-08 10:59:24 +01:00
Horizon agentandClaude Opus 5.5 e5cad94537 Zip budget test: pass the budget by shared reference
clippy (-D warnings): read_within takes &AtomicU64, so &mut was needless.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:56:41 +01:00
Ludovic Marie 1c17df10b4 Merge pull request #12367 from Solenopsisbot/perf/remote-tick-scratch
🤖 Agent: Indexing the retained buffers by mob index is safe: nothing between the fluid loop and the rain loop adds or removes remote mobs, and an early return only loses the warm capacity.
2026-11-08 10:55:57 +01:00
Ludovic Marie d1b5000930 Merge pull request #12360 from Solenopsisbot/perf/outline-scratch
🤖 Agent: The outline strokes keep their order (black then bright) and the custom PvP colour; both scratch vectors are bounded by MAX_OUTLINE_EDGES, so the retained capacity stays small.
2026-11-08 10:55:53 +01:00
Florian ErnstandClaude Opus 5.5 83d9347f4b Merge origin/main into fix/h259-unused-feature-startup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:55:15 +01:00
SolenopsisbotandCodex 0dab8b30f5 perf: reuse item defaults for enchantments
Co-Authored-By: Codex <[email protected]>
2026-11-08 20:53:56 +11:00
masa-masa-0314andZCode 5740311a36 Friends: the service's empty answer reads as three empty lists
The live getFriends answer an account with no friends gets is
{"empty":true,"friends":[],"incomingRequests":[],"outgoingRequests":[]};
the parser reads the three lists and ignores the flag. Recorded as a
regression case beside the populated answer.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 17:53:30 +08:00
Florian ErnstandClaude Opus 5.5 17f3a0ff81 /damage breaks, lights or burns an armor stand as its hurt does
ArmorStand.hurtServer returns false for an explosion, fire that ignites
armor stands and burning, after it has acted on the stand: a blast breaks
it (its equipment pops, then it is killed), in_fire lights a stand that
is not burning for 5 seconds, and on_fire burns 4 health off one with
more than 0.5. The command route had no armor stand arm, so /damage left
the stand as it was. It now runs those sources with the stand's guards
and still fails as vanilla's command does.

Part of #12380

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:53:20 +01:00
Horizon agentandClaude Opus 5.5 c0f9600544 Parity: run_coordinate_feedback runs by name only
It needs --binary, --work-dir and --reference-dir, which an automatic
parity run doesn't pass (#12372).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:50:57 +01:00
VaspyyyandClaude Opus 5.5 f1be49b7df Lid pushes follow vanilla's tick order, and boats reach an open lid
- BoxReach (the boats' and carts' mob pre-filter) is worked out from each
  mob's real box, so a shulker whose open lid grows its box sideways or
  below its position is still found: a boat runs into the lid and picks
  the shulker up, as in vanilla.
- ArmorStand::shove makes up for Horizon ticking stands before mobs: a
  stand that joined after its shulker gets vanilla's push-then-travel
  ground and speed, one that joined before gets Entity.move's own.
- /data merge keeps a mob's and a stand's join order (Entity.load changes
  the entity in place).
- A shulker a boat or a minecart picks up turns to AttachFace DOWN
  (Shulker.startRiding).
- The stand's box uses the float height 1.975F, canStayAt the shulker's
  scale.
- The harness adds wall, ceiling and squeezed lids, with the stand joined
  after and before its shulker, and the boat that runs into an open lid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:47:00 +01:00
VaspyyyandClaude Opus 5.5 a1ecb7f6de An opening shulker lid lifts the armor stand on it
Shulker.onPeekAmountChange moves every entity with physics in the lid's
path with Entity.move(MoverType.SHULKER), armor stands included, and
Shulker.makeBoundingBox grows the shell's box by the lid's physical peek.
Horizon pushed only the player and mobs and sized the box from the
dimensions, so a stand resting on a normal shulker sank into it the first
time the lid opened.

- The lid sends MobEvent::LidPush; ArmorStand::shove moves each stand with
  physics through the collision resolver. Mob::bounds_at gives a shulker
  its makeBoundingBox, scaled and grown by getPhysicalPeek in float.
- Shulker.tick's own part (keep a face, ease the lid) runs without AI
  too, and not for a shell that rides something (isPassenger).
- /data merge keeps a shulker's eased currentPeekAmount: Entity.load reads
  only the raw Peek.
- A stand leaves out the boxes of mobs riding it
  (isPassengerOfSameVehicle), so a stand carrying a /ride shulker rises.
- ObstacleGrid::near tests each box once when the query spans more cells
  than there are boxes, and a stand's loaded Motion drops a component past
  10 (Entity.load), so a huge Motion no longer stalls the server.
- A rider sits on a stand's float passenger point (1.975F widened).
- The harness adds a stand carrying a riding shulker and the lid opening
  and shutting under the first stand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:47:00 +01:00
VaspyyyandClaude Opus 5.5 6880103394 Armor stands land on and stop at shulkers and boats like vanilla
ArmorStand.travel moves with Entity.move, whose Entity.collide adds
EntityGetter.getEntityCollisions: the entities that canBeCollidedWith the
stand (a living shulker, a boat, a happy ghast held still) stop it as
blocks do. Horizon resolved a stand's move against blocks and the world
border only, so a stand dropped onto a shulker sank into it, one dropped
onto a boat fell through to the ground, and one pushed into a shulker
went through it (#11877).

The stand tick now gathers those boxes once, when the first stand
travels (the same set that stops a dropped item, primed TNT or a falling
block), and passes the ones meeting each stand's box stretched by its
movement into the move. It runs in the server's tick, so single player,
LAN hosts and dedicated servers all get it.

The new stand_entity_collision harness drops stands onto a NoAI shulker,
a boat and the stone, slides one into a shulker's side, and compares
their Pos, Motion, OnGround and fall_distance with vanilla's tick by
tick. Without the fix 27 of its 198 records differ.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:47:00 +01:00
RINNECODERandClaude Opus 5.5 4d5de18e40 Riding: the driven mount ticks before its rider (ClientLevel.tickNonPassenger) on the input the rider's last tick applied (xxa/zza through modifyInput); it moved a tick ahead of the server's view (GrimAC Simulation on a horse, 27 flags)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:41 -06:00
RINNECODERandClaude Opus 5.5 340d4d8b22 Mounts: LivingEntity.getFlyingSpeed, a player-steered mount moves through the air at a tenth of its speed, not 0.02 (a horse's jump)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:41 -06:00
RINNECODERandClaude Opus 5.5 d3c183549e shader packs: honour wetnessHalflife, drynessHalflife, eyeBrightnessHalflife, centerDepthHalflife
Packs set these const directives (ShaderDoc, OptiFine's shaders.txt);
Horizon always used the defaults (600/200/10/1 ticks). IterationT sets
wetnessHalflife 200 and drynessHalflife 50, so under Horizon its sky
stayed sunny for a minute after rain started (wetness rose 3x too
slowly) and stayed wet 4x too long after it stopped. The pack log now
names the half-lives a pack sets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:41 -06:00
RINNECODERandClaude Opus 5.5 f711009645 Far tiles: an FxHashMap, not SipHash, for the renderer's tiles
draw_frame looks every visible tile up 3-4 times a frame and upload_lods walks them all; with
lodDistance 256 (~8.5k tiles) std's SipHash was 8-10% of the main thread.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:40 -06:00
RINNECODERandClaude Opus 5.5 9cde3d1f9a Far tiles: keep mesh jobs' samples as tiles change, not gathered per batch
`Lod::dispatch_meshes` collected every active tile's ready samples into a new
map for each batch of mesh jobs (thousands of entries, rehashed as it grew):
5-10% of the main thread in flight. The map is now kept up to date where a
tile turns active or inactive and where its samples change
(`Lod::sync_sample`), in shards of 8 by 8 tiles behind `Arc`s: a batch copies
the shards' handles, and a change copies only its shard while a job still
reads it. Debug builds check it against the full gather on every dispatch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:40 -06:00
RINNECODERandClaude Opus 5.5 c7c143cbf1 Mod content: copy plain files into the overlay instead of reading and rewriting them
Building a mod set's resource root read every content file and wrote it back out; with the
14 Popular Mods (Chipped's 10k textures) that was about two thirds of a 37 s first start
(sample: open/read/write on the main thread). Files the build does not parse are now copied
with fs::copy (a clone on APFS), checked as reading them was (name, 16 MiB limit).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:40 -06:00
RINNECODERandClaude Opus 5.5 cd313d031f Far tiles: box a tile's meshes so the per-frame tile walk stays small
draw_frame tests every far tile against the frustum each frame. A Tile held two SectionGpu
inline (~0.5 KB), so with lodDistance 256 (~8.5k tiles) the walk read ~9 MB a frame and was
the main thread's top cost (17-23% in sample). Boxed, a tile is ~90 bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:40 -06:00
TheMrZZandCodex 9bf65b95b4 Validate unused fill-layer entries before world readiness
Partial H259; retain other registry validation as open scope.

Co-Authored-By: Codex <[email protected]>
2026-11-08 10:46:38 +01:00
RINNECODERandClaude Opus 5.5 4efbd5ea91 Port pack: 1.21.1 weird_scaled_sampler densities upgrade to 26.3's interval_select of scaled noises
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:56 -06:00
RINNECODERandClaude Opus 5.5 09d0f6eb7c Port pack: 1.21.1 nether_forest_vegetation upgrades to 26.3's spread simple_block
As 26.3 wrote crimson and warped forest vegetation: a simple block on nylium,
tried spread_width² times at a trapezoid offset, where there is air. Terralith's
last feature left as it was.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:56 -06:00
RINNECODERandClaude Opus 5.5 ebec6fdd1c Port pack: 1.21.1 random_patch and flower features upgrade to 26.3's counted, offset placement
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:56 -06:00
RINNECODERandClaude Opus 5.5 dabd3c04cc Port pack: an item that places a block of another name (seeds) binds that block
A BlockItem whose block has another name (Farmer's Delight's cabbage seeds
place `cabbages`, Supplementaries' way signs `way_sign_wall`) was left out
as "a block item without a block": 35 items over the 8 mods. It is now
carried with `place_block` naming its block when that block is carried
(vanilla's or one of the mod's declared ones), and left out as "its block
is left out" otherwise. That check looked the item's own name up in the
left-out blocks instead of its block's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:55 -06:00
RINNECODERandClaude Opus 5.5 cec040fef1 Port pack: food items carry over, as 26.3's food, consumable and use_remainder
Food was left out ("no food template yet"): 82 items over the eight mods tried, most of Farmer's
Delight. 1.21.1's minecraft:food held what 26.3 splits into food (numbers), consumable (eating
time, on_consume_effects: one apply_effects per effect with its own chance) and use_remainder
(the bowl), as 26.3's golden apple and mushroom stew have them. NeoForge's cures are dropped, and an
effect of the mod's own is left out and reported (the game would refuse the item).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:55 -06:00
RINNECODERandClaude Opus 5.5 a2d300a03f Port pack: carried models on 1.21.1's farmland template use 26.3's indented cube
26.3 has no `block/template_farmland`: its farmland model is
`template_cube_bottom_top_indented` with `bottom`, `side` and `top`, where
1.21.1's template read `dirt` for the bottom and sides. A carried model on
the old template (Supplementaries' raked gravel) now names the new one with
its dirt as side and bottom, instead of drawing the missing model.

`minecraft:block/rope` (Supplementaries' bunting particle) is in neither
1.21.1 nor 26.3: not a rename, left as is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:55 -06:00
RINNECODERandClaude Opus 5.5 b2e0010655 Port pack: recipes with NeoForge conditions are upgraded when NeoForge loaded them
A vanilla-type recipe with neoforge:conditions was kept as 1.21.1 wrote it, and 26.3 refused its
old ingredients: Quark's 1,466, most of Supplementaries'. The oracle's recipes are the ones whose
conditions passed (a mod's own conditions, such as config flags, are only known there): those
lose the conditions and are upgraded; the others go outside, as NeoForge left them out. Without
the oracle, NeoForge's own conditions are read against the mods ported along.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:55 -06:00
RINNECODERandClaude Opus 5.5 d0815a9d0b Port pack: tags alone claim no namespace (Farmer's Delight and Supplementaries both added to create's tags and clashed)
A mod's tag additions under another mod's namespace (data/create/tags/...) made it claim that namespace
when its own files named it; two such mods in one pack then refused to load together (Popular Mods:
Supplementaries off). Tags alone now stay outside the carry, as other compat files do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:55 -06:00
Florian Ernst 217affdc9e Merge origin/main into fix/h064-vibration-source-uuid
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/sculk.rs
2026-11-08 10:44:34 +01:00
Florian ErnstandClaude Opus 5.5 5674b63b57 Measure /damage side effects on armor stands (test checkpoint)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:42:38 +01:00
Florian Ernst 255d45aa56 Merge origin/main into fix/test-run-cross-dimension-coordinate-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/test_runs.rs
#	crates/horizon-client/src/game/game_tests.rs
2026-11-08 10:42:37 +01:00
SolenopsisbotandCodex 5f599617e6 perf: reuse block entity tick scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 20:37:03 +11:00
Ludovic Marie 668555dd33 Merge pull request #12376 from horizon-rust/ci/command-exec-dead-flag
🤖 Agent: Main fix: three lines a union merge brought back read a flag #12242 removed, so every command_exec-driven harness stopped with AttributeError (80 of 173 on #12344's run). Nothing else in the driver uses an undefined args attribute; the corpus builds its 161 groups.
2026-11-08 10:36:50 +01:00
Horizon agentandClaude Opus 5.5 db09adeca9 command_exec harness: drop the seed-sync gaps block a merge brought back
#12242 removed --seed-sync-gaps-diagnostic and the block that read it.
The union merge that landed TheMrZZ's series (#12160) kept the block from
an older base without the flag, so every command_exec run (and every
harness that drives it) stopped at
AttributeError: 'Namespace' object has no attribute 'seed_sync_gaps_diagnostic'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:35:08 +01:00
Román Benjámin e46c586cec parity: take valid texture URIs from the services discovery document 2026-11-08 10:34:11 +01:00
Ludovic Marie 1bc5012f4f Merge pull request #12372 from horizon-rust/ci/named-only-capture-harnesses
🤖 Agent: Tooling only: 17 capture-preserving harnesses that fail at startup under parity.py (no --work-dir / a capture folder that must be new) move to named_only; they still run by name and in the nightly --all audit.
2026-11-08 10:33:02 +01:00
Ludovic Marie 7976203343 Merge pull request #12241 from MSedgeMC/fix/tame-panic-damage
🤖 Agent: A cat or parrot panics from any #panic_causes damage and only a wolf keeps #panic_environmental_causes, as TamableAnimalPanicGoal is built per species; the old fire_ticks gate is gone. I made its test return early without extracted mob data (CI compared None with Some(true)).
2026-11-08 10:27:28 +01:00
TheMrZZandCodex gpt-6.1-sol aaf53aa0bd fix: retain test-run source for completion coordinate feedback
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-08 10:21:52 +01:00
AkirabaneandClaude Opus 5.5 6ff8c950e4 Merge origin/main into fix/9335-passengers
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:19:33 +01:00
Ludovic Marie f8f7d2574b Merge pull request #12231 from NikitaMGrimm/tnt-size-eye
🤖 Agent: Primed TNT's box is 0.98F and its eye height 0.15F, widened from float as EntityType.TNT's sized() gives them; the command_exec case reads them back. Merged main (command_exec corpus group order).
2026-11-08 10:16:27 +01:00
Horizon agentandClaude Opus 5.5 0d27a2a38f Parity: keep the capture-preserving harnesses out of automatic runs
Seventeen harnesses need --work-dir (a fresh capture folder) and
--reference-dir, or assert that their fixed capture folder is new.
parity.py passes neither, so whenever --changed selects them they fail
at startup, whatever the pull request changed: 14 of the 18 failures on
#12336's parity run were these. List them under named_only: they still
run when named and in the nightly --all audit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:16:25 +01:00
SolenopsisbotandCodex 728141ad4f fix: retain remote tick vectors after iteration
Co-Authored-By: Codex <[email protected]>
2026-11-08 20:13:42 +11:00
Loki 329aae2ae3 Merge remote-tracking branch 'origin/main' into fix/11382-vehicle-entity-random 2026-11-08 18:09:56 +09:00
LokiandAntigravity db270fecca game: use entity random for boat, minecart and item frame operations (fixes #11382)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 18:09:48 +09:00
SolenopsisbotandCodex 4bf7247bcb perf: reuse decoration scheduling scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 20:07:21 +11:00
adiraj66132 92fc757a73 security: harden guest commands, the JSON parser and zip totals
- A LAN/dedicated guest's command no longer runs the host's client-side
  native mod commands (they ran before any permission check; client mod
  commands belong to the local player only).
- The server-files JSON parser refuses documents nested past 128 levels
  instead of recursing unbounded.
- The zip reader caps the total inflated bytes across entries (4 GiB per
  archive), not just per entry.

Co-Authored-By: Mercury <[email protected]>

# Conflicts:
#	crates/horizon-client-render/src/zip.rs
2026-11-08 14:37:10 +05:30
Florian ErnstandClaude Opus 5.5 8a964bbb79 Drop a discarded trader llama from the mob list in the same tick
TraderLlama.maybeDespawn discards an eligible llama in its aiStep when its
DespawnDelay runs out, so it leaves the level's entity list in that tick.
Horizon's countdown set m.removed, but ai::tick still returned true, so
the creature tick kept the llama in game.mobs until the next tick. During
that extra tick the next mob snapshot and the native leave bookkeeping
still saw it. Commands, the tracker and collisions already skip removed
mobs, so commands showed no difference.

The final return of ai::tick now reports a trader llama that its own tick
removed, and the creature tick drops it in that tick, as it does for a
despawned mob. The rest of the llama's tick still runs, and other species
are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:06:51 +01:00
masa-masa-0314andZCode 8a84813b69 Friends: the account's friends service and its worker
Port the service behind vanilla 26.3's friends list, without its screens:

- authlib's FriendsService over the discovery document: getFriends,
  updateFriends, updateAttributes and updatePresence with the account's
  bearer token, If-None-Match/ETag caches, Retry-After poll intervals and
  the ten-second read cooldown (MinecraftServicesFriendsService).
- RemoteFriendListUpdateHandler's worker and state: one read a minute
  (five in the background), a forced read after a successful action, the
  result-code-to-state mapping and the cached lists.
- PlayerSocialManager's flags and accessors, with no screen using them.
- PresenceHandler: the sharePresence option picks the public status, the
  client supplies getPresenceStatus each tick, and closing posts OFFLINE.

Minecraft's constructor decides whether the worker starts from the
account's friendsPreferences (UserFlag.FRIENDS_ENABLED /
ACCEPT_FRIEND_INVITES); title.poll_account now does the same after its
getAttributes answer. The overlay, the Online options switches and the
title/pause icon changes stay for later parts, so no player-visible UI
changes here. A joined Realm is carried to the presence status through
ServerData.isRealm's equivalent (Action::Connect.realm).

No new harness: the list and presence parsers, the state mapping, the
sharePresence levels and Retry-After are unit-tested.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 17:01:47 +08:00
Florian ErnstandClaude Opus 5.5 6fefa6ec54 Save the source and projectile owner UUIDs of a vibration
VibrationInfo keeps the UUIDs of its source entity and of a projectile's
owner, and VibrationInfo.CODEC saves them as source and projectile_owner.
The sculk listener now takes them from the save layer's UUID of the
mob the event names, writes them, and reads them back. Loaded vibrations
keep no live entity; a player source stays unnamed.

Fixes #12186

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 10:01:27 +01:00
Florian Ernst 362e77ba6c Merge origin/main into fix/loot-mine-location-block-state
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
#	crates/horizon-items/src/items.rs
2026-11-08 09:59:18 +01:00
SolenopsisbotandCodex 65f1235daf perf: reuse remote entity tick scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 19:56:39 +11:00
Florian Ernst cdfe986e8c Merge origin/main into fix/h052-vehicle-container-save
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/data.rs
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 09:56:29 +01:00
Florian Ernst b56f598a71 Merge origin/main into fix/h181-noai-trader-llama-delay
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/ai.rs
#	tools/command_exec_corpus.py
2026-11-08 09:56:20 +01:00
Florian Ernst b778599d5f Merge origin/main into fix/h157-player-save-projection
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 09:56:11 +01:00
Florian Ernst 1699b82741 Merge origin/main into fix/named-predicate-context
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/advancements.rs
#	crates/horizon-client/src/game/functions.rs
2026-11-08 09:55:53 +01:00
Florian Ernst 79723cb13e Merge origin/main into fix/inline-armor-trim-holders
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/item_sync.rs
#	crates/horizon-client/src/lan/guest.rs
#	crates/horizon-protocol/src/item.rs
2026-11-08 09:55:26 +01:00
Horizon agent ee61f0ea71 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 09:55:20 +01:00
Florian ErnstandClaude Opus 5.5 84e22207fb Measure saved vibration source UUIDs (H064 test checkpoint)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:48:41 +01:00
SolenopsisbotandCodex 83de5c24d6 perf: reuse selection outline scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 19:48:00 +11:00
Ludovic Marie 2d22e6a7e3 Merge pull request #12311 from voidsipp/fix/round6-12063
🤖 Agent: Server configuration dialogs keep the panorama behind them (DialogScreen inherits Screen.extractBackground) instead of the flat 20/20/20 fill.
2026-11-08 09:43:43 +01:00
Ludovic Marie e1315b7e55 Merge pull request #12357 from umbx-dot/launcher/mod-version-picker
🤖 Agent: Includes #12355. A version picked in the Versions tab is kept (chosen-versions.json) until the player installs the recommended one, so the pre-build swap and the automatic updates no longer undo a deliberate pick.
2026-11-08 09:43:38 +01:00
dd8daeb619 HDR float frame: remove the mark resolve and lane state
Float writers already store colour and coverage. Remove the unused mark
resolve pipeline and shader, and keep the world scratch and parameters for
the tone curve and HUD composite that still use them.

Choose no-pack HDR gain explicitly without storing a frame lane. Keep the
uniform ABI and existing light arithmetic. TAA tracks baked gain and its
settings, preserves compatibility history when gain is off, and rejects
history when it resumes after a shader pack.

Use a non-float legacy oracle for writer tests and restore exact float,
OIT and visibility-buffer sessions in the identity harness. Preserve the
compatibility mark paths and the non-layered hand brightness sentinel.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:37:53 +01:00
Ludovic Marie 6f856b2c0a Merge pull request #12343 from Solenopsisbot/perf/lz4-decode-scratch-v2
🤖 Agent: Each stream now starts from 21 zeroed bytes (clear + resize, pushed): a bare resize left the previous stream's bytes past a short block, which the decompressor may read, so corrupt-stream checks could differ from a fresh buffer.
2026-11-08 09:37:00 +01:00
Ludovic Marie b0ad9abd0c Merge pull request #12333 from Solenopsisbot/perf/pack-page-scratch
🤖 Agent: The retained page list is cleared after the shader pack's post passes (pushed): it keeps its capacity, but no longer holds clones of arena page buffers until the next pack frame, which would keep freed pages alive on the GPU once the pack is off.
2026-11-08 09:36:57 +01:00
Ludovic Marie c5c53d4c6c Merge pull request #12354 from horizon-rust/ci/pack-uniforms-scripted-clock
🤖 Agent: Test-only: the smoothed-uniforms test steps PackInputs::frame_timing instead of backdating the wall clock, so a slow runner can no longer push centerDepthSmooth past its 1e-3 tolerance (failed verify on #12241 and #12311).
2026-11-08 09:33:49 +01:00
Ludovic Marie 3bca17f46a Merge pull request #12160 from TheMrZZ/fix/h248-hurt-cooldown-incoming
🤖 Agent: This head holds all 13 layers of TheMrZZ's series (12010 already merged; 11732 … 12133 below it), so landing it merges the rest. The top layer's own point: inside the hurt cooldown a hit is compared with `lastHurt` by its incoming amount, before armor and Resistance, and only the difference goes on to `actuallyHurt`, as `LivingEntity.hurtServer` does.
2026-11-08 09:26:53 +01:00
Ludovic Marie 7e306e4b25 Merge pull request #12337 from NikitaMGrimm/hdr-marks-b
🤖 Agent: Stacked on #12330: landing it lands the chain. Only the float frame (HORIZON_HDR_FRAME=float) changes its alpha; 8- and 16-bit frames keep their pipelines and marks.
2026-11-08 09:26:49 +01:00
Ludovic Marie 26d73e3f28 Merge pull request #12346 from Solenopsisbot/perf/worldgen-biome-bitset
🤖 Agent: A spilled word keeps its index (128 + spill offset) in the chained iteration, so the ids stay ascending as with the Vec bitset; 1 KB on the stack per generated chunk.
2026-11-08 09:26:46 +01:00
Ludovic Marie 929cb85c85 Merge pull request #12340 from Solenopsisbot/perf/worldgen-empty-blender
🤖 Agent: Blender has no interior mutability (two OpenMaps and an Option, read only), so one shared empty Arc is safe for every chunk without old-world data, and non-empty blenders stay per chunk.
2026-11-08 09:26:42 +01:00
Ludovic Marie e3966b586a Merge pull request #12338 from Solenopsisbot/perf/game-cpu-fanout
🤖 Agent: The ten handoff loaders share only the resource root and the parsed entity document, both immutable; a panic in a worker is resumed on the caller and Creative's error keeps its message, so startup fails the same way. No wall-clock number yet: the gain is the slowest loader instead of the sum.
2026-11-08 09:26:39 +01:00
umbx-dotandClaude Opus 5.5 7a1fe7f702 launcher: a native mod's Versions tab says which version fits, and picks one
A registry native mod's Versions tab lists each version's date and how it
fits the Horizon played (the fit line the trust text already uses), marks
the one the launcher would pick Recommended, and has Install on every
version but the installed one.

A version the player picks there stays as it is: no automatic update, no
offered update and no swap before a build, until the player installs the
mod again or picks its recommended version (mod-browser/chosen-versions.json).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:23:15 +01:00
Ludovic Marie 443f9b0642 Merge pull request #12330 from NikitaMGrimm/hdr-marks-a
🤖 Agent: On the float frame the renderer is rebuilt when the HUD layering changes, not only the format; still behind HORIZON_HDR_FRAME=float.
2026-11-08 09:19:46 +01:00
Horizon agentandClaude Opus 5.5 79738505fb LZ4 scratch: zero the header bytes for each stream, as a new buffer had them
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:19:36 +01:00
Horizon agentandClaude Opus 5.5 851bda5b5e Shader pack pages: drop the buffer clones after the frame, keep the capacity
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:19:04 +01:00
Horizon agent ce322efe53 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 09:17:08 +01:00
umbx-dotandClaude Opus 5.5 238bf31f77 launcher: automatic updates keep a native mod on a version that builds
The start's automatic update moved native mods to their newest version, which the build then swapped back when it doesn't build on the Horizon played. It now updates them only to the newest version that builds there, when that is known.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:16:44 +01:00
Horizon agentandClaude Opus 5.5 aa1f9c5dc9 Shader pack test: drive the smoothed uniforms with a scripted clock
smoothed_uniforms_take_iris_half_lives backdated the wall clock and let
update() measure the gap, so any delay between the two calls moved
centerDepthSmooth (0.1 s half-life) past its 1e-3 tolerance. It failed
verify on busy runners for unrelated PRs (#12241, #12311). Use
PackInputs::frame_timing, which update() already takes as the frame's
delta, so every step is exact.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:15:28 +01:00
k3yhandClaude Opus 5.5 b4ed614faa A potion or tipped arrow is named by its contents
PotionItem.getName and TippedArrowItem.getName name these items by
their contents (PotionContents.getName: the contents' custom_name, the
potion, else empty), over item_name. brewing::contents_name_key gives
that key, and the five functions that name a stack take it after the
custom_name component and before item_name.

New command corpus group: potion contents names.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:13:32 -05:00
SolenopsisbotandCodex e3082a0281 fix: satisfy LZ4 clippy
Co-Authored-By: Codex <[email protected]>
2026-11-08 19:13:15 +11:00
Horizon agent 013fb324e6 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 09:08:18 +01:00
Horizon agentandClaude Opus 5.5 0a871808fa Ticket fast path: compare the portal tickets with their level, as the sources store them
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:08:18 +01:00
Vinícius MoreiraandClaude Opus 5.5 0e6d23f59b Polytone: biome and dimension modifiers as attribute layers
Resource packs' Polytone biome and dimension modifiers now change the
environment attribute stack the frame draws (wiki "Environment
Attributes"): a dimension modifier's base group changes the dimension's
own entries, rain and thunder change the weather's, post_process adds a
layer after all others; a biome modifier's base group changes its target
biomes' entries (ids, #tags through the biome tags, patterns) and
post_process adds a biome layer after the weather. REMOVE drops an
entry; modifiers apply lowest priority first. Values that are scripting
expressions or colormaps are left for the next steps.

The stacks are built when packs load, only when a pack has modifiers, and
only the frame reads them: gameplay (sky darkening, the server's sun
angle) keeps vanilla's. Without such a pack nothing changes.

With such a pack on, its biome and dimension modifiers load and apply;
the environment_render and environment_system harnesses still match
vanilla.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:08:03 -03:00
Ludovic Marie 170cfb468c Merge pull request #12321 from voidsipp/fix/round6-12128
🤖 Agent: LAN join and respawn packets carry the world spawn and its rotation, not the acting player's personal spawn, and /setworldspawn now updates connected players in every dimension (SetWorldSpawnCommand.setSpawn, PlayerList.sendLevelInfo).
2026-11-08 09:07:49 +01:00
Vinícius MoreiraandClaude Opus 5.5 0d0b58a960 Polytone: pack configs and conditional overlays
Polytone packs keep most of their effects in pack overlays that a
polytone_condition turns on: a scripting expression over the pack's own
config entries (config('pack:fog') == 'ON', modOn('minecraft', range)),
or an object with require_config, require_mods and override. A pack's
config entries (assets/<namespace>/polytone/config_entries/*.json) are
now read at their default values, and an overlay with a condition is
active when it holds: an expression decides alone (packs pair it with
formats no version matches), an object only alongside the formats unless
it overrides them. Overlays without one work as before.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:07:42 -03:00
Vinícius MoreiraandClaude Opus 5.5 92e3012389 Environment: sample the sky through the vanilla attribute stack
Environment now builds the dimension's EnvironmentAttributeSystem as
addDefaultLayers does: the dimension type's attributes, a biome layer for
every attribute some biome sets, the timelines its tag lists (in order),
and the rain and thunder layers where Level.canHaveWeather. The frame
samples it with the camera's Gaussian biome mix (the probe's order) and
the biome at the camera, so every attribute blends and changes with the
weather the vanilla way, not only the sky and fog colours, and the
hand-written timeline sampling, biome colour blending and apply_weather
are gone. Biomes keep their attribute maps for the biome layer.

environment_render now passes with no tolerance except one rounding in
Sky's light level fraction (was 218 inexact cases), and covers cloud
height.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:07:42 -03:00
SolenopsisbotandCodex 0427c41bde perf: keep biome id bitsets on the stack
Co-Authored-By: Codex <[email protected]>
2026-11-08 19:05:34 +11:00
umbx-dotandClaude Opus 5.5 54bedee730 launcher: use the native mod version that builds on your Horizon
Installing a registry native mod took its newest version, and a build
left out any mod made for another Horizon, though the registry often has
an older version made for the Horizon the player picked.

native_fit::best picks the newest version that builds on the Horizon
played, else the newest that moves the build to its own commit without
clashing. Installs and updates use it, and before each build every mod
turned on whose installed version doesn't build is swapped for that
version, with a line saying which version is used.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:04:54 +01:00
Kacper 23bd1b7627 Merge pull request #12318 from TheApple05/codex/parity-12181
Keep resource-pack parity paths intact with JSON headers
2026-11-08 09:00:21 +01:00
Kacper 920346fb43 Merge pull request #12010 from TheMrZZ/fix/h236-clone-dimensions
Resolve /clone's source and target dimensions
2026-11-08 09:00:13 +01:00
Kacper 632d6cd4f4 Merge pull request #11654 from MSedgeMC/fix/set-custom-model-data
A loot table's set_custom_model_data writes the item's model data
2026-11-08 09:00:01 +01:00
Kacper 98342f1bd0 Merge pull request #11476 from baytyone/xp-orbs-riding-merge-water
Experience orbs: pickup over the vehicle, a kept following player, merges before the move, eye-level water
2026-11-08 08:59:50 +01:00
Kacper d2cadb1c39 Merge pull request #11473 from MSedgeMC/fix/vindicator-johnny
A vindicator that was named Johnny stays Johnny
2026-11-08 08:59:39 +01:00
Kacper cab807c9d7 Merge pull request #11463 from MSedgeMC/fix/palette-pack
Chunk storage: pack a section's palette as vanilla's PalettedContainer.pack does
2026-11-08 08:59:32 +01:00
SolenopsisbotandCodex 70e0a37264 fix: satisfy pack page lint
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:56:55 +11:00
SolenopsisbotandCodex 58b2eaa55e fix: preserve LZ4 scratch stream length
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:51:32 +11:00
SolenopsisbotandCodex 104fe56b30 perf: reuse LZ4 decode scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:51:32 +11:00
SolenopsisbotandCodex 98d9f172bd perf: share empty worldgen blenders
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:43:32 +11:00
Ludovic Marie b49f32e4ab Merge pull request #12332 from martipops/settings/shell-choice-step
🤖 Agent: Left and Right step a focused choice back and on in the settings shell, as Shift-press and press do; other rows keep their keys.
2026-11-08 08:41:32 +01:00
Ludovic Marie 32613f5637 Merge pull request #12331 from Vaspyyy/biome-id-past-table
🤖 Agent: A biome id past the table after a proxy's server switch names plains instead of panicking (#12148: "the len is 67 but the index is 67"), and the reconfiguring client plays menu music as ServerReconfigScreen lets it.
2026-11-08 08:41:29 +01:00
Ludovic Marie 3f604c3028 Merge pull request #12329 from Solenopsisbot/perf/vegetation-scratch
🤖 Agent: Vegetation patches iterate their position sets directly instead of copying them, and multiface growth shuffles fixed arrays; the shuffle draws the same number of randoms in the same order.
2026-11-08 08:41:25 +01:00
Ludovic Marie ed4077ec1d Merge pull request #12328 from Solenopsisbot/perf/creature-vegetation-scratch
🤖 Agent: The creature tick's MobEvent buffer is kept between ticks and drained, returning its capacity even when a handler errors.
2026-11-08 08:41:22 +01:00
Ludovic Marie f9b605a934 Merge pull request #12327 from Solenopsisbot/perf/pack-resource-fanout
🤖 Agent: Pack names, clouds, the forcefield and custom sky load on four scoped threads; panics still propagate (resume_unwind) and the values are the same.
2026-11-08 08:41:18 +01:00
Ludovic Marie fbd34a0343 Merge pull request #12326 from J5Casey/braid/fishing-medium
🤖 Agent: The fishing_medium native hook lets a mod make a bobber float and fish in another block, with its own loot table and lure range; in water the vanilla path is untouched.
2026-11-08 08:41:14 +01:00
Ludovic Marie e9208d8d86 Merge pull request #12325 from Solenopsisbot/perf/displacement-frame-scratch
🤖 Agent: The displacement pass collects its nearby sections into a SmallVec sized to its 64-section cap and reuses its section table, removing the per-frame heap allocations; the GPU writes are unchanged.
2026-11-08 08:41:10 +01:00
Ludovic Marie e0081cb2ae Merge pull request #12324 from k3yh/loot-components-modelled
🤖 Agent: A loot set_components patch for a component Horizon models (potion_contents and the rest) is read into its field as a saved stack's is, so a rolled potion gives its effect; damage and custom_name keep their own paths.
2026-11-08 08:41:06 +01:00
Ludovic Marie 9990881f3a Merge pull request #12291 from masa-masa-0314/feat/realms-account-uuid
🤖 Agent: The single-player owner is the signed-in account's UUID and older worlds move their owner to it on open; I made the migration keep the offline player files as .dat_offline backups instead of deleting them.
2026-11-08 08:41:01 +01:00
k3yhandClaude Opus 5.5 24a3e509bd /loot give to a full inventory throws nothing out
The give branch of cmd_loot spawned what give couldn't fit at the
command's position and counted every drop. LootCommand.playerGive puts
each drop in with Inventory.add: what fits stays, the rest is lost, and
the drop counts only when add returns true, which it does when all of
it went in or for a creative player.

New command corpus group: loot give full inventory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:39:11 -05:00
SolenopsisbotandCodex 7dda0df3c0 perf: fan out game CPU resource loads
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:38:23 +11:00
Ludovic Marie 6539a95ef6 Merge pull request #12322 from masa-masa-0314/feat/realms-create-world
🤖 Agent: Generate New World opens Create World and uploads the temporary world it writes, as RealmsCreateWorldFlow.createWorld does.
2026-11-08 08:37:45 +01:00
Ludovic Marie 4a1adf44fd Merge pull request #12295 from glitchsnake/fix/11854-mob-health-boost
🤖 Agent: A mob's max_health is refreshed whenever Health Boost's amplifier changes or it ends (load, conversion, milk, death protection), and health is clamped to the new maximum, as refreshDirtyAttributes does. I merged main: #12255 already refreshes Health Boost in apply_death_effect and the trader's drink, so this PR's copies of those blocks were dropped and its test kept.
2026-11-08 08:37:41 +01:00
Ludovic Marie 4cf8cd0957 Merge pull request #12257 from voidsipp/fix/round6-12230
🤖 Agent: Piston block events and sculk charge cursors check the block-ticking ticket range (ServerLevel.shouldTickBlocksAt), not chunk ticking readiness, so they run in a freshly ticketed chunk; moving pistons still wait for readiness, as the oracle shows. Merged main (isPositionTickingWithEntitiesLoaded's doc now names the entities).
2026-11-08 08:33:21 +01:00
Nikita Maximilian GrimmandGPT 6.1 Sol eb835292f5 HDR float frame: store colour and coverage without light marks
Choose float alpha storage independently of the no-pack HDR gain. Plain
draws and the mirror keep their RGB; opaque finishes write alpha 1 and
blended draws keep coverage. Keep the existing written-light arithmetic.

Run light-mark redraws and hand unmark/remark only on compatibility
frames. Preserve the non-layered 0.2 hand's UI-brightness sentinel. Stop
resolving float alpha into light, while retaining the resolve oracle and
lane state for the next cleanup.

Check real shader outputs on the GPU and preserve the existing pictures
with differential HDR, pack, OIT, visibility-buffer and hand captures.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-08 08:32:06 +01:00
SolenopsisbotandCodex ddaada5bb9 perf: reuse shader pack page scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 18:31:27 +11:00
VaspyyyandClaude Opus 5.5 6b33c13380 Divide MenuTooltipPositioner's widget offset as floats like vanilla
getOffset divides the pointer's distance from the widget edge by the
widget height as floats: 26.3's bytecode is iload_3, i2f, iload_2, i2f,
fdiv before Mth.lerp and Math.round(float). The decompiled text without
the casts made widget_offset an int division, so a hovered widget's
tooltip stayed height - 3 below its edge until the pointer was a whole
height away, then jumped. On a 20 px button it sat up to 10 px lower than
vanilla's. The offset now follows the pointer as vanilla's does, rounded
with jmath::round_f32, and the test expectations are the values the real
26.3 MenuTooltipPositioner returns for the same inputs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:28:21 +01:00
VaspyyyandClaude Opus 5.5 b03828a569 Show the title screen icon buttons' tooltips while hovered like vanilla
The title screen's icon row (Friends, Language, Accessibility) drew its
sprites but no tooltip: the tooltip pass after the button loop only knew
Create World, Select World and Multiplayer. Vanilla gives these buttons a
Tooltip (CommonButtons.language and accessibility, FriendsButton while
friends are available), which WidgetTooltipHolder draws while the button
is hovered, placed by MenuTooltipPositioner and split at 170 px. The
accessibility onboarding screen builds its Accessibility Settings... and
Language... buttons with the same CommonButtons, so they lacked the same
tooltips.

Title::button_tooltip now names each screen's button tooltips, and both
the narration hints and the draw loop read it. The draw loop remembers
the hovered button's tooltip and draws it after the widgets (and after
the onboarding fade, which vanilla applies to widgets only) with the same
positioner (ui::tooltip_menu), before the toast so the toast stays on top.
Fixes #12067.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:28:21 +01:00
VaspyyyandClaude Opus 5.5 99bd306532 Scatter a destroyed chest boat's or container minecart's contents like vanilla
AbstractChestBoat.remove and AbstractMinecartContainer.remove spill the
container through Containers.dropContents for every removal reason that
destroys the vehicle: the kill of VehicleEntity.destroy and a creative
hit's discard alike. That spill has no entity_drops gate; only the hull
item and chestVehicleDestroyed's second, empty spill follow the rule.

Horizon's damage path got both vehicles wrong. A chest boat dropped each
stack whole at its position with an item's own motion and a pickup
delay of 10, and never drew chestVehicleDestroyed's per-slot spots. A
container minecart scattered its contents only with entity_drops on, so
with the rule off (or from a creative hit with it off) the contents
vanished.

Both now share one Containers.dropContents helper (the /kill path used a
second copy): the contents scatter from a spot in the vehicle's block in
pieces of 10 to 30 with no pickup delay, before the ENTITY_DIE event and
before a boat's lead, as Entity.kill and AbstractBoat.remove order them.
The hull item and the empty spill still need entity_drops and a
non-creative hit. Every damage source (players, LAN guests, explosions,
lightning, /damage) goes through these two functions, so single player,
LAN and dedicated servers all get it.

The vehicle_command_damage harness now breaks a chest boat with /damage,
and a chest minecart with entity_drops off both by /damage and by a
creative player's hit, against the real 26.3 server. Each break must
leave the 5 diamonds inside the vehicle's block with no pickup delay,
and the 40 dirt as piles in that block whose counts add up to 40. A
gameplay test seeds the level random, breaks a chest boat and a chest
minecart, and checks that the random ends where vanilla's two
Containers.dropContents calls leave it (the empty second spill's draws
included).

The port map rows for AbstractChestBoat.remove and
AbstractMinecartContainer.remove point at the shared helper instead of
the removed /kill copy. The chestVehicleDestroyed row now says what is
left there: a player's hit on a container minecart angers no piglins yet.

Fixes #12121

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:27:26 +01:00
Horizon-agent 2fae2a9101 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 08:21:34 +01:00
Ludovic Marie aedfd10c93 Merge pull request #12220 from NikitaMGrimm/hdr-frame-rebuild
🤖 Agent: The HDR float frame follows HDR, Smooth Gradients and the picked pack at once by rebuilding the renderer; all of it is behind HORIZON_HDR_FRAME=float, so the default path only pays one bool check per frame.
2026-11-08 08:19:20 +01:00
VaspyyyandClaude Opus 5.5 6d72b0375e Fire shulker bullets from the box centre and turn them at standable blocks
A shulker's bullet started at the centre of the shulker's block cell.
Vanilla's ShulkerBullet constructor starts it at
owner.getBoundingBox().getCenter(), and Shulker.makeBoundingBox grows
that box toward the open side by the lid's physical peek (times the
scale), so an open floor shulker fires from the top of its block
(#11974). shulker::bullet_origin now builds that box with
getProgressDeltaAabb at the shulker's scale, from the peek the goal
sees (the attack goal runs before Shulker.tick eases the peek, in both
games).

In flight, a bullet turned only before a full collision cube. Vanilla
asks Level.loadedAndEntityCanStandOn(next, bullet): the top face of the
block's collision shape, in the bullet's collision context, is full
(Block.isFaceFull). Top slabs, upside-down stairs, closed top trapdoors
and the like now turn it too, instead of the bullet flying into them
(#11970). collisions::projectile_can_stand_on implements the test, and
ProjectileWorld::solid becomes can_stand_on(pos, entity_y), since
scaffolding reads the bullet's height.

New harness shulker_bullet: the real ShulkerBullet constructor for all
six attach faces, every peek of an opening lid and four scales, and
loadedAndEntityCanStandOn over forty block states at four bullet
heights, compared bit for bit with the Rust probe.

Fixes #11974
Fixes #11970

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:17:17 +01:00
Horizon agentandClaude Opus 5.5 4f71890226 Ticket fast path: compare the portal tickets' chunks after the main merge
#12232's portal_matches read self.portal as (chunk, level) pairs; here it holds
PortalTicket, so it compares ticket.chunk.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:16:49 +01:00
Ludovic Marie f64f181d1d Merge pull request #12319 from glitchsnake/fix/11651-spawn-finalize
🤖 Agent: EntityType.create spawns (frogspawn tadpoles, infested silverfish, allay duplication, villager breeding, sniffer eggs, the thunder skeleton horse...) skip finalizeSpawn's rolls; I fixed the lightning witch, which vanilla still finalizes with CONVERSION (Villager.java:748).
2026-11-08 07:57:06 +01:00
martipops 47a832db72 Merge remote-tracking branch 'upstream/main' into settings/shell-choice-step 2026-11-08 01:56:53 -05:00
Horizon agentandClaude Opus 5.5 3627361d25 Merge main into #12295
#12255 already refreshes Health Boost and clamps absorption in apply_death_effect
and the wandering trader's drink; main's blocks are kept, this PR's copies of them
dropped, and its clear-all-effects test kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:55:18 +01:00
Horizon agent db1e3398ce Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 07:54:46 +01:00
SolenopsisbotandCodex 5288273f01 perf: reuse vegetation placement scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 17:50:24 +11:00
SolenopsisbotandCodex e55b51a4b0 perf: reuse creature event scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 17:47:51 +11:00
Ludovic Marie fe020bb113 Merge pull request #12314 from NikitaMGrimm/warden-digging-test
🤖 Agent: The warden digging test ticks the warden at age 1, since the client moves tickCount on before it ticks; it had failed on main since #12110 wherever the vanilla data is prepared.
2026-11-08 07:45:06 +01:00
Ludovic Marie 0ae694bb66 Merge pull request #12308 from voidsipp/fix/round6-12001
🤖 Agent: F3 shows the debug overlay on the title screen and F3+F6 opens the debug options over the panorama, closing back to the title without grabbing the pointer, as TitleScreen allows.
2026-11-08 07:45:02 +01:00
Ludovic Marie 25b7c04314 Merge pull request #12307 from NikitaMGrimm/tnt-piston-powder
🤖 Agent: A piston push of primed TNT applies the blocks it moves through (powder snow, soul sand, slime), as the piston's Entity.move does. Stacked on #12290.
2026-11-08 07:44:59 +01:00
Ludovic Marie 1a8589a0dd Merge pull request #12305 from NikitaMGrimm/hdr-oit-written
🤖 Agent: Improved Transparency writes light on the float frame too, behind the same HDR_WRITE_LIGHT override. Stacked on #12302.
2026-11-08 07:44:55 +01:00
Ludovic Marie 7176e0df23 Merge pull request #12303 from Solenopsisbot/perf/worldgen-shape-capture
🤖 Agent: The decoration shape-capture vector is kept per region and restored on every path, including errors, instead of a fresh Vec per update.
2026-11-08 07:44:51 +01:00
Ludovic Marie fad4b0f5dd Merge pull request #12302 from NikitaMGrimm/hdr-vis-written
🤖 Agent: On the float frame the visibility buffer writes light itself; behind the HDR_WRITE_LIGHT override, false on the default frame. Stacked on #12282.
2026-11-08 07:44:48 +01:00
Ludovic Marie 8ca3dc19dd Merge pull request #12299 from d3xc0/android-touch-1
🤖 Agent: Adds the touch control layout model (schemes, 14 controls, load/save with defaults); nothing calls it yet, so players see no change.
2026-11-08 07:44:44 +01:00
Ludovic Marie 26ee23d376 Merge pull request #12296 from voidsipp/fix/round6-12089
🤖 Agent: An option button's pointer cursor follows hover only, while its focus still highlights it (AbstractWidget.handleCursor tests hover), and a click on a disabled button no longer focuses it.
2026-11-08 07:44:40 +01:00
Ludovic Marie fe524fefb2 Merge pull request #12293 from J5Casey/braid/screen-input-act
🤖 Agent: Screen drag and wheel get v2 hooks that lend the game mutably, heard just before v1; v1 keeps its signature and still hears the turn unless v2 cancels it or closes the screen.
2026-11-08 07:44:36 +01:00
Ludovic Marie bb26451389 Merge pull request #12292 from voidsipp/fix/round6-12218
🤖 Agent: A resting item in a lava cauldron burns: each lava cauldron its box meets (the cauldron's inside shape, not the air above) applies LAVA_IGNITE and its own lavaHurt, stopping once the item is gone; netherite stays immune, as the new oracle shows.
2026-11-08 07:44:33 +01:00
SolenopsisbotandCodex b7468c0196 perf: fan out independent pack resource loads
Co-Authored-By: Codex <[email protected]>
2026-11-08 17:43:18 +11:00
Horizon agent 282b7b3085 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 07:42:55 +01:00
Horizon agent 7d269ccea1 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/realms.rs
2026-11-08 07:42:42 +01:00
Horizon agent c072d21438 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 07:42:38 +01:00
SolenopsisbotandCodex 94bd299a9a perf: reuse displacement frame scratch
Co-Authored-By: Codex <[email protected]>
2026-11-08 17:39:59 +11:00
bf6a7d910c HDR float frame: rebuild when the HUD layering changes
Choose the frame format and HUD layering together. No-pack float HDR needs
the HUD layer, so devices without it keep the compatibility frame. A 0.2
pack still gets its float frame in SDR and HDR.

Rebuild when picking or dropping a pack changes the layering even if the
format stays float, so the renderer always has the layering a fresh start
would give it.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:39:39 +01:00
Horizon agentandClaude Opus 5.5 944a3e1c80 Keep the offline player files as backups when the owner moves
The migration renamed nothing back if it went wrong: it deleted the offline
.dat and .dat_old after writing the account's file. They now stay beside it as
.dat_offline and .dat_old_offline.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:36:14 +01:00
Horizon agent f57866e72f Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 07:35:55 +01:00
k3yhandClaude Opus 5.5 8c8615875e A potion a loot table rolls with set_components gives its effect
StackData::set_loot_component kept every component but trim in other,
its saved form, so a potion a chest or a mob's loot rolled with a
set_components potion_contents gave no effect: brewing::contents_effects
reads potion and potion_extra. A modelled component is now read into
its field the way a saved stack's is (decode_components). Damage stays
in other, as the stack holds it, and custom_name keeps its own reading.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:34:11 -05:00
Horizon agentandClaude Opus 5.5 2e2635f83d Lightning: the witch from a villager is still finalized
Villager.thunderHit converts with finalizeSpawn(level, difficulty,
EntitySpawnReason.CONVERSION, null); only Pig.thunderHit skips it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:33:26 +01:00
VaspyyyandClaude Opus 5.5 dde2b2abff Start the menu music while reconfiguring and test the #12148 path
Vanilla's ServerReconfigScreen is a plain Screen, not a
LevelLoadingScreen, so MusicManager.tick starts the MENU music that
getSituationalMusic picks once the player is gone. Horizon treated the
reconfiguration as loading (no player, no title, no credits) and stayed
silent until the next world; a reconfiguring connection no longer
counts as loading.

A Game test (prepared resources, ignored by default) puts a column
holding a biome id one past the table at the origin, reads it through
biome_at and biome_name_at (plains), then checks that a reconfiguring
client picks and starts the menu music and that the level probe after
it reads plains' music without a panic.

Biomes::name's doc now says that Biomes::get clamps such an id to the
last entry instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:31:01 +01:00
VaspyyyandClaude Opus 5.5 85b61b6f6e Stop the client crashing on a biome id past its table after a server switch
A proxy that moves the player to another server reconfigures the
connection. Horizon keeps the last server's columns until the next
Login clears them, but Reconfigured already swaps in the next server's
biome table. When the last server had a biome the game lacks (added to
the table as id 67) and the next one does not, those columns hold an id
past the 67-entry table. situational_music still probed the level at
the origin during the reconfiguration, and biome_at indexed the table
with that id: "index out of bounds: the len is 67 but the index is 67".

Vanilla never reads that level: handleConfigurationStart calls
clearClientLevel, which nulls the player, so getSituationalMusic
returns the menu music. The music probe now asks Remote::has_level
(Login seen, no reconfiguration since) instead of logged_in.

The three places that indexed the biome table directly (biome_at for
music, ambience and custom skies, biome_name_at for random textures and
fishing, and /fillbiome's filter) now go through Biomes::name, which
names plains for an id past the table, as biome_at already did for a
missing column and Biomes::id does for an unknown name.

Fixes #12148

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:30:57 +01:00
Horizon agent aee2c83c2f Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 07:29:43 +01:00
Horizon-agent 24c3cabed3 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-08 07:20:51 +01:00
Horizon agentandClaude Opus 5.5 61d43cf005 Skip the tamable panic test without extracted mob data
with_built_test_world runs nothing where the data is missing (CI), and the
test then compared None with Some(true); it now returns early there, as the
other with_built_test_world tests do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:16:00 +01:00
Horizon agent b62553197b Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 07:15:37 +01:00
Ludovic Marie 6ef71901bf Merge pull request #12316 from horizon-rust/fix/test-batch-starting-feedback
🤖 Agent: the batch-starting line now goes to the /test command's own source inside the command (TestBatchSummaryDisplayer); the test-instance block still uses the server source. Lifecycle stays 95/99: its 4 if-loaded records are an oracle artifact.
2026-11-08 07:13:17 +01:00
Ludovic Marie da954fba54 Merge pull request #12315 from horizon-rust/fix/mob-scores-until-removal
🤖 Agent: mob_died no longer resets scores at the death; vanilla does it in onDestroyed, so unloaded corpses keep them. The #12185 shulkers get fixed UUIDs so /scoreboard players list can match at all.
2026-11-08 07:13:14 +01:00
LokiandAntigravity 4f54fcf47b mobs: skip finalizeSpawn for EntityType.create spawns (fixes #11651)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 15:13:09 +09:00
TheApple05andCodex GPT-6 911bab7e0f Preserve resource-pack parity paths with JSON headers
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-08 06:11:54 +00:00
Ludovic Marie 22e6e5c8a4 Merge pull request #12242 from NikitaMGrimm/entity-own-random
🤖 Agent: A new item's own random now makes its UUID (Mth.createInsecureUUID, the first two draws), then bobOffs, yaw and motion in that order, instead of a process-wide clock-seeded UUID; I merged main (the seed-sync gaps diagnostic this PR retires stays out).
2026-11-08 07:09:07 +01:00
Ludovic Marie d77d31fff1 Merge pull request #12245 from Solenopsisbot/perf/tick-scratch-reuse
🤖 Agent: Block contact scratch lists are kept on Game and restored on every Result path; I replaced extend(drain(..)) with append, the clippy error that failed verify (clippy -D warnings passes now).
2026-11-08 07:09:04 +01:00
Ludovic Marie 2c04e1ba85 Merge pull request #12211 from rdbtCVS/perf/beehive-stay
🤖 Agent: bees_stay_in_hive's night and rain answer is kept for the hive's turn instead of building the mob world per occupant.
2026-11-08 07:09:00 +01:00
Nikita Maximilian Grimm edddce4485 Merge remote-tracking branch 'origin/main' into warden-digging-test 2026-11-08 07:08:38 +01:00
Horizon-agent 030e7e6ff6 Merge commit '02ff63cf4bc4f243391974eb957afa247f4c5a01' into HEAD 2026-11-08 07:05:41 +01:00
masa-masa-0314andZCode 32cc9c56e0 Realms: Generate New World opens Create World and uploads what it makes
`RealmsResetWorldScreen`'s Generate New World tile is enabled: it opens the Create
New World screen (`RealmsCreateWorldFlow.createWorld`), and its Create builds the
temporary world the upload packs instead of entering a local one.

- The tile returns the Realm, slot and reset kind to the title, which shows Create New
  World with the default options. Create (`create_or_upload`) builds a `Level` from
  those options (name, seed, game type, difficulty, cheats, game rules, feature flags
  and data packs) and hands it back to the Realms screen; Back/Escape returns to the
  configuration screen (or the list for a new Realm).
- `realms::create_temporary_world` mirrors `createTemporaryWorldFolder`: a temporary
  folder with level.dat from `WorldData.createTag(null)` (no `singleplayer_uuid`), the
  world generation and game rules, and the Create World screen's data packs moved into
  `datapacks/`. No chunks: the Realm generates them.
- The upload runs the same `RealmCreationTask` → `SwitchSlotTask` → `packAndUpload`
  chain as a local-world upload, with `RealmsWorldOptions.createFromSettings` (slot
  name, game mode, difficulty, the game's version) and the hardcore setting. While it
  runs `AlertScreen` (`mco.create.world.reset.title`) shows a Cancel; success returns
  to the configuration screen, or plays the Realm just created
  (`RealmsMainScreen.play`); cancel returns to the reset screen; a failure shows
  `mco.create.world.failed`.
- `Level::write_upload` writes only the upload's files (no player file, weather or
  clocks), with `singleplayer_uuid` omitted as `createTag(null)` writes it.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 14:05:35 +08:00
Horizon-agent 02ff63cf4b Merge commit '1cccd4167d8525b125ac6cc74f0dcc235683e767' into HEAD 2026-11-08 07:04:51 +01:00
Ludovic Marie 6ebe4a33c3 Merge pull request #11867 from MrCloudy2/fix/passengers-players
🤖 Agent: `execute on passengers` finds the players riding a vehicle on a server (the player in place, then the hub guests by their vehicle); I merged main, whose new mob-children arm now follows this PR's player_passengers, so both players and mob riders are found.
2026-11-08 07:04:32 +01:00
Horizon-agent 1cccd4167d Merge commit '849c2915611feced73975c8310f5d5f603f747af' into HEAD 2026-11-08 07:03:58 +01:00
Horizon-agent 849c291561 Merge commit 'ca441c3ea06bbff7c2cae41cd4b3f0cf3ea88813' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 07:03:46 +01:00
Horizon-agent ca441c3ea0 Merge commit '5ee77de8c82449a4830f716a1be8d7553fdd65d4' into HEAD 2026-11-08 07:02:48 +01:00
martipopsandClaude Opus 5.5 dba8453681 Settings: Left and Right step a focused choice back and on
On the settings shell (Extras, Graphics), a value between `<` `>` could
only be stepped back with Shift-click or, with an Extras setting on, the
wheel: a pad or keyboard player could only go forward. Left and Right on a
focused choice now step it back and on, as its arrows show, from the D-pad
and the arrow keys alike. They are a Shift press and a press, the same path
as a click, so Undo and saving follow.

They step at once rather than after A arms the row, as sliders do: A keeps
its step forward and there is no mode to find, and up and down passing over
a choice never change it. Up and down leave the row, and LB and RB reach the
page list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:02:15 -05:00
Horizon-agent 5ee77de8c8 Merge commit '35ab3f27b058be561260e09d9676d289cde52565' into HEAD 2026-11-08 07:01:55 +01:00
voidsip ba5f08c10c Keep respawn fallback and spawn rotations consistent with world data 2026-11-08 00:01:37 -06:00
Horizon-agent 35ab3f27b0 Merge commit 'ed1d96c9117c54ea7a6d74556f34fa08d05aeb8a' into HEAD 2026-11-08 07:00:52 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4018f7925e Warden digging test ticks the warden at age 1, as the client does
Since #12110 a warden's client tick plays the heartbeat when its age is
a multiple of the beat delay. The test ticked a new warden at age 0, so
it heard a heartbeat. The client loop moves the age on before it ticks,
as vanilla's `tickCount++` does, so the game never ticks at age 0.

Fixes #12312

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 07:00:07 +01:00
Horizon-agent ed1d96c911 Merge commit 'd7c560ec78d66a2c022a494be7c084f5aa7dfad9' into HEAD 2026-11-08 06:59:58 +01:00
Horizon-agent d7c560ec78 Merge commit 'dc41d4bc93996ede221a19266f924b30f683e39b' into HEAD 2026-11-08 06:59:13 +01:00
Ludovic Marie cbb2b99022 Merge pull request #12285 from voidsipp/fix/round6-12201
🤖 Agent: An export that fails on a missing lib/tzdb.dat now names the Java to repair, matched only on the FileNotFound/NoSuchFile line for that exact path; other failures keep their message.
2026-11-08 06:58:24 +01:00
Ludovic Marie 7fdce634b4 Merge pull request #12283 from k3yh/loot-world-pack-ids
🤖 Agent: /loot and pack functions take the world's packs' loot tables: the id list comes from the loaded tables (root plus pack overlay) and is refreshed on /reload.
2026-11-08 06:58:20 +01:00
Horizon-agent dc41d4bc93 Merge commit '826fdf6306a88ad931520b0a9f34801675b76660' into HEAD 2026-11-08 06:57:09 +01:00
Horizon-agent 826fdf6306 Merge commit '1c878671de71a6827708ed1eb51141b32da22d2a' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 06:56:51 +01:00
Horizon-agent 1c878671de Merge commit '414fcee500187355e0a27c64819b2d9823a93a3f' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 06:56:24 +01:00
Ludovic Marie 7d084fc8aa Merge pull request #12282 from NikitaMGrimm/hdr-write-light
🤖 Agent: On the float frame every shader writes its own light; all of it sits behind the HDR_WRITE_LIGHT pipeline override, false on the default frame, so the SDR path compiles it out. Stacked on #12220 and #12247.
2026-11-08 06:54:56 +01:00
Ludovic Marie edb0905d0f Merge pull request #12267 from J5Casey/braid/menu-links
🤖 Agent: Mod menu types can show slots of other containers (native_menu_links) instead of mirroring them each tick; stacked on #12246, lands after it.
2026-11-08 06:54:53 +01:00
Ludovic Marie f6dda117a8 Merge pull request #12265 from voidsipp/fix/round6-12129
🤖 Agent: Other crates' records, native mods included, now reach latest.log from INFO with their target prefixed; they still stay off stderr, so only the log file grows.
2026-11-08 06:54:49 +01:00
Ludovic Marie 5c0ac57986 Merge pull request #12263 from J5Casey/braid/loot-custom-data
🤖 Agent: match_tool and loot predicates now test minecraft:custom_data as NbtUtils.compareNbt(pattern, data, true), with JSON patterns converted as JsonOps does; compare_nbt moved to horizon-world for both callers.
2026-11-08 06:54:45 +01:00
Ludovic Marie a12e378c46 Merge pull request #12262 from k3yh/loot-give-guests
🤖 Agent: /loot give gives to every target player, each guest with their own state in place via for_player, as LootCommand.playerGive does.
2026-11-08 06:54:41 +01:00
Ludovic Marie c1e81319cb Merge pull request #12259 from J5Casey/braid/player-break-api
🤖 Agent: native_player_break_block breaks a block as a given player through the game's own player-break path (spectator, adventure can_break, spawn protection, native_destroy_block, loot, wear, stats).
2026-11-08 06:54:38 +01:00
Ludovic Marie 7232e9f6bd Merge pull request #12255 from echo-ilovech3ss/fix-11887-mob-absorption
🤖 Agent: Death protection and milk removal clamp a mob's absorption to its remaining max_absorption (onAttributeUpdated), instead of subtracting 4 per Absorption level.
2026-11-08 06:54:34 +01:00
Ludovic Marie fe63866acc Merge pull request #12254 from rison1337/perf/tight-terrain-occlusion
🤖 Agent: CPU terrain occlusion tests each section's padded mesh bounds instead of the whole 18-block cell, so empty space no longer keeps a hidden section drawn; sections without bounds keep SECTION_BOX.
2026-11-08 06:54:31 +01:00
Ludovic Marie 4c66382f4e Merge pull request #12252 from masa-masa-0314/feat/realms-slot-pictures
🤖 Agent: Realms world slots draw their own pictures and template pages arrive page by page. Cost to note: the four 1024-tall slot pictures now live in the HUD atlas for every player, about one 1024-tall shelf of GPU memory; a follow-up should load them only while Realms screens are open.
2026-11-08 06:54:28 +01:00
Ludovic Marie 604e9cb82f Merge pull request #12251 from k3yh/loot-set-name-components
🤖 Agent: /loot now applies set_name and set_components to its drops, as a chest's roll already did, and names each drop by its display name in the feedback.
2026-11-08 06:54:24 +01:00
Ludovic Marie 205a2387c3 Merge pull request #12248 from vnsmoreira/attributes/timelines
🤖 Agent: Ports vanilla's timelines, keyframe tracks and all 32 easings into horizon-world with Java's float/double steps; nothing in the game calls it yet, only the two new oracles' probes.
2026-11-08 06:54:21 +01:00
Ludovic Marie 1d56d438df Merge pull request #12247 from NikitaMGrimm/hdr-light-function
🤖 Agent: One hdr_mark_light function serves the display and resolve passes; a GPU test checks it against the old code for every 8-bit mark and both easing modes.
2026-11-08 06:54:17 +01:00
Ludovic Marie 7a87965ec8 Merge pull request #12246 from J5Casey/braid/craft-grid-3
🤖 Agent: The recipe_sources native hook lets recipe placement into a mod menu type's grid take ingredients from containers the mod names; stacked on #12240 and #12233, lands after them.
2026-11-08 06:54:13 +01:00
Ludovic Marie 1334feb5fa Merge pull request #12244 from TheMrZZ/fix/h268-global-selector-order
🤖 Agent: An unbounded arbitrary @e walks the joined families in insertion order (EntityLookup.getEntities), falling back to the old order if the walk fails; it builds one count map per such query, other selectors keep their path.
2026-11-08 06:54:09 +01:00
Ludovic Marie fb75635ec3 Merge pull request #12243 from NikitaMGrimm/fn-tick-time
🤖 Agent: #minecraft:tick functions now run before the levels tick, so they read the game time from before the tick, and /schedule'd ones run in ServerLevel.tickTime once the clock has moved, as MinecraftServer.tickChildren orders them.
2026-11-08 06:54:06 +01:00
J5CaseyandClaude Opus 5.5 b1a75237d0 Native hook: fishing_medium, a bobber that floats and fishes outside water
A mod answers `native_fishing_medium(dimension, hook, position, owner, medium)`
for a bobber that is flying or floating, not in water and not hooked to an
entity. `Some(NativeFishingMedium)` makes the spot a medium: in the block of
its `surface` or below, the bobber floats, waits, bites and is reeled in as
in still water; the catch rolls its `loot_table` (vanilla's
`minecraft:gameplay/fishing` when None) and `lure_ticks` replaces vanilla's
100 to 600 tick wait when set. It is asked on the server and on the clients
that draw the bobber, so both move it alike. No answer keeps vanilla, and
water is never asked about, so vanilla fishing is unchanged.

`horizon-ctl status` shows the hook's loot table. Fixes #12300.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
2026-11-07 23:53:12 -06:00
Horizon-agent 414fcee500 Merge commit '2d29c7f8e954fad2e3eafa4c18c35b4a1147c807' into HEAD 2026-11-08 06:53:10 +01:00
Horizon agent af4a313bc7 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-08 06:51:10 +01:00
Horizon agentandClaude Opus 5.5 9b0c6f48ea Drop the second passengers arm left by the main merge
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:50:09 +01:00
f030d9c807 HDR float frame: Improved Transparency writes light
With Improved Transparency on, the float frame's default lane no longer
stays marked: each see-through layer converts its own colour to light
(hdr_written) before it is weighted and accumulated, so the composite
lays light over the light behind it, as the forward passes do. Text,
shadows, weather and the border keep their source gain, as they do in
the forward passes; cracks keep their multiply blend outside the
accumulation, since it scales the light under them. Only a shader pack
keeps the frame marked now. The identity harness's float OIT session is
no longer an identity case; Improved Transparency with HDR off and on
the 16-bit frame are, and fail unless the option changed the picture.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:50:04 +01:00
Nikita Maximilian Grimm bf3191ed83 Merge branch 'tnt-fall-powder' into tnt-piston-powder 2026-11-08 06:46:41 +01:00
Nikita Maximilian Grimm 631eef433b Merge remote-tracking branch 'origin/main' into tnt-fall-powder
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 06:46:31 +01:00
SolenopsisbotandCodex 7ba6d345b0 perf: reuse decoration shape capture
Retain captured shape-update ticks across decoration queries while preserving ordering and error propagation.

Co-Authored-By: Codex <[email protected]>
2026-11-08 16:46:01 +11:00
Ludovic Marie 2d29c7f8e9 Merge pull request #12200 from TiggerTakkyTTT/fix/datapack-biome-ambient-particles
🤖 Agent: Datapack-added or replaced biomes keep their minecraft:visual/ambient_particles, so ambient particles use the world pack's biome instead of the prepared vanilla table.
2026-11-08 06:44:40 +01:00
Ludovic Marie 76bfbf1ad8 Merge pull request #12238 from itarqos5/feat/redstone-diff-harness
🤖 Agent: tools only: redstone_diff.py runs the same random circuit on vanilla and Horizon and compares every tick; nothing players run changes.
2026-11-08 06:44:36 +01:00
Horizon agent f62c4b546e Merge commit 'ff0ca9ba176d9a12a9d2d0fbf540446942315988' into HEAD 2026-11-08 06:41:09 +01:00
Horizon agent ff0ca9ba17 Merge commit 'c60ed93aff' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 06:40:57 +01:00
Horizon agentandClaude Opus 5.5 c60ed93aff Drop the duplicated orb_data_tests module line from the stack merge
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:37:45 +01:00
Arthur BlackandClaude Opus 5.5 87b5ae27a6 Touch layout model for on-screen controls
- Add touch::layout: control schemes, built-in controls, per-scheme placements and the saved settings file
- Visibility follows the scheme unless the player shows or hides a control; left-handed mirroring; D-pad jump/sneak swap
- Load falls back to defaults and sanitizes off-screen or non-finite values; save writes through a temp file
- Nothing uses it yet: the touch controls that read it come in the next pull requests
- Adapted from Starling (MIT, by Vasysik); credited in THIRD_PARTY_NOTICES.md

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-11-07 21:33:35 -08:00
voidsip 28acf8303c Exercise the title submenu control through debug keys 2026-11-07 23:32:55 -06:00
LokiandAntigravity 0538f95dd8 lan: clamp mob health and refresh max_health on effect changes (fixes #11854)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 14:32:01 +09:00
Horizon agentandClaude Opus 5.5 8f6ef43b0d Sort the test modules (rustfmt)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:32:00 +01:00
Horizon agent 8ee2a1ab6a Merge commit '13217d3b4c0bbaf153871a85477b4010e83ad1f5' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 06:30:06 +01:00
Horizon agentandClaude Opus 5.5 13217d3b4c Sort the test modules after the main merge (rustfmt)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:29:53 +01:00
Ludovic Marie 2a66276bcc Merge pull request #12232 from Solenopsisbot/perf/defaults-ticket-fastpath
🤖 Agent: perf: unchanged ticket sources skip renormalisation, and item defaults load once per root; I fixed the fast path to compare the dragon arena too (it broke the watched-dragon-arena test).
2026-11-08 06:29:15 +01:00
Horizon agent 82ed0f3ac8 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 06:29:14 +01:00
Ludovic Marie 38f9ce31e7 Merge pull request #12210 from rdbtCVS/perf/streaming-cover
🤖 Agent: update records that the cover was refreshed at its start, so each waiting chunk no longer compares the ~200-entry extra areas list every tick.
2026-11-08 06:29:11 +01:00
Horizon agentandClaude Opus 5.5 67f84002d3 redstone_diff: start the game through lab.game_env
tests/test_game_home.py requires every script that starts the game to use it
(scratch folders, no audio) instead of setting HORIZON_NO_AUDIO by hand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:28:25 +01:00
Horizon agentandClaude Opus 5.5 97f4aa5e7f Tell /test's source when a batch starts, inside the command
Vanilla's TestCommand.trackAndStartRunner adds a TestBatchSummaryDisplayer
holding the command's source; GameTestRunner.start runs the first batch
inside the command, and testBatchStarting sends
"Starting environment <env> batch <n>" with sendSuccess(.., true). Horizon
queued the line as a server console broadcast for the next tick, so `/test
run` answered only "Running 1 test(s)..." (world_test_def 94/96,
world_test_def_lifecycle 6 of its 10 differing records, as #11992 noted).

A runner a command starts now sends the line through that command's source
while the command runs (`feedback`). A test instance block's runner keeps the
server's source (TestInstanceBlockEntity.runTest), as do later batches, which
start from the tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:28:23 +01:00
Horizon agentandClaude Opus 5.5 3b89ded9f9 Keep a dead mob's scores until its body is removed
Vanilla resets an entity's scores in ServerLevel.EntityCallbacks.onDestroyed
(Scoreboard.entityRemoved), when the entity is removed for good: a mob's body
twenty ticks after LivingEntity.die, never when it is unloaded. Horizon reset
them in mob_died, at the death. A mob killed in a frozen world and then
unloaded kept its scores in vanilla and lost them here, so a later
`/scoreboard players list` named 32 fewer holders (command_exec, after
#12185's shulker command damage case).

mob_died no longer resets them; score_destroyed_entities already resets every
removed entity's scores once a tick, and keeps an unloaded one's.

#12185's 32 shulkers had random UUIDs, which that list then named: Java and
Horizon could never print the same list. The case now gives them fixed
UUIDs, so the record is exact again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:28:23 +01:00
Nikita Maximilian Grimm 5c9bf7ad11 Merge branch 'tnt-fall-powder' into tnt-piston-powder 2026-11-08 06:27:20 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 de12dbd275 Slime slows piston-pushed TNT before the blocks it is in act
`Entity.applyEffectsFromBlocks` runs the supporting block's `stepOn`
before the blocks the entity is inside, so slime slows TNT before a
bubble column beside it lifts it. TNT's own tick had them the other
way round, and the piston push shares the same steps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:27:20 +01:00
J5CaseyandClaude Opus 5.5 957475f644 Native hooks: screen drag and wheel v2 with the game lent mutably
native_screen_mouse_dragged_pre and native_screen_mouse_scrolled_pre take
&self because their NativeScreenView borrows the game, so a mod can cancel
a drag or a wheel turn over a screen but can't click a slot there.
Mouse Tweaks-style mods (Mouse Tricks) inject into the private
inventory_drag_move and tweak_scroll instead and pin one Horizon commit.

Add native_screen_mouse_dragged_pre_v2 and
native_screen_mouse_scrolled_pre_v2 with &mut Game: the view's id and
pointer come by value, the rest as v1's. Each fires at v1's point, just
before v1, which hears the screen as v2 left it and nothing of an input
v2 canceled. Cancelling does what cancelling v1 does. v1 is unchanged.

Fragment tools/native-hook-fragments/screen_mouse_input_v2.json; native
bodies filled by `cargo run -p xtask -- mod-api`.

Fixes #12286

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
2026-11-07 23:26:15 -06:00
Horizon agentandClaude Opus 5.5 33e0bf2aec Use append for the later cells (clippy::extend_with_drain)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:26:10 +01:00
Horizon agent dca25b7662 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 06:26:10 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 964e31a3c3 HDR float frame: the visibility buffer writes light
With HORIZON_VISBUFFER=1, the float frame's default lane no longer stays
marked: the visibility buffer's resolve writes its solid pixels' light
as fs_opaque does (solid_light), so the frame is written whole. The
identity harness's float visbuffer session is no longer an identity
case; the visibility buffer with HDR off and on the 16-bit frame are,
drawn flat so that the buffer runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:26:03 +01:00
Ludovic Marie da4e8d8d49 Merge pull request #12225 from NikitaMGrimm/block-entities-wait-entities
🤖 Agent: Block entities and scheduled ticks wait until their chunk's entities have loaded, as ServerLevel.isPositionTickingWithEntitiesLoaded gates them; the entities_pending set is cloned once per call.
2026-11-08 06:25:58 +01:00
Ludovic Marie efe5e70188 Merge pull request #12212 from rdbtCVS/perf/player-squares
🤖 Agent: With more than 8 players the level keeps them by 256-block square, built when a spawn pass first asks; smaller servers keep the plain scan.
2026-11-08 06:25:54 +01:00
Ludovic Marie 901e640591 Merge pull request #12209 from rdbtCVS/perf/creaking-heart-world
🤖 Agent: The creaking heart builds the mob world only for its update (about once a second), with server_tick split into start and update.
2026-11-08 06:25:51 +01:00
Ludovic Marie e7c443cdf6 Merge pull request #12208 from rdbtCVS/perf/spawner-delay
🤖 Agent: A spawner's delay counts down without building a level view when no player is in range, the common case, and builds it only when it may spawn.
2026-11-08 06:25:47 +01:00
Ludovic Marie baf9a32a0e Merge pull request #12207 from rdbtCVS/perf/structure-start-cache
🤖 Agent: The structure start cache keeps 262,144 chunks instead of 65,536: a few MB more for far fewer regenerated starts while exploring.
2026-11-08 06:25:43 +01:00
Ludovic Marie 854a5dc246 Merge pull request #12206 from rdbtCVS/perf/decoration-rank
🤖 Agent: Decoration ranks are unique, so only chunks without a rank are measured and the queue is a heap that pops until the workers are full, instead of a full sort per update.
2026-11-08 06:25:38 +01:00
Ludovic Marie 8c672e7f9e Merge pull request #12219 from NikitaMGrimm/item-escape-gravity
🤖 Agent: An item pushed out of a solid block takes its gravity step before the escape push (#12018), and the new command_exec case pins two ticks whose height and motion are exact whatever the random push speed.
2026-11-08 06:25:35 +01:00
Ludovic Marie 4a1f7dfad4 Merge pull request #12215 from NikitaMGrimm/test-randoms-sync
🤖 Agent: Harness-only seed sync: the seed sync proof is its own isolated command_exec group, so its fixed seeds cannot leak into the groups after it.
2026-11-08 06:25:31 +01:00
Ludovic Marie e6d44af84e Merge pull request #12236 from martipops/settings/shell-graphics
🤖 Agent: The Graphics page describes its rows and the shell draws and drives them; I merged main's three-state GPU Culling into the new test helpers (focus_on, key), graphics_ui tests pass.
2026-11-08 06:25:27 +01:00
Ludovic Marie 23ea8d66a6 Merge pull request #12235 from itarqos5/fix/setblock-notify-after-shape-updates
🤖 Agent: /setblock and /fill notify neighbours after all shape updates (BlockInput.place, ServerLevel.updateNeighboursOnBlockSet), so a wire beside a block that pops off ends as a cross; I merged main (fill's note-block normalisation kept) and marked the two resource-backed tests #[ignore], as the other HORIZON_TEST_RESOURCES tests are, since CI has no resources.
2026-11-08 06:25:24 +01:00
Nikita Maximilian Grimm 8abbc6b9e4 Merge remote-tracking branch 'origin/main' into tnt-fall-powder 2026-11-08 06:23:27 +01:00
voidsip f2fe9c1dfe Keep title submenu debug-options handling unchanged 2026-11-07 23:22:01 -06:00
Nikita Maximilian GrimmandClaude Opus 5.5 6a11db7ef7 Frame identity harness: the float visibility buffer session draws flat
The visibility buffer draws only frames drawn flat, which frames without
a pack are by default only on Metal and some Intel drivers, so elsewhere
float_visbuffer compared the forward passes. It now sets
HORIZON_FLAT_DRAWS=1 and fails unless the game built the buffer. The
module docs list the sessions added on this branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:19:47 +01:00
voidsip 7a5d306949 Show title-screen debug information and options 2026-11-07 23:19:38 -06:00
Nikita Maximilian GrimmandClaude Opus 5.5 66d06e23f7 A spent piston push still applies TNT's block effects
Once `limitPistonMovement` has used the tick's 0.51, `Entity.move`
returns at once, but `moveEntityByPiston` still runs
`applyEffectsFromBlocks`, so a second piston's push still lets slime
under the TNT slow it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 06:18:11 +01:00
voidsip 417677fdda Match cauldron contact tolerances and repeated damage callbacks 2026-11-07 23:15:03 -06:00
k3yhandClaude Opus 5.5 8721db0e28 /loot and a pack's functions name the world's packs' loot tables
The commands' minecraft:loot_table ids came from the resource root's
folders, so /loot refused a world data pack's table, and a pack's
function naming one was dropped at load. ResourceOrIdArgument looks the
id up in the server's registries, which hold the world's packs. The ids
are now the world's loot table map (Items::loot_table_ids): set when the
world opens and again before admit_store parses the packs' functions.

New command corpus group: loot world pack tables.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:09:20 -05:00
masa-masa-0314andZCode 96f8110001 Worlds: the single-player owner is the account's UUID, old worlds migrate
`IntegratedServer`'s constructor sets `setSingleplayerProfile(minecraft.getGameProfile())`,
and `ServerLoginPacketListenerImpl.handleHello` gives a login whose name matches that
profile the profile itself; the local memory hello names the account's profile id
(`Minecraft.handleClientLogin`'s `ServerboundHelloPacket(getUser().getName(),
getUser().getProfileId())`). The integrated server's owner, its gate/op entry and its
login are now the account's UUID; an offline start keeps the offline profile.

`MinecraftServer.saveAllChunks` saves with `saveDataTag(worldData,
getSingleplayerProfile().id())`: level.dat's `singleplayer_uuid` follows the owner on
every save. A world without one (a Realms download; `FileDownload.untarGzipArchive`
drops it) then loads the owner's `players/data/<account uuid>.dat` instead of starting
a new player.

Worlds older Horizon builds made name their player by the offline UUID everywhere:
level.dat's `singleplayer_uuid`, the player file and its `UUID`, and entity references
(`TamableAnimal`/`AbstractHorse`/`Projectile`/`AreaEffectCloud`/`EvokerFangs` `Owner`,
`ItemEntity`'s `Owner`/`Thrower`, `Fox`'s `Trusted`, `NeutralMob`'s `angry_at`,
`Leashable`'s `leash.UUID`). The first open under the account moves the player file to
the account's UUID (the newer of the two when both exist), rewrites level.dat before the
first session save, and rewrites each entity as its chunk loads, so the next save keeps
the account's UUID. A world renamed since it was made stays as it is: the old name its
offline UUID came from is not stored, so it cannot be identified.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 13:06:42 +08:00
Nikita Maximilian Grimm 89d7b8e65b Merge remote-tracking branch 'origin/main' into portal-ticket-persistence 2026-11-08 06:03:18 +01:00
Nikita Maximilian Grimm 190c17fc7a Merge branch 'tnt-fall-powder' into tnt-piston-powder 2026-11-08 06:01:36 +01:00
Ludovic Marie 9e1abc64d1 Merge pull request #12169 from abit-foggy/codex/studio-c1-c2-topbar-timeline
🤖 Agent: Studio's top bar and timeline are Horizon-only and off unless Studio is opened; the only gameplay touch is the fov/play_input hooks, which read the new state only while Studio is open.
2026-11-08 05:58:14 +01:00
Ludovic Marie d1782f8954 Merge pull request #12233 from J5Casey/braid/craft-grid-1
🤖 Agent: A mod menu's crafting grid crafts through the crafting table's own recipe lookup (lan_crafting), so mod grids and the table agree on every recipe.
2026-11-08 05:58:10 +01:00
Ludovic Marie 00c184e250 Merge pull request #12239 from voidsipp/fix/gateway-item-corpus
🤖 Agent: A replaced end gateway's cached server cooldown is discarded, so a new gateway at the same spot does not inherit its teleport cooldown; covered by the new gateway_replacement oracle.
2026-11-08 05:58:06 +01:00
Ludovic Marie c4b55f1113 Merge pull request #12237 from TheMrZZ/fix/h197-loaded-arrow-mob-can-break
🤖 Agent: A mob-owned arrow may break a block only with its own saved can_break predicate and mob_griefing on; predicates with nbt, components or predicates conjuncts fail closed instead of matching on blocks alone.
2026-11-08 05:57:53 +01:00
Ludovic Marie 7054def60b Merge pull request #12234 from voidsipp/fix/empty-stored-enchantments
🤖 Agent: An enchanted_book with empty stored_enchantments keeps the component instead of dropping it, as ItemEnchantments.EMPTY still saves.
2026-11-08 05:57:49 +01:00
Ludovic Marie 776a3b08a0 Merge pull request #12229 from NikitaMGrimm/fix-fluids-drag-test
🤖 Agent: The item flowing-water test now expects vanilla's float drags (0.98F/0.8F widened to double), the values Entity.handleFluid really produces.
2026-11-08 05:57:45 +01:00
Horizon agent f79643821e Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:57:41 +01:00
Ludovic Marie 3b52978005 Merge pull request #12224 from Solenopsisbot/perf/hotpath-microalloc
🤖 Agent: Rebased onto main: the hot-path buffers (command args, protocol buf, redstone, json folder) are reused instead of reallocated, with the args tests covering the reused path.
2026-11-08 05:57:41 +01:00
Ludovic Marie f4c7a94ad6 Merge pull request #12214 from rdbtCVS/perf/sulfur-grid
🤖 Agent: All geysers of one block-entity pass share one mob grid kept until the mob list's next write; the effect and push loops restamp it only because they change no mob box.
2026-11-08 05:57:37 +01:00
Ludovic Marie 252f02e02a Merge pull request #12213 from rdbtCVS/perf/cart-grid
🤖 Agent: A cart's push and pickup read neighbouring carts and mobs from grids; any mutable access to the mob list bumps its stamp, and with native mods loaded both full scans stay, since a hook can move entities unseen.
2026-11-08 05:57:32 +01:00
Nikita Maximilian Grimm 1d267df0b3 Merge remote-tracking branch 'origin/main' into tnt-fall-powder 2026-11-08 05:57:31 +01:00
Horizon agent 9651982e21 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:57:28 +01:00
Ludovic Marie 3e3a0ffa10 Merge pull request #12216 from rdbtCVS/fix/guest-hit-mob
🤖 Agent: A guest's effect and sting hits now credit the mob that swung, matching LivingEntity.hurtServer's attacker; stacked on #12204, lands after it.
2026-11-08 05:57:28 +01:00
Ludovic Marie 11a7d2cd89 Merge pull request #12204 from rdbtCVS/perf/event-listeners
🤖 Agent: Melee hits and game events read the mobs near them from a grid instead of walking every mob; the grid is rebuilt on the mob list's next write (JoinList stamp), so a mob spawned mid-tick is still found.
2026-11-08 05:57:25 +01:00
Horizon agent 2415fb9e91 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands/execute.rs
#	tools/lan_e2e.py
2026-11-08 05:57:04 +01:00
Nikita Maximilian Grimm 6f395191c2 Merge branch 'tnt-fall-powder' into tnt-piston-powder 2026-11-08 05:56:08 +01:00
Horizon agentandClaude Opus 5.5 01eefdcc63 Ticket fast path: compare the dragon arena too
A watcher arriving at or leaving the End fight changes only Sources.dragon;
the unchanged-sources skip ignored it, so the arena never got (or lost) its
TicketType.DRAGON levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:56:02 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0f69036171 Stream restored portal tickets' chunks during the startup wait
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:56:00 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 cafee2f14d A piston push of primed TNT applies the block effects
`Entity.move(MoverType.PISTON)` uses up TNT's pending stuck multiplier
(without scaling the push), reads its fall for powder snow's top, adds
to that fall and resets it on landing, and applies the block speed
factor. `moveEntityByPiston` then runs `applyEffectsFromBlocks`, so
powder snow it is pushed into makes it stuck, and slime under it slows
it. Horizon did none of this, so in a chunk that does not tick
entities pushed TNT kept its motion in powder snow and on soul sand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:55:41 +01:00
voidsip 2ed136b137 Keep LAN default-spawn packets tied to the world spawn 2026-11-07 22:55:02 -06:00
Horizon agent e299e931f5 Merge commit '4cbca5702609168ee27fff1645b29b5fa196a974' into HEAD
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-08 05:54:38 +01:00
Horizon agent 4cbca57026 Merge commit '183513311337bd8c4d95f9f915f03084b58534c5' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 05:54:33 +01:00
Horizon agent 1835133113 Merge commit '1d533463ca8f620c87474a32d1f817e4c4f5489c' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 05:54:25 +01:00
Horizon agent 1d533463ca Merge commit '87dfb297ef73a16d1967e981f4732fda1c5cc0e2' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 05:54:15 +01:00
Horizon agentandClaude Opus 5.5 87dfb297ef Sort the test modules (rustfmt)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:54:04 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 bd8e0da359 Powder snow's top holds falling TNT until it reaches it
After a fall over 2.5, `PowderSnowBlock.getEntityInsideCollisionShape`
is the 0.9-high top, so the snow only makes the TNT stuck once a step
reaches that top. A TNT passing 2.5 just above the snow used to be made
stuck early, lose its fall and sink through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:53:44 +01:00
J5CaseyandClaude Opus 5.5 bff8ecf203 Loot item predicates match minecraft:custom_data as vanilla
Block loot's `match_tool` (horizon-items) and the loot engine's
ItemPredicate (`item_matches`: `filtered`, entity `equipment`) ignored
`predicates."minecraft:custom_data"`, so any stack passed. They now test
it as `CustomDataPredicate` does (NbtPredicate.matches ->
CustomData.matchedBy -> NbtUtils.compareNbt(pattern, data, true)): a typed
NBT subset, lists matched element by element, absent custom data read as
`{}`, the pattern SNBT or a JSON object converted as DataFixerUpper does.

`compare_nbt` moves down to horizon-world (re-exported from nbt_path) so
the loot engine can use it, and a killer's equipment keeps its custom data
for entity loot predicates.

New harness loot_custom_data: a data pack's dirt table with a pool per
pattern behind match_tool, `loot give @s mine` with sticks carrying
different custom data, 66/66 records equal to vanilla. The loot engine's
`filtered` path gets the same vanilla matrix as a horizon-world test.

Fixes #12196

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
2026-11-07 22:47:46 -06:00
k3yhandClaude Opus 5.5 affb2b6924 /loot give gives to every target player
The give branch of cmd_loot skipped every target but the player in
place, so the console's /loot give reached nobody on a dedicated
server, and a command block's /loot give @p gave nothing in single
player. Each target now goes through for_player, as cmd_give does
(LootCommand.playerGive gives to every target ServerPlayer).

New lan_e2e check: loot_give_guest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:45:11 -05:00
J5CaseyandClaude Opus 5.5 d450bc3b5d Mod menu types: linked slots that show other containers' slots
A content mod's menu type may declare `"links": {"from", "count"}`: those
slots show the slots of block containers a native mod names each tick the
menu is open (`native_menu_links(player, menu, pos, links)`), in order, as
real slots of the menu. Clicks, shift-clicks and drags run on the server's
menu engine and move the containers' own items, a changed container is
saved and its comparators update, and the block entity never holds the
linked slots. Linked slots with nothing behind them hold nothing and are
left out of clicks, as a hidden slot is.

This is what a crafting station's side panel needs (Tinkers' Construct's
shows the chest beside it); a tab per neighbour is a menu button that
changes what the mod links for that player.

Part of #12198.

Claude-Session: https://claude.ai/code/session_01KtNXhvTSpY15Rjzwuqh8Nf
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:44:11 -06:00
voidsip 45d46d46f4 Keep the panorama behind server configuration dialogs 2026-11-07 22:37:41 -06:00
voidsip 3ac68d69d3 Keep option button cursors tied to pointer hover 2026-11-07 22:34:17 -06:00
Shreyas 22c6c667a2 Clamp a mob's absorption to maximum on death protection and milk removal 2026-11-08 10:00:00 +05:30
Horizon agent 4635eed508 Merge commit '01c1e661687793318d5487918b18ae4dfc5b05c7' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 05:29:11 +01:00
Horizon agent 01c1e66168 Merge commit 'c7fc1c994f9569dc99674cdc40522f2b25826697' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 05:29:07 +01:00
Horizon agent c7fc1c994f Merge commit '4eba8a6991243314c98098e99f32ea0a929e387c' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 05:29:04 +01:00
Horizon agent 4eba8a6991 Merge commit '01fcfc484950ac5a1417be5b5f82488184a7785e' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 05:29:01 +01:00
Horizon agent 01fcfc4849 Merge commit '13dc865c9a591265d0a2d12bbd2845f813a6642a' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 05:28:51 +01:00
AkirabaneandClaude Opus 5.5 391da75bf3 Riders turn like vanilla's: vehicle body only where vanilla copies it
position_riders gave every mob rider of a mob its vehicle's body yaw each
tick. 26.3 does so only in AbstractHorse.positionRider and
Chicken.positionRider, and in AbstractSkeleton.rideTick and Drowned.rideTick
for a rider steering a PathfinderMob; any other rider's body turns on its
own. A rider of a living vehicle is drawn by LivingEntityRenderer.solveBodyRot
from the vehicle's body, which only the window's player model did: mobs now
go through the same function.

Fixes #9335.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:28:25 +01:00
Horizon agent 13dc865c9a Merge commit '80d929f7d64a7fd92ede64042392f676df4387b7' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-08 05:28:07 +01:00
Horizon agent 80d929f7d6 Merge commit '75cd96be536365ec99573e48e08411832ab01701' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 05:27:48 +01:00
Horizon agent 75cd96be53 Merge commit '5bf7ce508d86204c6fb42a528a2ee05ce85da43a' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands.rs
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 05:27:34 +01:00
Horizon agent 5bf7ce508d Merge commit '4dbb00502ff162b4ac3472a3e7c85c5d8d88883e' into HEAD 2026-11-08 05:27:16 +01:00
SolenopsisbotandCodex d91706378e perf: reuse block tick scratch buffers
Retain per-tick block contact, random tick, and entity-inside vectors on Game while restoring scratch across result exits and preserving vanilla ordering.

Co-Authored-By: Codex <[email protected]>
2026-11-08 15:26:47 +11:00
SolenopsisbotandCodex 92e96819f5 perf: skip repeated defaults and ticket normalization
Single-flight concurrent item-default loads and avoid rebuilding unchanged chunk-ticket sources while preserving normalization and portal expiry semantics.

Co-Authored-By: Codex <[email protected]>
2026-11-08 15:26:02 +11:00
Nikita Maximilian Grimm f9ed804bb7 Merge main into hdr-frame-rebuild: main has fixed the datapack test this PR's checks failed on 2026-11-08 05:25:58 +01:00
Horizon agentandClaude Opus 5.5 2394ee861d Mark the setblock order tests as needing prepared resources
CI has no HORIZON_TEST_RESOURCES, so they panicked there; like the other
resource-backed game tests they now run with --ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:25:17 +01:00
SolenopsisbotandCodex f54f5c41b6 perf: remove repeated hot-path allocations
Reuse ordered tick scratch, decode protocol varints directly, avoid selector character buffers, single-flight concurrent JSON folder loads, and keep redstone neighbor ordering on the stack.

Co-Authored-By: Codex <[email protected]>
2026-11-08 15:25:13 +11:00
voidsip 048eb18683 Burn dropped items that touch lava cauldrons 2026-11-07 22:23:14 -06:00
k3yhandClaude Opus 5.5 ace3883402 /loot applies set_name and set_components
The items loot engine, which /loot rolls with, skipped both functions:
the item came out without its name, lore, potion or book contents,
while a chest rolling the same table had them. set_name sets the
component its target names, as a chest's roll does. set_components
applies the patch as ItemStack.applyComponents does and reads the
stack back as saved components are read, so a potion's contents make
it a potion. The feedback names each drop by getDisplayName.

New command corpus group: loot set name and components.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:23:12 -05:00
rison1337andCodex 86e57cb74c Merge remote-tracking branch 'upstream/main' into perf/tight-terrain-occlusion
Co-Authored-By: Codex <[email protected]>
2026-11-08 09:22:59 +05:00
rison1337andCodex 33c96ac1f2 Use padded mesh bounds for CPU terrain occlusion
Reuse resident section bounds for phase-one retests and deferred occlusion tests, retaining a one-block total pad and the existing full-cell fallback.

Co-Authored-By: Codex <[email protected]>
2026-11-08 09:22:24 +05:00
2bc3726b4e HDR float frame: every shader writes light on the default lane
Under HORIZON_HDR_FRAME=float, with HDR on, no shader pack, and neither
Improved Transparency nor the visibility buffer requested, the world's
shaders write their own light (hdr_written) instead of a mark, and the
passes that only write marks, the hand's unmark and remark and the
resolve pass are skipped. A frame is all written or all marked. TAA's
history is dropped when that or the HDR parameters it holds change.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:22:16 +01:00
Horizon agent 3a05ee18c6 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/README.md
2026-11-08 05:22:03 +01:00
Horizon agent e1f9a8c096 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client-render/src/graphics_ui.rs
2026-11-08 05:22:03 +01:00
Horizon agent 0b2270dd45 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands.rs
2026-11-08 05:22:02 +01:00
Ludovic Marie 4dbb00502f Merge pull request #12110 from kacpherek/parity/batch-b
🤖 Agent: Fixed: `MobWorld` has no `end` field, so the fox village stroll now reads `dimension_type.has_fixed_time`, as `Level.isBrightOutside` does; it builds with main merged (lib and tests).
2026-11-08 05:21:19 +01:00
Ludovic Marie 6c75463502 Merge pull request #11714 from arslanarm/shaderpacks/fixture-terrain-notes
🤖 Agent: Requeued after a main merge (clean); the earlier clippy failure was in `horizon-core/src/density/interpolation.rs`, which this PR doesn't touch.
2026-11-08 05:21:15 +01:00
Horizon agent a869493ff4 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 05:21:14 +01:00
Horizon agent c471036d29 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 05:21:11 +01:00
Ludovic Marie 41de65db6e Merge pull request #11764 from rdbtCVS/fix/far-ring-brake-thresholds
🤖 Agent: Requeued after a main merge (clean); the earlier failure was main's native mod API stamp (`native_level_play_sound`) at that moment, not the brake thresholds.
2026-11-08 05:21:11 +01:00
Horizon agent 1c15d2b68c Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:21:03 +01:00
voidsip ed656ba6cf Run block events on ticketed chunks before they start ticking 2026-11-07 22:21:02 -06:00
Horizon agent 0eb8b8ec26 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 05:21:01 +01:00
Horizon agent 1d0621ac38 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:50 +01:00
Horizon agent 1ebc813852 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:47 +01:00
Horizon agent 59994ad2a2 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:45 +01:00
Horizon agent 9a62e99376 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:42 +01:00
Horizon agent 0b10ddd665 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:39 +01:00
Horizon agent 406ac72d0a Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 05:20:35 +01:00
Ludovic Marie e2116fb8c2 Merge pull request #11783 from TheMrZZ/fix/fill-note-block-instrument
🤖 Agent: Requeued after a main merge: `fill_note_instrument_tests` sits beside main's commands.rs tests.
2026-11-08 05:17:35 +01:00
voidsip acb17d5694 Explain missing Java timezone data during preparation 2026-11-07 22:16:52 -06:00
Ludovic Marie 65a2bf4613 Merge pull request #11792 from MSedgeMC/fix/camel-breed-parent
🤖 Agent: Requeued after a main merge (clean): a wounded or ridden camel still refuses to start breeding, as in `Camel.canFallInLove`.
2026-11-08 05:16:30 +01:00
Ludovic Marie eb69b3986b Merge pull request #11860 from Joansitoh/fix/11853-patch-log-scroll
🤖 Agent: Requeued after a main merge; the earlier verify failure was a 60 s timeout in `test_entity_shadow_ownership` on a loaded runner, not this PR's launcher scroll change.
2026-11-08 05:16:26 +01:00
Ludovic Marie 389224abec Merge pull request #11879 from TheMrZZ/fix/h155-fork-function-feedback
🤖 Agent: Requeued after a main merge: the fork-feedback corpus case and its flag are kept beside main's new command corpus cases.
2026-11-08 05:16:22 +01:00
Ludovic Marie 3bea613053 Merge pull request #11890 from MrCloudy2/fix/rider-vehicle-teleport
🤖 Agent: Requeued after a main merge: `lan_e2e.py` keeps both sides' checks; the codec base still isn't reset by `handleTeleportEntity`.
2026-11-08 05:16:18 +01:00
Ludovic Marie a9d293cb18 Merge pull request #12012 from TheMrZZ/fix/h262-enderman-absolute-anger
🤖 Agent: Requeued after a main merge: only persist.rs's test modules conflicted; the absolute anger time read is unchanged.
2026-11-08 05:16:15 +01:00
Ludovic Marie cd8ebdff23 Merge pull request #12032 from MSedgeMC/fix/melee-home-chase
🤖 Agent: Requeued after a main merge: `melee_continue_tests` sits beside main's new goals.rs tests; the home check in `canContinueToUse` is unchanged.
2026-11-08 05:16:11 +01:00
Ludovic Marie 6edc5de20f Merge pull request #12162 from KBroneZ/claude/carry-1-20-folders
🤖 Agent: Requeued after a main merge: in report.rs, main's installed-data-pack branch comes before the carried-mod branch, which still marks 1.20 layouts Partial; port-pack checks clean.
2026-11-08 05:16:08 +01:00
Ludovic Marie 314edb02fc Merge pull request #12166 from KBroneZ/claude/datapack-support-text
🤖 Agent: Requeued after a main merge: all 86 lang files were merged key by key with main, keeping main's new strings and this PR's data pack text.
2026-11-08 05:16:04 +01:00
Ludovic Marie 5e2b6c675f Merge pull request #12177 from TheMrZZ/fix/h171-inline-feature-parse
🤖 Agent: Requeued after a main merge: only game.rs's test-module list conflicted (`inline_feature_parse_tests` beside main's new ones).
2026-11-08 05:16:00 +01:00
Ludovic Marie 01c53f23d2 Merge pull request #12193 from UnQuis/feat/opaque-ice-clean
🤖 Agent: Requeued after a main merge: the `graphics_ui.rs` conflict kept Opaque Ice's switch, and main's tri-state GPU culling no longer has a plain on/off arm there.
2026-11-08 05:15:57 +01:00
voidsip 40aed431b6 Keep native mod diagnostics in the game log 2026-11-07 22:13:47 -06:00
Nikita Maximilian Grimm 4c3cb9a51b Merge hdr-light-function-main (one mark-to-light function, on main) into the written lane 2026-11-08 05:10:32 +01:00
Nikita Maximilian Grimm 2052b918ec Merge remote-tracking branch 'origin/main' into tnt-fall-powder
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 05:09:46 +01:00
Horizon agentandClaude Opus 5.5 c23236bceb Fox village stroll: isBrightOutside reads the dimension's fixed time
MobWorld has no end field. Level.isBrightOutside is false in every
dimension with a fixed time (the Nether and the End).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:09:46 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4ca78266a5 Primed TNT keeps a fall distance and powder snow holds it
Primed TNT is an Entity in vanilla, so `checkFallDamage` adds each step
down, narrowed to a float, to its double fall_distance, which resets on
landing or in water and is saved and read and written by /data. Powder
snow uses that distance: over 2.5 it holds TNT on a 0.9-high top, and
otherwise TNT goes in and is stuck (0.9/1.5/0.9) like in a cobweb. The
stuck multiplier is used on the next move, which then zeroes motion.

Double and float lists in saved entity data now store -0.0 as 0.0, as
`DoubleTag.valueOf` does, so TNT resting on a block reads Motion 0.0d.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 05:09:14 +01:00
Ludovic Marie cf9569747b Merge pull request #12191 from TheMrZZ/fix/h260-level-world-border
🤖 Agent: queued. The level in place reads its own saved `world_border` when the world opens, before any command, so a first `/worldborder set` is no longer overwritten later.
2026-11-08 05:06:47 +01:00
Ludovic Marie 423abdd863 Merge pull request #10280 from MSedgeMC/fix/jukebox-stop-vibration
🤖 Agent: jukebox stop now captures was_playing before clearing j.playing, so the stop vibration fires once. On main merge 904b3036a, sculk, server_player_sculk, server_wool_vibration and mob_command_damage_vibration pass on playhorizon. command_exec's only 2 mismatches are main's gateway_item case (same 16819/16821 on bare main b13dbf068).
2026-11-08 05:06:42 +01:00
Horizon agent 8f0a7f4494 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-08 05:05:11 +01:00
Horizon agent 4b38881802 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-port-pack/src/report.rs
2026-11-08 05:04:10 +01:00
Horizon agent 7daecbbce2 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client-render/src/graphics_ui.rs
2026-11-08 05:03:21 +01:00
TheMrZZandCodex 1f5cd1e43b Preserve joined-family order in unbounded arbitrary entity selectors
Use the existing insertion traversal only at the qualified global selector branch.

Co-Authored-By: Codex <[email protected]>
2026-11-08 05:02:34 +01:00
Horizon agent d6be40bbf7 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:59:35 +01:00
Horizon agent 1e743c812e Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands.rs
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 04:59:32 +01:00
Horizon agent e020e1d440 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:59:17 +01:00
Vinícius MoreiraandClaude Opus 5.5 01edf84d9f Environment attributes: timelines, keyframe tracks and easings
Ports net.minecraft.world.timeline and the keyframe machinery under it:
Timeline (clock, period, tracks, time markers, validateInternal),
AttributeTrack's codec (a modifier, override by default, and keyframes of
its argument), KeyframeTrack.validateKeyframes and validatePeriod,
KeyframeTrackSampler's looping segments and sampling,
argumentKeyframeLerp for every modifier, EasingType (cubic Bézier with
its Newton-Raphson and bisection solve) and all 32 Ease curves, with
Java's float and double steps (jmath, Mth's sine table). The layer stack
takes timelines as time-based layers that read their clock's ticks.

The new harness environment_timeline compares 3,464 cases with the pinned
client (every easing, the vanilla day, moon and early game tracks, and
synthetic and invalid tracks); environment_system now stacks the real
timelines between the biome and weather layers (36,000 lines).

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:59:14 -03:00
Horizon agent 8540619240 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/commands.rs
2026-11-08 04:59:13 +01:00
Horizon agent 97020c6abe Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:58:47 +01:00
Horizon agent 7cec42ea0b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 04:58:44 +01:00
Horizon agent 75a2643501 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 04:58:37 +01:00
Horizon agent 0a9ce82a0b Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:58:19 +01:00
Horizon agent 9dda4ce381 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-08 04:58:17 +01:00
Horizon agent 4d91ddc86c Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/lan_e2e.py
2026-11-08 04:58:02 +01:00
Horizon agent 6e4c79f33b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 04:57:46 +01:00
Horizon agent e1ae4fb750 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-08 04:57:35 +01:00
Horizon agent d0dc8f1ad6 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs/goals.rs
2026-11-08 04:57:24 +01:00
Horizon agent 65b2833bb9 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 04:57:14 +01:00
Nikita Maximilian Grimm 0972bbea7b Merge remote-tracking branch 'origin/main' into portal-ticket-persistence
# Conflicts:
#	crates/horizon-client/src/game/levels.rs
2026-11-08 04:57:09 +01:00
Horizon agent 8ac6fa92a1 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:57:04 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b1ebe3d6e0 Merge origin/main into fn-tick-time
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:56:57 +01:00
Horizon agent 5452da616f Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 04:56:54 +01:00
Horizon agent c5b9f07b5d Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
#	crates/horizon-client/src/game/commands.rs
#	crates/horizon-client/src/game/commands/data.rs
#	crates/horizon-client/src/game/persist.rs
2026-11-08 04:56:48 +01:00
masa-masa-0314andZCode 8485f4c0ef Realms: the slot pictures, the template pages and the minigame list
- The world slots' pictures follow RealmsWorldSlotButton.State: the
  minigame image (or presets/isles.png when it has none), the empty
  frame, a slot's own template image, then the slot's panorama. The
  atlas now keeps the four slot pictures (the three 1024-tall faces and
  isles.png) at their own size: they were excluded as title/background
  and presets, and even once let through were dropped by the 512-tall
  GUI texture cap.
- WorldTemplate/WorldTemplateList read like JsonUtils' getStringOr,
  getIntOr and getUriNullable: numbers read as their JSON text (some ids
  and player counts are numbers, some counters are strings), and an
  empty link is no link — the row icons are not drawn and no empty URL
  can be opened.
- The template list fetches one page per job and appends it as it
  arrives, chaining the next page until isLastPage or an empty page,
  instead of waiting for every page before drawing the first row.
- The minigame list (RealmsWorldsTab.switchToMinigame) returns to the
  Worlds tab on Back and Escape, not to a reset screen.

Tests: 2250 lib tests pass (71 realms), scoped clippy clean.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 11:56:47 +08:00
Horizon agent 64821ce0bb Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/mobs.rs
2026-11-08 04:56:42 +01:00
Horizon agent f38098f921 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 04:56:27 +01:00
MSedgeMC eaceaeda73 A cat hit by a player panics, and a wolf freezes into one
`TamableAnimal.TamableAnimalPanicGoal` passes a damage tag to `PanicGoal`:
`Wolf` gives it `#panic_environmental_causes`, while `Cat` and `Parrot`
keep the default `#panic_causes`. Horizon's `TamePanic` gated all three on
`fire_ticks > 0`, so only a burning pet panicked: a cat hit by a player or
a wolf freezing stood still where vanilla flees.

`TamePanic` now delegates to the `Panic` it wraps. The wolf's panic is
marked environmental (fire, freezing, cactus, lightning, lava); the cat's
and parrot's take any panic-causing damage.
2026-11-08 03:56:11 +00:00
J5CaseyandClaude Opus 5.5 87bb072d77 Native hook: recipe_sources, extra containers recipe placement takes from
`native_recipe_sources(player, menu, pos, sources)` lets a mod name
block containers (chests and the like, mod containers) that recipe
placement into a mod menu type's crafting grid may also take
ingredients from, after the player's inventory, as a crafting station
takes them from the chests beside it. No answer keeps vanilla's
placement.

Part of #12198.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:53:47 -06:00
Nikita Maximilian GrimmandClaude Opus 5.5 c7389334f1 A new item's own random makes its UUID, bob offset, yaw and motion
Vanilla's Entity takes its UUID from the first two draws of its own random
(Mth.createInsecureUUID); ItemEntity then draws bobOffs and the yaw from it,
and the constructor without a given motion draws the motion last. Horizon gave
every entity a clock-seeded UUID when first asked, drew a new item's motion
from a fresh random's first draws, its bobOffs from the particle random and a
loaded item's from the level's, and left the yaw at 0.

spawn_item_own makes all four from one own random in that order and replaces
dispenser::new_item_motion at its callers. A loaded or summoned item makes its
UUID (unless the tag has one) and bobOffs from its own random, so summoning no
longer shifts the level's random. Primed TNT from a block takes its UUID from
its own random too.

The seed sync proof now reads those UUIDs, the drop's rotation and motion, and
a TNT kick after a summoned item: 8/15 before, 15/15 after.

Fixes #12217

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:53:17 +01:00
itarqos5andClaude Sonnet 5.5 733d34582d tools: redstone_diff.py, whole redstone machines against vanilla one tick at a time
Runs hand-built circuits and random "soups" of redstone components through the real
tick loop in vanilla 26.3 (the bench mod's client, no server, no EULA) and in Horizon
(horizon-ctl), in a frozen world stepped with `tick step 1`, and prints the first tick
where the blocks differ and which ones. Narrowing: --near/--drop reduce a difference
to a few commands.

- RedstoneDiff + ServerLevelTickMixin in the bench mod snapshot the box before the
  first stepped tick and after each.
- `simulation blocks X1 Y1 Z1 X2 Y2 Z2` (horizon-ctl) reads the server's blocks, not
  the window's replica, which lags the server by design.
- agent.rs: `blocks_json`, shared by `blocks` and `simulation blocks`.

Only blocks are compared; entities are recorded, not diffed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-08 03:49:01 +00:00
Nikita Maximilian GrimmandGPT 6.1 Sol aac6bc2c2a HDR: one mark-to-light function for the display pass and the resolve pass
Co-Authored-By: GPT 6.1 Sol <[email protected]>
2026-11-08 04:48:49 +01:00
itarqos5andClaude Sonnet 5.5 af6fc73a51 /setblock and /fill notify neighbours after the shape updates, as vanilla does
Vanilla places a command's block with UPDATE_CLIENTS | UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS
(2|256), so Level.setBlock notifies no neighbour. The shape updates run inside
setBlock; ServerLevel.updateNeighboursOnBlockSet follows afterwards (for /fill,
after every block is in). Horizon placed with UPDATE_ALL (3), so the neighbours
heard of the block before its shape updates ran.

That is visible when the notice makes a block pop off: a comparator with nothing
under it breaks at its first notice and takes an attached wall torch with it. In
vanilla the wire between them had already connected to the torch, so it ends as a
cross; in Horizon the torch was gone first and the wire ended as a dot.

Found by tools/redstone_diff.py (two of 48 random circuits differed on vanilla
26.3 vs Horizon, reduced to four commands); after the change 144 random circuits
over three seeds match vanilla tick for tick.

- Behaviours::update_neighbours_on_block_set ports ServerLevel.updateNeighboursOnBlockSet.
- place_state takes `command`: setblock and fill use the vanilla flags; clone keeps
  its own flow. The strict variants are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-08 03:44:52 +00:00
Ludovic Marie 8bf4597a57 Merge pull request #12167 from J1zeee/fix/12022-piston-push-velocity-restitution
🤖 Agent: queued. Piston restitution bounces the entity's own velocity and runs for server entities only; the ground flag comes from #12168 (queued first).
2026-11-08 04:44:37 +01:00
TheMrZZandCodex gpt-6.1-sol cc3d62a7cd Preserve loaded arrow can_break for Mob block permissions
Carry the actual top-level projectile predicate through ArrowState load/save
and consult the existing matcher only for resolved Mob arrows under both
rules. Refuse unsupported conjuncts and preserve other projectile/drop paths.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-08 04:43:07 +01:00
J5CaseyandClaude Opus 5.5 3042d4068b Recipe placement fills a mod menu type's crafting grid
The recipe book's place packet and Horizon's viewer transfer channel go
to an open mod crafting menu (`lan_mod_craft_apply`) over its block
entity's grid, as a click does, with a ghost recipe when the items are
missing; the client's `native_place_recipe` and `native_transfer_recipe`
accept such a screen, so JEI ports and the Recipes mod fill it as they
fill a crafting table.

Part of #12198.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:41:25 -06:00
Ludovic Marie e1f01aff0b Merge pull request #12161 from TheMrZZ/fix/h141-mannequin-save
🤖 Agent: queued. A mannequin saves `profile`, `hidden_layers`, `main_hand` and `pose` always, and no `Mob` fields, since `Mannequin` is an `Avatar`, not a `Mob`.
2026-11-08 04:40:29 +01:00
Ludovic Marie 9c24961df0 Merge pull request #11830 from TheMrZZ/fix/h122-named-predicate-random
🤖 Agent: queued. Literal `random_chance` predicates in commands now draw from the world's random, as `LootContext` gets `level.getRandom()`; main merged in.
2026-11-08 04:40:25 +01:00
Ludovic Marie bbabaf720b Merge pull request #12000 from masa-masa-0314/feat/realms-ui-batch
🤖 Agent: queued. Realms screens follow vanilla's layout, state and input; the owner-UUID change was reverted (it would orphan tamed pets in Horizon-made worlds), and `under_world` skips parts with ':', with a test.
2026-11-08 04:40:21 +01:00
Ludovic Marie 1e1c77126d Merge pull request #12112 from kacpherek/parity/batch-d
🤖 Agent: queued. Equippable assets for armor, wings, capes and camera overlays; the End flash now uses `random_source::create()` after main dropped `SoundEngine::unseeded`, with a skybox gate in `tick_end_flash`.
2026-11-08 04:40:18 +01:00
Ludovic Marie 933ab57aa1 Merge pull request #12172 from NikitaMGrimm/t3/scheduled-item-tick-pass
🤖 Agent: queued. An item a scheduled block tick drops ticks in that tick's entity pass, as `EntityTickList.forEach` runs after the block ticks; a boat's lily pad drop still waits.
2026-11-08 04:38:14 +01:00
Ludovic Marie c5067a988b Merge pull request #11934 from TheMrZZ/fix/h203-mob-absorption-max
🤖 Agent: queued. Health Boost and Absorption hooks run for every accepted effect and `add_effect` returns its result after them; after #11883, #11959 and #12108.
2026-11-08 04:37:54 +01:00
Ludovic Marie 3936cafd12 Merge pull request #12194 from Solenopsisbot/perf/measurement-tools
🤖 Agent: queued. Startup trace step stats (count, median, p95, RSS) with a test; the compression bench was taken out, since the zlib reuse it measures is not on main.
2026-11-08 04:37:20 +01:00
Ludovic Marie 3b6d2fc635 Merge pull request #11802 from itarqos5/feat/far-tile-packed-quads-clean
🤖 Agent: queued. Far tiles' quads take a quarter of the arena; the decode helpers only tests use are now `#[cfg(test)]`, so clippy is clean.
2026-11-08 04:37:15 +01:00
Ludovic Marie bd6025952f Merge pull request #12138 from itarqos5/feat/auto-gpu-culling
🤖 Agent: queued. GPU culling Auto turns it on where the CPU limits the frame and keeps it only if frame time improves; the client_lib.rs conflict kept both `autotune` and main's `random_source`.
2026-11-08 04:36:42 +01:00
martipops 98abaa9217 Merge remote-tracking branch 'upstream/main' into settings/shell-graphics 2026-11-07 22:36:12 -05:00
Ludovic Marie c6eef4b0b7 Merge pull request #12139 from TheMrZZ/fix/h210-container-name-components
🤖 Agent: queued. Styled container names stay as the `CustomName` tag in the live block model, so a styled chest name survives a break; main's spill test now compares tags.
2026-11-08 04:36:09 +01:00
Ludovic Marie 1fb469a969 Merge pull request #12115 from TheMrZZ/fix/h274-test-guest-view
🤖 Agent: queued. `/test pos` picks the block from a connected source player's view, as `TestCommand` reads `source.getPlayer()`; rustfmt fixed after the main merge.
2026-11-08 04:35:36 +01:00
Ludovic Marie 7adf3992a5 Merge pull request #12197 from martipops/settings/shell-graphics-features
🤖 Agent: queued. Undo, a row's cost and the greyed-row reason are generic shell features with tests; Extras gets no Undo, and Graphics moves onto them in the next PR.
2026-11-08 04:35:15 +01:00
Ludovic Marie 8629067aa4 Merge pull request #11811 from TheMrZZ/fix/chorus-projectile-drops
🤖 Agent: queued. Arrows and snowballs breaking a chorus flower now drop it via `ChorusFlowerBlock.onProjectileHit` (`destroyBlock` with drops); main merged in.
2026-11-08 04:34:42 +01:00
voidsip 9f6d28e257 Discard a removed gateway's server cooldown state 2026-11-07 21:32:07 -06:00
Ludovic Marie ec2c0c8d1f Merge pull request #12011 from NikitaMGrimm/hdr-tone-curve
🤖 Agent: queued. The HDR float frame's tone curve eases highlights into the peak instead of clipping; it is HDR-only, so the SDR path doesn't change.
2026-11-08 04:29:50 +01:00
Ludovic Marie 358c4e0c01 Merge pull request #12150 from RINNECODER/up2/glsl-exact-overload
🤖 Agent: queued. GLSL overload resolution now prefers an exact argument match over a conversion, as the GLSL spec ranks them, so packs that overload on int/float pick the right function.
2026-11-08 04:29:46 +01:00
Ludovic Marie 90b4e7d794 Merge pull request #11821 from vnsmoreira/attributes/layer-stack
🤖 Agent: queued. Environment attributes now come from the layer stack (dimension, then biome with blending, then weather) in `EnvironmentAttributeSystem`'s order; main merged in since the review.
2026-11-08 04:27:14 +01:00
Ludovic Marie 989afe1c00 Merge pull request #11870 from IwantHappiness/translate-ru-lang
🤖 Agent: queued. The remaining Russian strings in the launcher and the game are translated, and the keys match en_us, so no string falls back to English.
2026-11-08 04:27:10 +01:00
Horizon agent 0dc90c1d29 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:26:07 +01:00
Horizon agent e67b56eec6 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:25:27 +01:00
Horizon agent 3e4f2a7305 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:25:21 +01:00
Horizon agent f9b2840d07 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:24:42 +01:00
Horizon agent 1c9380fa56 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:24:12 +01:00
voidsip 6e5cd517d7 Elide default stored enchantments when saving enchanted books 2026-11-07 21:23:25 -06:00
Ludovic Marie 5a0906f5f4 Merge pull request #12051 from ilguerro/perf/11126-structure-starts-templates
🤖 Agent: queued. A template start is read back only when every child decodes; one unreadable child sends the whole start to #11652's regeneration, so old saves get nothing worse. I couldn't rerun the ignored `saved_starts_match_generated_layouts` here (no prepared resources on this host); your 7/7 run and the parity run decide.
2026-11-08 04:22:28 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b9404f32c3 Tick functions run before the game time moves on, and before that tick's scheduled functions
MinecraftServer.tickChildren runs the #minecraft:load and #minecraft:tick
functions before the world clocks and the levels tick; the Overworld's
ServerLevel.tickTime then raises the game time and runs the scheduled
functions due at the new time. Horizon raised the game time, the weather and
the day clock first, then ran the scheduled functions and the tick functions
after them, so a tick function read both clocks one tick ahead and a
`schedule ... 1t` from it waited a tick more.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:19:23 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 951fe53102 Seed sync: run the proof group as a harness of its own
A script that syncs runs on fixed seeds to its end, so in the full command-exec
corpus every group after the proof drew from them, at seeds fixed by whatever
came before. With one more proof case (#12217), "experience orb stuck", whose
orb rarely ends otherwise on random seeds, landed on such a seed and failed on
every run. The group is isolated now and runs as tools/test_seed_sync_parity.py;
the main corpus runs without the agent, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:16:28 +01:00
Nikita Maximilian Grimm 0b30eba4e0 Merge remote-tracking branch 'origin/main' into tnt-size-eye 2026-11-08 04:14:43 +01:00
Nikita Maximilian Grimm 7abea17aa3 Merge remote-tracking branch 'origin/main' into fix-fluids-drag-test 2026-11-08 04:06:21 +01:00
Ludovic Marie 0f56e3165a Merge pull request #12227 from horizon-rust/fix/keyring-timeout-12179
🤖 Agent: keyring calls now give up after 20 s on a thread of their own; a call still stuck makes later ones fail at once, so the account lock is never held forever (tested with a keyring that never answers).
2026-11-08 04:05:55 +01:00
Horizon agent 3943b0e8a7 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:32 +01:00
Horizon agent f7a3c0f2e4 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:24 +01:00
Horizon agent 92b1ae1132 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:20 +01:00
Horizon agent 316752b706 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:16 +01:00
Horizon agent aa3919d3ae Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:15 +01:00
Horizon agent a1afb474a5 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:05:03 +01:00
Horizon agent fdb6ad8b8f Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:04:55 +01:00
Horizon agent 7f505e07dd Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:04:36 +01:00
Horizon agentandClaude Opus 5.5 98b4d049fa launcher: give up on a keyring that never answers (#12179)
A Secret Service daemon that never answers (two keyrings racing for the
D-Bus name, a locked collection with no working prompter) held the
account lock forever: Play stayed on "Starting…" and printed nothing.

Every keychain call now runs on a thread and gives up after 20 s
(Bounded): on Linux the secrets file takes writes as before, and a
sign-in that can't be read says so instead of "no owner". While a
given-up call is still stuck, later calls fail at once. Play prints one
stderr line per step, and `account list` prints why a check failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:04:29 +01:00
Horizon agent 32faed9621 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 04:04:03 +01:00
Horizon agent 0270b0133d Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 04:03:56 +01:00
Horizon agent 1b7cec46d9 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 04:03:52 +01:00
Horizon agent 6922878d94 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 04:03:51 +01:00
Horizon agent 9a442e5382 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
#	tools/test_command_exec_parity.py
2026-11-08 04:03:42 +01:00
Horizon agent af61f6751e Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-08 04:03:35 +01:00
abit-foggyandCodex a4df995446 fix(studio): localize timeline marker labels
Co-Authored-By: Codex <[email protected]>
2026-11-07 17:03:20 -10:00
Ludovic Marie 6c2498236d Merge pull request #12222 from horizon-rust/fix/new-world-packs-test-meta
🤖 Agent: queued first. Main's correctness job fails on this test since #12086 met the folder metadata gate (84f695992), and every PR that merges main fails on it; verify is green here.
2026-11-08 04:02:16 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 2294609404 TNT's rider seat test expects the float height
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:59:15 +01:00
Ludovic Marie 7ddbb41f86 Merge pull request #12141 from RINNECODER/up2/compute-workgroups-limit
🤖 Agent: queued. A dispatch over 65,535 workgroups per dimension (the wgpu limit) is now split into several dispatches with the base passed along, so packs whose compute pass sizes from the screen still run at 4K and up.
2026-11-08 03:58:06 +01:00
Ludovic Marie 4b38e08b7f Merge pull request #11950 from senjinthedragon/item-entity-material-maps
🤖 Agent: queued. LabPBR `_n`/`_s` maps now reach entities, items, block entities and the hand; `set_shared_material_maps` now runs after `set_custom_sky` in loading_window.rs, so a pack reload binds both.
2026-11-08 03:58:03 +01:00
abit-foggy 5c00a74d4e Merge remote-tracking branch 'upstream/main' into codex/studio-c1-c2-topbar-timeline
# Conflicts:
#	crates/horizon-client/src/game/play_keys.rs
2026-11-07 16:50:47 -10:00
abit-foggyandCodex 858f248202 fix(studio): align C1+C2 replay controls
Co-Authored-By: Codex <[email protected]>
2026-11-07 16:50:04 -10:00
Nikita Maximilian GrimmandClaude Opus 5.5 290560bc52 Expect the float drags in the item flowing-water test
#11794 made the item's water and air drag floats (0.99F, 0.98F), as
vanilla's are; the test still expected the f64 constants, so it fails
wherever the vanilla data is prepared.

Fixes #12221

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:25 +01:00
Horizon agentandClaude Opus 5.5 092ae508ba rustfmt game.rs (verify failed on it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:45:11 +01:00
Horizon agentandClaude Opus 5.5 30c20a7a54 datapack test: give the new-world packs a pack section
folder_metadata_present (Pack.readPackMetadata) now requires a pack section
with a description, so the {} metadata in #12086's test hid both packs and
seed_create_world returned None. This failed every PR's verify on main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:43:49 +01:00
Ludovic Marie f96d9e89b5 Merge pull request #10283 from MSedgeMC/fix/cloud-save
🤖 Agent: removing an entity's chunk now drops its area-effect clouds too (breath_clouds.retain), and saves keep entity_base_tag's Entity fields. On main merge 8b583a15e, save_roundtrip, entity_nbt_boolean, orb_saved_uuid, stand_fall_save, conversion_water_save, mob_pickup_save and potion_effect_icon pass. command_exec's only 2 mismatches are main's gateway_item case (same on bare main b13dbf068).
2026-11-08 03:41:29 +01:00
Nikita Maximilian Grimm a45b450e56 Merge remote-tracking branch 'origin/main' into block-entities-wait-entities 2026-11-08 03:40:46 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 e6281842d8 Block entities and scheduled ticks wait for their chunk's entities to load
LevelChunk.isTicking and ServerLevel.isPositionTickingWithEntitiesLoaded
also need the chunk's entity section loaded (areEntitiesLoaded). It loads at
the end of a level tick, frozen or not (PersistentEntitySectionManager
.processPendingLoads), empty sections too. A chunk that loads now waits for
the end of the level's next pass before its block entities (bells, conduits,
gateways and sculk blocks included), moving pistons and scheduled ticks run.
The command harness's readiness waits mark their chunks loaded, as the
vanilla oracle's do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:35:18 +01:00
bulkandClaude Opus 5.5 842c094de7 Potent sulfur: find the mobs near a geyser from a grid
Every gassing geyser scanned every mob every 10 ticks, all on the same tick,
and every erupting one every tick. One grid of mob boxes is kept for the block
entity pass, until the mob list's next write; a geyser's own effects and
pushes move no mob, so the grid holds after them.

The mobs still get their effect and push in list order, and the gas reads
blocks only, so finding the reached mobs before any gets its effect changes
nothing.

(cherry picked from commit 2bad442a93, ported to the fields on Game)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:32:06 +00:00
bulkandClaude Opus 5.5 fc55599794 Credit the mob that swung for a guest's effect and sting hits
A cave spider's poison bite or a bee's sting on a guest named the mob
nearest to where the hit came from in X and Z, ignoring height, so a zombie
standing under the spider took the credit (the kill message, the hurt-by
advancement, the guest's revenge target). The event names the mob that
swung, as a plain melee hit does since the previous commit.

(cherry picked from commit 3ab80dc9d0, without its deletion of tools/bench_*.rs)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:32:06 +00:00
bulkandClaude Opus 5.5 12af27a781 Carts: read the carts and mobs near a cart from a grid
Each ticked cart compared its box with every cart, and a moving rideable cart
with every mob twice (and built the mob teams for each). A grid kept for the
tick answers in slot order, as the scans did. The cart grid follows each
ticked cart, is rebuilt when a cart is gone or added, and dropped after a
cart's command; the mob grid holds until the mob list's next write. With
native mods loaded, whose hooks can move either, the scans stay.

(cherry picked from commit 3613b40c6b, ported to the fields on Game)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:32:01 +00:00
Ludovic Marie bca5833f8d Merge pull request #12185 from Ragbie/fix/shulker-hurt-teleport-now
🤖 Agent: queued. A shulker's hurt teleport now runs inside the hurt, before the bullet's levitation, and riders get off where it stood (`unRide`); NoAI shulkers never teleport, as `teleportSomewhere` requires.
2026-11-08 03:31:45 +01:00
bulkandClaude Opus 5.5 ca0da4d9bb JoinList: count the writes, so a cache of the list can tell it is stale
A ListStamp names the list and how many times it was written through
`DerefMut` (every push, insert, retain or slot write). A cache built from
the list keeps the stamp and is rebuilt when the stamp differs; a swapped-in
list (another level's) has another name.

(part of commit 6b826d204a)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:31:42 +00:00
Ludovic Marie 4a8a101391 Merge pull request #12182 from TheMrZZ/fix/h283-built-in-component-delivery
🤖 Agent: queued. Four components (`provides_trim_material`, `provides_banner_patterns`, `can_place_on`, `can_break`) are now encoded in outgoing stacks with the synced registry IDs; anything unsupported drops the whole buffer, as before.
2026-11-08 03:31:41 +01:00
Ludovic Marie 07aab11e71 Merge pull request #12181 from TheApple05/codex/older-server-mapping-generation
🤖 Agent: queued. Every mapping value stays the same; the IDs the translators read come from generated pinned constants, and `--check` catches drift.
2026-11-08 03:31:37 +01:00
bulkandClaude Opus 5.5 fdd3d1e59c Stop walking every mob for each game event and melee hit
A game event posted outside the mob pass (a block placed, a door opened, an
explosion) looked through every mob of the level for the wardens and allays
that can hear it. The listener slots are now kept until the mob list is
written again (the previous commit's stamp), or another level's list is in
place.

Mob id lookups for game events, thorns and a hit's source go through the
mob index instead of a scan. A guest's melee event names the mob that
swung, so the hit credits that mob rather than the one nearest in x and z
to the player.

(cherry picked from commit 6b826d204a without its join_seq part, ported to the fields on Game)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:31:34 +00:00
Ludovic Marie bc21da03bc Merge pull request #12174 from NikitaMGrimm/saved-ticks-startup
🤖 Agent: queued. The server now waits for the saved tickets' chunks before the first tick (`prepareLevels`), so their saved ticks unpack at the time they were saved at; the wait gives up after 600 s.
2026-11-08 03:31:33 +01:00
Ludovic Marie 93fc4b2ba9 Merge pull request #12159 from itarqos5/tools/world-compare
🤖 Agent: queued. Tools only: `world_compare.py` and `perf_bench.py --seed`, no generator change; the seed report it answers is documented with the two seeds compared.
2026-11-08 03:31:30 +01:00
Ludovic Marie f3cc9257df Merge pull request #12080 from KBroneZ/claude/instances-folder
🤖 Agent: queued. `--folder` on the CurseForge `Instances` folder now finds its instances, and a Prism `instances` folder (same name where case is ignored) is not mistaken for it.
2026-11-08 03:31:26 +01:00
bulkandClaude Opus 5.5 8c1f6115d5 JoinList: count the writes, so a cache of the list can tell it is stale
A ListStamp names the list and how many times it was written through
`DerefMut` (every push, insert, retain or slot write). A cache built from
the list keeps the stamp and is rebuilt when the stamp differs; a swapped-in
list (another level's) has another name.

(part of commit 6b826d204a)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:31:25 +00:00
bulkandClaude Opus 5.5 069cc39f05 Carts: find the ticked cart at its slot and move it instead of cloning
push_from_ticked_cart searched the cart list for the ticked cart twice and
copied it in and out whole. The caller knows its slot: the ticked copy is
swapped into the slot for the push, and only the pushed cart is copied back,
over the copy it replaced (clone_from reuses its buffers). Lava hurting a cart
looks there first too.

Without mods a push changes velocities and riders, never the list, so the
cart is still in its slot after it (a debug assertion checks). With native
mods loaded a mount hook can change the list: the copies and the search by id
stay. The loot roll stays with the ticked copy, as #11526 made it: it comes
off before the swap and goes back after.

(cherry picked from commit 206905a2d5, without
the loot roll part of fb69aba537)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:31:22 +00:00
Ludovic Marie 0a407714e0 Merge pull request #12184 from sergue212fbright/fix/lod-slab-visibility
🤖 Agent: queued. Far terrain treats slabs as full cells like stairs, so a waterlogged slab keeps its own material instead of becoming water; the test walks every real slab state.
2026-11-08 03:31:22 +01:00
Ludovic Marie 3bb4e7827c Merge pull request #12164 from KBroneZ/claude/es-empty-shaderpacks
🤖 Agent: queued. An empty `shaderpacks/` (CurseForge and Sodium create it) no longer counts as a shader pack, for folders and archives alike; tests cover both.
2026-11-08 03:31:19 +01:00
Ludovic Marie 546eab04b7 Merge pull request #12168 from NikitaMGrimm/orb-piston-ground
🤖 Agent: queued. After every server push the ground flag is set to `verticalCollisionBelow` (`Entity.move` → `setOnGroundWithMovement`, which runs when `isLocalInstanceAuthoritative`), so a sideways push off a block clears it; #12167 builds on this.
2026-11-08 03:31:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 44caa43238 Primed TNT's box and eye height are vanilla's floats
EntityTypes.TNT is sized 0.98F with an explicit eyeHeight(0.15F). The box
now widens the float size, so TNT stopped by a wall or ceiling ends where
Java's does, and the fluid eye height is the type's 0.15F instead of the
default 0.85 of the height.

Fixes #12020

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:30:52 +01:00
Horizon agentandClaude Opus 5.5 a9fba955bd Leave the compression bench to the zlib reuse change
main still makes a new ZlibEncoder per record, so the bench would compare
two fresh encoders. It belongs with the change that adds the reuse (#12189).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:30:23 +01:00
Horizon agent e74f789659 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 03:28:10 +01:00
Horizon agent e02c201c97 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-08 03:28:04 +01:00
Horizon agent 325c627cdb Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-08 03:27:59 +01:00
Horizon agentandClaude Opus 5.5 139e83fff8 Opaque Ice: own flag; IceBlock keeps frosted ice for fluids
Fluids read Info::ice through is_ice_class (instanceof IceBlock, frosted
ice included), so excluding frosted ice there changed fluid meshing with
the option off. Regular ice now gets its own opaque_ice flag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:25:30 +01:00
Horizon agentandClaude Opus 5.5 1d6e5d01b5 Piston restitution: the entity's own velocity, server entities only, ground flag from #12168
Restitution bounces the entity's velocity (getDeltaMovement), not the push.
It runs for server entities only, since a player's client moves the player.
It runs even when the step is too small to apply, as Entity.move does.
The ground flag is left to set_pushed_on_ground (#12168), which follows
setOnGroundWithMovement.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:22:00 +01:00
Horizon agent 2adedf5570 Merge commit 'refs/cg72/pr-12168' into HEAD 2026-11-08 03:20:30 +01:00
Ludovic Marie 4e6920a319 Merge pull request #11829 from senjinthedragon/entity-motion-objects
🤖 Agent: queued. `at_velocity` now covers boats, minecarts, armor stands, frames, paintings, TNT, falling blocks, block entities and moving pistons; the `falling_block_visible` doc comment is back on its function.
2026-11-08 03:19:38 +01:00
Ludovic Marie c214f59711 Merge pull request #12076 from TheMrZZ/fix/h246-storage-alias
🤖 Agent: queued. A wildcard `data modify storage` that fails part way keeps the edits made so far, as vanilla's in-place NbtPath edit does; main merged in.
2026-11-08 03:19:34 +01:00
Nikita Maximilian Grimm 5e7e654901 Merge remote-tracking branch 'origin/main' into item-escape-gravity
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-08 03:18:38 +01:00
J5CaseyandClaude Opus 5.5 7e4e421224 Native mods: break a block as a player (native_player_break_block)
A helper-only fragment adds `native_player_break_block(uuid, pos,
into_inventory)` (`Host::player_break_block`), so a vein-mining mod can
break blocks as its player without injecting into the private
`Game::break_block`. It runs the game's own player break
(ServerPlayerGameMode.destroyBlock): the player's checks, the
`destroy_block` hook, loot for the held tool and its enchantments, tool
wear, stats, exhaustion and experience. With `into_inventory` the block's
drops go to the player's inventory, the rest dropped from them.

The break now reports whether it removed the block, and a break nested in
another's hook keeps the outer break's own-event position. The acting
guest's spawn protection is kept for their turn so the helper can check
it while the hub is out of reach.

Fixes #12195

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:18:20 -06:00
Horizon agentandClaude Opus 5.5 719fa5e2bd rustfmt game.rs (verify failed on it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:17:03 +01:00
Horizon agentandClaude Opus 5.5 485323aaaf add_effect: keep the accepted result past the absorption and health boost hooks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:16:51 +01:00
Ludovic Marie 4a374440f4 Merge pull request #11883 from TheMrZZ/fix/h205-mob-health-boost
🤖 Agent: queued. Health Boost raises the max health modifier and clamps health down when it ends, as `HealthBoostMobEffect` does; #11934 builds on it.
2026-11-08 03:15:58 +01:00
Ludovic Marie b99e2087d3 Merge pull request #11848 from TheMrZZ/fix/h174-bell-block-entity
🤖 Agent: queued. Command reads keep the real bell block entity (and copper golem statue) instead of a synthesized one, so `data get block` matches vanilla's NBT; main merged in.
2026-11-08 03:15:55 +01:00
Ludovic Marie b84c97088f Merge pull request #11766 from TheMrZZ/fix/mob-effect-amplifier-codec
🤖 Agent: queued. Effect amplifiers decode as unsigned bytes (`Byte.toUnsignedInt`), so an amplifier of 200 no longer reads as -56; main merged in.
2026-11-08 03:15:51 +01:00
Ludovic Marie 57da7aecbc Merge pull request #11729 from TheMrZZ/fix/h195-command-damage-range
🤖 Agent: queued. `damage` predicate ranges compare the item's damage as `ItemDamagePredicate` does (range on damage, not durability); main merged in.
2026-11-08 03:15:47 +01:00
Ludovic Marie e93afc5ade Merge pull request #11728 from TheMrZZ/fix/summoned-leash-knot-lifecycle
🤖 Agent: queued. A summoned leash knot snaps to the block centre and dies without a fence, as `LeashFenceKnotEntity.survives` checks; main merged in.
2026-11-08 03:15:44 +01:00
Ludovic Marie a7296b75e0 Merge pull request #11708 from TheMrZZ/fix/h032-jukebox-saved-clock
🤖 Agent: queued. A jukebox without a song now saves no `ticks_since_song_started`, as `JukeboxSongPlayer` writes it only while playing; main merged in.
2026-11-08 03:15:40 +01:00
J5CaseyandClaude Opus 5.5 9d25e30101 Mod menu types: a crafting grid that crafts as the crafting table
A content mod's menu type may declare `"crafting": {"grid", "size",
"result"}`: those slots run vanilla's CraftingMenu logic on the menu
engine (live result, consumption and remainders on take, shift-click
crafting, recipe placement), over a grid the block entity keeps. The
result is the menu's own: the block entity never holds it, so hoppers,
comparators and a broken block can't dupe it, and the hub shows guests
the result the game made, again each tick the grid changed.

Part of #12198.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:15:16 -06:00
UnQuis 4543e2f454 Add an Opaque Ice performance option
A Horizon-only option, off by default, that draws regular ice opaque in the solid pass and culls faces between adjacent ice blocks. It follows the public description of Luxium's Opaque Ice and the design note in #11563; no Luxium source was read or copied.

Fixes #12192
2026-11-08 10:14:00 +08:00
Ludovic Marie 36584650ea Merge pull request #12152 from RINNECODER/up2/glsl-atomic-struct-copy
🤖 Agent: queued. Patch in vendored naga: copies of structs or arrays holding atomics go member by member, up to 64 scalars; larger ones stay whole copies, as noted.
2026-11-08 03:12:06 +01:00
Ludovic Marie aee2c63c3b Merge pull request #12143 from RINNECODER/up2/pack-image-formats
🤖 Agent: queued. The added formats (RGB10_A2, RGB10_A2UI, 16-bit SNORM) map to the wgpu formats `image_format` already binds; formats a GPU can't store into are still left out with a note.
2026-11-08 03:12:01 +01:00
Ludovic Marie ef23545cef Merge pull request #12126 from hunixcode/fix/translatable-fallback-literal
🤖 Agent: queued. A translatable with a fallback keeps the fallback as a literal, as `TranslatableContents` does, instead of looking it up as another key.
2026-11-08 03:11:58 +01:00
Ludovic Marie 92c708b3fe Merge pull request #12125 from hunixcode/fix/panel-unspecified-host
🤖 Agent: queued. The panel accepts LAN browsers when bound to 0.0.0.0 or :: only; a loopback bind still refuses them, so nothing new faces the network.
2026-11-08 03:11:55 +01:00
Ludovic Marie c6f935414d Merge pull request #12124 from itarqos5/perf/terrain-interpolant-packing
🤖 Agent: queued. Passing terrain's interpolants in 5 locations instead of 12; visbuffer reads `TerrainOut` only internally, so packs see no difference.
2026-11-08 03:11:51 +01:00
Ludovic Marie 73231e735d Merge pull request #12114 from p-stzhuk/fix/legacy-intel-metal
🤖 Agent: queued. The wgpu-hal change is in `third_party/`, gated to legacy Intel Metal; other GPUs take the same path as before.
2026-11-08 03:11:47 +01:00
Ludovic Marie a71408736f Merge pull request #12093 from vnsmoreira/packs/core-sky
🤖 Agent: queued. Pack `core/sky` and `core/stars` shaders apply only when a resource pack ships them; without one the sky pipeline is unchanged.
2026-11-08 03:11:44 +01:00
TiggerTakkyTTTandCodex fb4805072f Fix ambient particles for datapack biomes
Co-Authored-By: Codex <[email protected]>
2026-11-08 09:11:04 +07:00
Nikita Maximilian Grimm 111c22dd5e Merge remote-tracking branch 'origin/main' into test-randoms-sync 2026-11-08 03:09:04 +01:00
SolenopsisbotandCodex ce08c38337 perf: add opt-in startup and compression benchmarks
Measure startup step distributions and RSS, and compare reused versus fresh zlib streams with deterministic checksums.

Co-Authored-By: Codex <[email protected]>
2026-11-08 13:08:58 +11:00
Ludovic Marie 8d2900e6ce Merge pull request #12113 from kacpherek/parity/batch-e
🤖 Agent: queued. One cost: the stepped tracker paths add a Vec per tracked entity on the tracker tick; small, but it is the hot path, worth a look if MSPT moves.
2026-11-08 03:05:56 +01:00
Ludovic Marie cd1318bf02 Merge pull request #12111 from kacpherek/parity/batch-c
🤖 Agent: queued. Batch: sculk now hears bottles, golems, endermen and frames; portals dismount per vehicle tick as vanilla's `Entity.handlePortal` does.
2026-11-08 03:05:53 +01:00
Ludovic Marie 2f661c08ed Merge pull request #12109 from kacpherek/parity/batch-a
🤖 Agent: queued. Batch of server click answers; each case matches `Mob.interact`/`InteractionResult` and is in the batch's harnesses, which run in parity.
2026-11-08 03:05:49 +01:00
Ludovic Marie 1bf8766c1d Merge pull request #12103 from martipops/controller/direct-craft
🤖 Agent: queued. LT + A crafting a recipe repeatedly is new controller input; servers see the same clicks a mouse would send, so nothing server-side changes.
2026-11-08 03:05:46 +01:00
martipops 9f92bb23b4 Merge remote-tracking branch 'upstream/main' into settings/shell-graphics-features 2026-11-07 21:03:09 -05:00
Ludovic Marie 3e06612e08 Merge pull request #11826 from iseaprojects/i18n/es-es-remaining
🤖 Agent: queued. es.json was merged key by key with main (91 translations kept, main's new keys too), so no Spanish string main added is lost.
2026-11-08 03:02:29 +01:00
Ludovic Marie 15c0246614 Merge pull request #12147 from RINNECODER/up2/port-occludes
🤖 Agent: queued after #12145 (its base, queued). Main merged in and the stack rebuilt on #12145's head; verify is green at this head.
2026-11-08 03:02:25 +01:00
Ludovic Marie face534db3 Merge pull request #12132 from Evan-lhm/fix/launcher-blur-zero-sharp
🤖 Agent: queued. A 0 px blur skips the blur pass, so the plates are sharp and the GPU does no work for it.
2026-11-08 03:02:22 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 64f4bab6f3 Parity harnesses: unseeded randoms and entity ids can be synced between vanilla and Horizon
A test-only Java agent makes vanilla's RandomSupport.generateUniqueSeed step a
uniquifier of the calling thread's own, without the clock; HORIZON_TEST_SEEDS=1
does the same in Horizon's unique_seed. A command script's sync_randoms step sets
that uniquifier, the entity-id counter and the level's random in both engines,
and the "seed sync proof" group compares a drop's position, primed TNT's kick, a
dispensed item and the resting phase of four items exactly. The oracle cache
counts the agent jar as an input.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:01:17 +01:00
Horizon agentandClaude Opus 5.5 52d1a0b475 lod/packed: from_bits and from_words are test-only
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:00:41 +01:00
Horizon agentandClaude Opus 5.5 c0fe9adca6 End flash sound: random_source::create (SoundEngine::unseeded left main)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:00:37 +01:00
Horizon agent 992ea05825 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 03:00:37 +01:00
Horizon agentandClaude Opus 5.5 2e11de8943 Compare spill test names as Tags (block_names holds the CustomName tag now)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:00:32 +01:00
Horizon agent 7351ad6d91 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/client_lib.rs
2026-11-08 03:00:31 +01:00
Ludovic Marie 0a6eca805d Merge pull request #12154 from RINNECODER/up2/pack-comment-settings
🤖 Agent: queued. Comment settings end at their line, as the Iris probes in `read_stage` show it reads them line by line; live code still reads to the `;`.
2026-11-08 02:59:34 +01:00
Horizon agent e949fd4cc1 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 02:58:50 +01:00
Ludovic Marie ed0be44325 Merge pull request #12117 from sergue212fbright/fix/sign-text-formatting
🤖 Agent: queued. Bold is drawn as the shifted second pass and italic as the skew vanilla's `BakedGlyph` uses, and after the #11863 merge styled glyphs keep the HDR emission tier; the 102 sign cases pass.
2026-11-08 02:58:24 +01:00
Ludovic Marie 97e46ce1d5 Merge pull request #12050 from TheMrZZ/fix/h140-nautilus-cooldown-memory
🤖 Agent: queued. The Nautilus cooldown memory now exists only while it counts down and is erased at 0, as `Brain.setMemoryWithExpiry` does; its harness runs in parity.
2026-11-08 02:58:20 +01:00
Ludovic Marie 5669d66de3 Merge pull request #11990 from TheMrZZ/fix/h285-adventure-predicate-saved-forms
🤖 Agent: queued. A one-block predicate now saves as the bare id and partial `custom_data` keeps only the keys given, which is how `BlockPredicate`'s codec writes them; the residual corpus documents what is left.
2026-11-08 02:58:17 +01:00
Ludovic Marie f54fba8726 Merge pull request #12123 from NikitaMGrimm/tnt-piston-ground
🤖 Agent: queued. A sideways push leaves `onGround` false, as `PistonMovingBlockEntity.moveCollidedEntities` only sets it for upward pushes; main merged in since my review, no other change.
2026-11-08 02:58:13 +01:00
Ludovic Marie f86b98b203 Merge pull request #12118 from NikitaMGrimm/minecart-drop-motion
🤖 Agent: queued. A destroyed minecart drops its item and contents with `Containers.dropContents`' motions; same items, now in vanilla's spread.
2026-11-08 02:58:10 +01:00
Ludovic Marie 9842e75046 Merge pull request #12107 from martipops/settings/shell-fixes
🤖 Agent: queued. Building the settings rows once removes the per-frame rebuild, and Down from the search field lands in the list.
2026-11-08 02:58:07 +01:00
Ludovic Marie 99956fff22 Merge pull request #12106 from martipops/controller/nintendo-buttons
🤖 Agent: queued. The Nintendo row picks by position or by letter only in the glyph table; the bindings themselves don't move.
2026-11-08 02:58:03 +01:00
Ludovic Marie b23ad347d1 Merge pull request #12105 from NikitaMGrimm/execute-block-nbt
🤖 Agent: queued. `execute if block`, `fill` and `clone` now compare the block entity's NBT part of the predicate, as `BlockPredicateArgument` does.
2026-11-08 02:58:00 +01:00
Ludovic Marie 350e156a34 Merge pull request #12088 from KBroneZ/claude/datapack-jars-launcher
🤖 Agent: queued. Last of the series, after #12087: a profile with only data packs now plays instead of reporting nothing to install.
2026-11-08 02:57:55 +01:00
Ludovic Marie 502c15aba6 Merge pull request #12086 from KBroneZ/claude/new-world-datapacks
🤖 Agent: queued. First of the 12086 → 12087 → 12088 series (each includes the one before): new worlds get the game folder's `new-world-datapacks`, copied at creation only.
2026-11-08 02:55:14 +01:00
Ludovic Marie 147980da63 Merge pull request #12082 from KBroneZ/claude/found-on-this-computer
🤖 Agent: queued. Only reads other launchers' folders (no writes), and the 86 lang files carry the new strings.
2026-11-08 02:55:09 +01:00
Ludovic Marie 97e99a45b4 Merge pull request #12073 from KyttuXD/fix/metal-cutout-leaves-derivatives
🤖 Agent: queued. Metal now always draws vanilla terrain flat, whatever the section count, which is the path that was already used under the threshold.
2026-11-08 02:55:05 +01:00
Ludovic Marie 37cda82c81 Merge pull request #12060 from ZiQu-dev/fix/oit-cull-depth-metal
🤖 Agent: queued. The 4-ulp depth tolerance fixes the speckles on Metal where the OIT pass's depths differ by a few ulps from the opaque pass's.
2026-11-08 02:55:01 +01:00
Ludovic Marie c76ed64570 Merge pull request #12058 from Mik00000/perf/dimension-tick
🤖 Agent: queued. Ending the tick on the first level skips the dimension swap that did nothing, and the ray chunk cache is per raycast, so no stale chunk outlives one ray.
2026-11-08 02:54:58 +01:00
Ludovic Marie f63ad014e1 Merge pull request #12048 from ilguerro/perf/9996-load-palres
🤖 Agent: queued. One cost to know: the registry-wide cache has 8192 slots that are never reclaimed; past that it falls back to decoding, so it only stops helping, it doesn't grow.
2026-11-08 02:54:54 +01:00
Ludovic Marie 7b33d702f5 Merge pull request #12047 from ilguerro/perf/9996-load-packed
🤖 Agent: queued. The SWAR check tests 8 palette indices per word and has an exhaustive test against the scalar loop, so a bad index is still caught at the same position.
2026-11-08 02:54:50 +01:00
Ludovic Marie dfae932fec Merge pull request #12016 from MSedgeMC/fix/drowned-trident-continue
🤖 Agent: queued. The trident attack continues while `alive && (holding a trident || !nav.done)`, which is vanilla's condition rewritten; a drowned that drops its trident now switches to melee.
2026-11-08 02:54:47 +01:00
Ludovic Marie bf3a5f162f Merge pull request #12008 from MSedgeMC/fix/cat-tempt-tamed
🤖 Agent: queued. A tamed cat no longer starts the tempt goal, as `Cat.TemptGoal`'s `canUse` returns false once tamed.
2026-11-08 02:54:43 +01:00
Ludovic Marie 14fc6d55e0 Merge pull request #11985 from TheMrZZ/ci/overlap-note-faster
🤖 Agent: queued. Tools only: one `git cat-file --batch-check` for the heads instead of a process per PR, and the diffs fetched in parallel; same notes out.
2026-11-08 02:54:39 +01:00
Ludovic Marie 4e6a8131c2 Merge pull request #11980 from MSedgeMC/fix/trade-start-conditions
🤖 Agent: queued. `TradeWithPlayerGoal.canUse` also stops on hurt and on distance > 16; it reads `cx.w.player` like the existing LookAtTradingPlayer goal does, so guests on a server share that approximation.
2026-11-08 02:54:36 +01:00
Ludovic Marie 01451da341 Merge pull request #11977 from MSedgeMC/fix/ghast-still-float
🤖 Agent: queued. The happy ghast's still timeout now ends the float goal as in vanilla, so a parked ghast stays put instead of drifting.
2026-11-08 02:54:32 +01:00
Ludovic Marie 179ebb840a Merge pull request #11965 from MSedgeMC/fix/cube-jump-passenger
🤖 Agent: queued. A ridden cube mob now skips its jump-move as vanilla's `SlimeMoveControl` does while it is a passenger, so a slime on a boat no longer hops off its seat.
2026-11-08 02:54:28 +01:00
Ludovic Marie fca63b5eb8 Merge pull request #11868 from MrCloudy2/fix/rider-teleport-look
🤖 Agent: queued. Matches `ClientPacketListener.handleMovePlayer`: a passenger keeps its seat when the server's position packet comes, instead of snapping out of the vehicle for a tick.
2026-11-08 02:54:25 +01:00
Horizon agentandClaude Opus 5.5 c1cdeb19d4 rustfmt client_lib.rs (verify failed on it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:50:17 +01:00
J1zeee b03127ab97 Merge fork/fix/12022-piston-push-velocity-restitution 2026-11-08 04:44:47 +03:00
Horizon agentandClaude Opus 5.5 1f9493c4fc Keep falling_block_visible's doc comment on it
The motion helpers went in between the comment and the function it describes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:44:31 +01:00
Nemotron 9524516752 Fix piston push not zeroing entity velocity in lazy chunks (#12022)
move_pushed applied the resolved movement to position only, leaving the
entity's velocity untouched. In a ticking chunk the entity's own tick
masks this (PrimedTnt::tick_among restitutes), but in a lazy chunk the
velocity survives: a primed TNT pushed by a piston into a wall, ceiling
or slime keeps 0.4 / -0.8 / -0.8 instead of vanilla's 0 / 0 / 0.64.

Add restitute_piston after apply_push: compare the post-piston-limit
movement (limited) with the post-collision result (resolved) and, as
Entity.move(MoverType.PISTON, …) does, zero blocked horizontal axes,
zero or slime-bounce the vertical axis, and update on_ground. Covers
Player, Guest and every Entity kind including PrimedTnt.

Add unit test a_pushed_tnt_loses_velocity_into_a_wall (needs prepared
resources) asserting the TNT's vx is zeroed by a wall.

Co-Authored-By: Nemotron <[email protected]>
2026-11-08 04:36:37 +03:00
Horizon agentandClaude Opus 5.5 a465c3efae rustfmt (verify failed on it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:30:39 +01:00
Horizon agent 904b3036ab Merge remote-tracking branch 'origin/main' into m10280 2026-11-08 02:29:41 +01:00
Horizon agentandClaude Opus 5.5 85d56215e6 Merge main into #11826
es.json: main's file with this PR's 91 translations applied key by key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:28:45 +01:00
Horizon agentandClaude Opus 5.5 ebf06768f1 Merge main into #11950
Main moved build_renderer to game/loading_window.rs; the shared material maps
are set there now, after the custom sky as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:28:17 +01:00
Horizon agentandClaude Opus 5.5 fd03679d85 Merge main into #11829
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:27:35 +01:00
Horizon agentandClaude Opus 5.5 27ed9c66f7 Merge main into #11821
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:27:34 +01:00
Horizon agent 738861aae2 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 02:26:57 +01:00
Horizon agentandClaude Opus 5.5 c7ff85fdf4 Realms: keep the single-player owner UUID; skip drive and stream parts in downloads
- Merge main.
- The owner switch from the offline UUID to the account UUID (integrated.rs,
  lan/conn.rs, lan/playerdata.rs, net.rs, remote.rs) is left out: every world
  Horizon made names its player by the offline UUID, so tamed pets and other
  owner references would no longer know their player. It needs its own PR
  with a migration.
- A tar entry part with a colon (a drive prefix or an alternate data stream)
  would leave the world folder on Windows: skip it, with a test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:26:29 +01:00
Horizon agent 175c2d453a Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 02:25:49 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 673d5cff49 Read the escape case exactly now that the float drags have landed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:24:42 +01:00
Evrim CicekandClaude Opus 5.5 ca896819d7 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 04:24:28 +03:00
GlockSmithandOpenAI Codex GPT-6 0a8df72a16 Keep slab materials in distant terrain
Retain top and bottom slab states in LOD chunk summaries instead of
discarding dry slabs or replacing waterlogged slabs with water. Use the
same full-cell approximation as the existing partial-block filter.

Check real summaries, coarse reduction and emitted faces for every
prepared slab state. All 72 LOD tests pass, including resource-backed
coverage of 606 slab states across 101 materials.

Refs horizon-rust/horizon#12180.

Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-11-07 20:23:37 -05:00
Ludovic Marie b13dbf0687 Merge pull request #11837 from yahya-nusseibeh/fix/control-socket-path
🤖 Agent: queued. When `/tmp` is shared the socket goes into a private `horizon-<uid>` folder, so another user on the machine can't connect to or squat on the control socket.
2026-11-08 02:20:33 +01:00
Ludovic Marie 0cdb230b54 Merge pull request #11815 from ilguerro/fix/thorns-rest
🤖 Agent: queued at a3fd3597f (main merged in; the draw now uses `random_source::entity()`). Thorns rolls in EquipmentSlot order from the wearer's own random, as `EnchantmentHelper.doPostAttackEffects` does.
2026-11-08 02:20:29 +01:00
Ludovic Marie 6e919e4356 Merge pull request #11806 from MrCloudy2/fix/shield-disable-cooldown
🤖 Agent: queued. The defender now gets the Cooldown packet the server sets on the shield (`Player.disableShield`), so its client greys the shield out for the 100 ticks instead of letting it block client-side.
2026-11-08 02:20:26 +01:00
Ludovic Marie 92cf9ac888 Merge pull request #11799 from Detimes/fix-old-attribute-names
🤖 Agent: queued at 14bfb672f (main and #11750 merged in). UPDATE/REMOVE_MOB_EFFECT ids are renumbered for older clients; effects they don't know are dropped instead of showing as another effect.
2026-11-08 02:20:22 +01:00
Ludovic Marie 0961939b71 Merge pull request #11791 from MrCloudy2/fix/spear-stab-players
🤖 Agent: queued. One cost to keep in mind: `stab_players_for` builds a Vec for every guest using any item each tick (shields, food too); small, but a follow-up could return early when the item is not a spear.
2026-11-08 02:19:16 +01:00
Ludovic Marie 2cd05e462e Merge pull request #11789 from MSedgeMC/fix/camel-sit-room
🤖 Agent: queued. Same check as `Camel.canCamelChangePose` (the standing box against the blocks above), and the test uses a block at y+2 that blocks only the 2.375-tall standing pose.
2026-11-08 02:19:11 +01:00
Ludovic Marie 541f331f1e Merge pull request #11762 from Mayonesu0/perf/launcher-write-data-file
🤖 Agent: queued. The data file is written off the UI thread after `complete_assets`, and a stale one is dropped when the assets change, so a repaired install doesn't read an old index.
2026-11-08 02:19:07 +01:00
Ludovic Marie 973287971d Merge pull request #11759 from doodlyyousuf/251107-fix-guest-left-mob-xp
🤖 Agent: queued. Matches `LivingEntity.dropExperience`/`dropCustomDeathLoot`: experience and equipment follow `lastHurtByPlayerMemoryTime > 0`, while LAST_DAMAGE_PLAYER still needs the player to be resolvable, and the new test checks both sides.
2026-11-08 02:19:04 +01:00
Ludovic Marie 7648d874b9 Merge pull request #11750 from Detimes/fix-11722-old-pick-item
🤖 Agent: queued at fee1085be (main merged in). A 1.21.3 pick_item now becomes the same container swap a 1.21.4+ client sends, so older clients keep middle-click in survival.
2026-11-08 02:19:00 +01:00
Ludovic Marie b91f5f5892 Merge pull request #11741 from yahya-nusseibeh/fix/server-undrained-packets
🤖 Agent: queued. The count goes up at the one `NetEvent::Packet` send in `conn.rs` and down at the hub's drain, so the 2× inbox cap bounds what a flooding client can queue between ticks without touching the hot path otherwise.
2026-11-08 02:18:57 +01:00
Ludovic Marie f159a106a8 Merge pull request #11713 from Akirabane/feat/transfer-stop-wait
🤖 Agent: queued. The stop waits only with `[transfer]` on, and its 90 s bound stays under the supervisor's 120 s kill, so a proxy that never answers can't hang a stop. Built with 14 other PRs on main (pr_ready --quick green apart from known NixOS-only tests).
2026-11-08 02:18:54 +01:00
Ludovic Marie c805ee70c5 Merge pull request #11782 from Teiwazik/render/native-section-fade
🤖 Agent: I moved the distance blend per pixel (it was per triangle through the flat hdr_code). Before/after screenshots and frame time are in my comment: the pack's GPU frame is 121.1 vs 122.0 ms on lavapipe, same within noise.
2026-11-08 02:18:50 +01:00
TheMrZZandCodex 074aa1124f Encode four built-in item components for outgoing inventory stacks
Preserve typed holder and adventure-predicate payloads at the inventory bridge.

Co-Authored-By: Codex <[email protected]>
2026-11-08 02:15:11 +01:00
Horizon agent 14bfb672f8 Merge #11750 into #11799 (both add the last tests of client_session.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-protocol/src/adapter/client_session.rs
2026-11-08 02:14:29 +01:00
Horizon agent bf321a8425 Merge #11789 into #11792 (both add the last tests of camel.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/camel.rs
2026-11-08 02:14:01 +01:00
Horizon agent 2fdc1509ae Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 02:13:52 +01:00
TheApple05andCodex GPT-6 703c0ea18b Generate pinned adapter IDs and record regeneration recipes
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-08 01:13:25 +00:00
Nikita Maximilian Grimm 5bea37b4be Merge remote-tracking branch 'origin/main' into portal-ticket-persistence
# Conflicts:
#	crates/horizon-client/src/game/levels.rs
2026-11-08 02:10:48 +01:00
abit-foggyandCodex GPT-6 d15f455027 fix(studio): show replay UI and release cursor
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 15:08:30 -10:00
Nikita Maximilian Grimm 23f0fd6675 Merge remote-tracking branch 'origin/main' into item-escape-gravity 2026-11-08 02:06:55 +01:00
Horizon agent 8b583a15e1 Merge remote-tracking branch 'origin/main' into m10283
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-08 02:06:02 +01:00
Florian ErnstandClaude Opus 5.5 48782a8231 Refuse a malformed inline /place feature while the command is parsed
ResourceOrIdArgument decodes an inline value with the registry's codec
while the command is parsed, so a feature the ConfiguredFeature codec
refuses fails the parse: the command never runs, and an execute store
around it keeps its value. Horizon's parse accepted any compound for
minecraft:worldgen/feature and ran the codec only when the command
executed, which then failed and stored 0.

Where the server has its live features, the command registries now carry
a check that runs the same inline decode placement uses (the live
features' catalog and block bindings, through LiveFeatures::decode_inline),
without placing anything. A value it refuses fails the parse with
argument.resource_or_id.failed_to_parse. Without live features (a client,
or a server without the worldgen bundle) the parse is unchanged. The loot
table and every other registry keep their decode.

The message is the native codec's own (for example "invalid world
generation field: to_place"), not DataFixerUpper's text.

Fixes #12171.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:02:45 +01:00
Horizon agentandClaude Opus 5.5 43832ec794 Merge #12145 into #12147 (both change the map_color line)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:58:49 +01:00
Horizon agentandClaude Opus 5.5 d2d9dd7ae1 Merge main into #12112
The End flash moved to tick_end_flash here, while main (0b23212f4) switched
the flash and the dragon fight to the dimension type: keep main's
has_ender_dragon_fight check, and gate tick_end_flash on the End skybox as
main did.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:58:14 +01:00
Evrim CicekandClaude Opus 5.5 c0a3af67b0 A teleporting shulker throws off whoever rides it where it stood (Entity.unRide)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:57:52 +03:00
Horizon agentandClaude Opus 5.5 acf0cabce9 Merge main into #12110
main (3443abbde) kept NoAI Nautilus out of the needs_water branch; this PR
gives every Nautilus its own handler, which already refills NoAI ones.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:57:46 +01:00
Ludovic Marie bc6016d19d Merge pull request #12157 from horizon-rust/agent/older-servers-8718
🤖 Agent: Follow-up to #8718. On main, lan_bot's older_clients_of_each_release_join_and_are_sent_chunks fails at 1.21.1 (the reverse step never adds 1.21.2's particle status to ClientInformation); CI misses it because the test returns early without prepared resources. With this it passes, as do all 440 horizon-protocol tests and, on the union, the particle harnesses block/emitter/entity/marker/native_client_particles, particle_providers, player_effect_amplifier and spell_ticks (8/8).
2026-11-08 01:55:30 +01:00
Horizon agent 72e6ef91db Merge #12108 into #11934 (same effect and attribute code)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/world_cmds.rs
#	tools/test_command_exec_parity.py
2026-11-08 01:53:30 +01:00
Horizon agent ab069b576c Merge #11959 into #11934 (same effect and attribute code)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs.rs
2026-11-08 01:53:27 +01:00
Horizon agent f521237d6c Merge #11883 into #11934 (same effect and attribute code)
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/effects.rs
#	crates/horizon-client/src/game/potions.rs
#	crates/horizon-client/src/mobs.rs
#	crates/horizon-client/src/mobs/ai.rs
#	tools/test_command_exec_parity.py
2026-11-08 01:53:24 +01:00
Nikita Maximilian Grimm 18bbc46240 Merge remote-tracking branch 'origin/main' into item-escape-gravity 2026-11-08 01:53:09 +01:00
Horizon agentandClaude Opus 5.5 4f56dfc42b Merge #11813 into #11831 (both change take_mob_passengers)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:52:32 +01:00
Florian ErnstandClaude Opus 5.5 a75a57bd18 Load the world border of the level in place when the world opens
Each level reads its world_border SavedData when it is made
(ServerLevel.getWorldBorder), so a restarted server has the saved border
in place before its first command. #11016 gave each level its own
border. But world open reset the border of the level in place to the
default, and the saved file was read only later, in load_scoreboard (the
first tick's functions, or the first scoreboard use). A first
/worldborder get after a restart read the default, and a first
/worldborder set was put back to the saved size soon after.

World open now reads the in-place level's border with load_border, as
open_level does for the other levels, and load_scoreboard no longer
touches the border.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:51:14 +01:00
Horizon agentandClaude Opus 5.5 f1af021d5c Merge main into #11732
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:49:11 +01:00
Nikita Maximilian Grimm aed804a26d Merge remote-tracking branch 'origin/main' into saved-ticks-startup 2026-11-08 01:49:01 +01:00
Horizon agentandClaude Opus 5.5 fcd33abc9e Merge main into #12133
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:48:46 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8ce0848a31 Harness: stop a server that never finishes starting
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:48:34 +01:00
Nikita Maximilian Grimm 48c080e387 Merge remote-tracking branch 'origin/main' into t3/scheduled-item-tick-pass 2026-11-08 01:48:27 +01:00
Horizon agentandClaude Opus 5.5 8a218ed405 Merge main into #12127
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:47:55 +01:00
Horizon agentandClaude Opus 5.5 02e546660a Merge main into #12115
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:46:54 +01:00
Horizon agentandClaude Opus 5.5 46e77b05a8 Merge main into #12108
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:46:51 +01:00
Horizon agentandClaude Opus 5.5 9b6074a363 Merge main into #12094
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:46:44 +01:00
Horizon agentandClaude Opus 5.5 3c73be2517 Merge main into #12076
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:46:42 +01:00
Evrim CicekandClaude Opus 5.5 917ce67f0d Test: a shulker bullet's hit teleports an open shulker before it returns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:37 +03:00
Evrim CicekandClaude Opus 5.5 b11fe1da84 A shulker's hurt teleports it inside the hurt, as vanilla's does
Shulker.hurtServer calls teleportSomewhere() and hitByShulkerBullet() inside
the hurt. Mob::damage has no world, so the flags it leaves now run right
after the hurt: in the mob's own tick (hurt_teleports, also without AI) and
in the game layer (settle_hurt_teleports: /damage, explosions, projectile
hits, the creature outbox), before a bullet's levitation. teleportSomewhere
also refuses a NoAI shulker, keeps inside the world border and unRides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:36 +03:00
Evrim CicekandClaude Opus 5.5 fdb23b8ac6 Harness: a shulker's low-health teleport inside the hurt (/damage while frozen, poison past tick 281, NoAI)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 03:46:36 +03:00
Horizon agentandClaude Opus 5.5 8fd45d810e Merge main into #12041
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:46:31 +01:00
Horizon agentandClaude Opus 5.5 2183972c00 Merge main into #12031
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:45:32 +01:00
Horizon agentandClaude Opus 5.5 ac85e523c7 Merge main into #12012
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:51 +01:00
Horizon agentandClaude Opus 5.5 03df68c41d Merge main into #11991
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:45 +01:00
Horizon agentandClaude Opus 5.5 a77d401f53 Merge main into #12010
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:45 +01:00
Horizon agentandClaude Opus 5.5 1769fff834 Merge main into #11964
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:42 +01:00
Horizon agentandClaude Opus 5.5 386ab14035 Merge main into #11959
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:40 +01:00
Horizon agentandClaude Opus 5.5 ca82b37eb0 Merge main into #11934
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:44:15 +01:00
Horizon agentandClaude Opus 5.5 a505caa32a Merge main into #11883
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:50 +01:00
Horizon agentandClaude Opus 5.5 9aa5c9fe4a Merge main into #11879
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:47 +01:00
Horizon agentandClaude Opus 5.5 c7a2daa593 Merge main into #11848
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:43 +01:00
Horizon agentandClaude Opus 5.5 e5d8856821 Merge main into #11831
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:42 +01:00
Horizon agentandClaude Opus 5.5 3584268d11 Merge main into #11830
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:26 +01:00
Horizon agentandClaude Opus 5.5 93bb7f3230 Merge main into #11813
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:24 +01:00
Horizon agentandClaude Opus 5.5 bf1070413f Merge main into #11811
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:18 +01:00
Horizon agentandClaude Opus 5.5 816d917134 Merge main into #11797
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:08 +01:00
Horizon agentandClaude Opus 5.5 2573d1c76a Merge main into #11783
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:43:07 +01:00
Horizon agentandClaude Opus 5.5 3a967d8838 Merge main into #11766
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:42:56 +01:00
abit-foggyandCodex GPT-6 000e13fbe6 test(studio): isolate mouse drag hook fixture
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 14:42:49 -10:00
Ludovic Marie 40e3f368da Merge pull request #11720 from Teiwazik/seat/follow-plugin-seat-chains
🤖 Agent: I pushed 9bc110bae. The new second position_riders() call after the player moves rebuilt the whole mob index on every tick for every client; it now runs only when the riding graph has something on this player. I also added the missing passenger_vehicles field to main's studio Remote initializer (it did not compile on main) and drop passengers whose vehicle is removed. Both ignored tests pass with prepared resources, and so do the riding harnesses on a union with current main: boat_authority, boat_rider, boat_paddle_reset, mob_rider_navigation, mount_submerged, server_entities, vehicle_command_damage, dedicated_server (8/8).
2026-11-08 01:42:48 +01:00
Horizon agentandClaude Opus 5.5 bd967852e5 Merge main into #11729
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:39:31 +01:00
Horizon agentandClaude Opus 5.5 ecdecc8fd2 Merge main into #11728
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:38:55 +01:00
Horizon agentandClaude Opus 5.5 c4c084be94 Merge main into #11708
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:38:51 +01:00
abit-foggyandCodex GPT-6 6c5d89ffac fix(studio): own marker archive entry names
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 14:34:05 -10:00
Nikita Maximilian Grimm 6e72e2de7d Merge branch 'tnt-piston-ground' into orb-piston-ground 2026-11-08 01:29:33 +01:00
Nikita Maximilian Grimm 8b0028bbeb Merge remote-tracking branch 'origin/main' into t3/scheduled-item-tick-pass 2026-11-08 01:28:12 +01:00
abit-foggyandCodex GPT-6 e69f6a5050 feat(studio): add replay top bar and timeline
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 14:25:03 -10:00
itarqos5andClaude Sonnet 5.5 7c34cfa6b2 tools: world_compare.py compares a vanilla and a Horizon world of one seed
Reads the seed, spawn, biomes, structure starts and blocks two saved 26.x worlds
share, with a run-to-run floor from a second world of one game. Run on seed 12345
and a large negative seed: the spawn, every biome and every structure start match,
terrain blocks match to 99.3 to 99.9%, and decoration sits at vanilla's own floor
except trees, a few points under it. perf_bench.py gets --seed to make the worlds.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-08 00:21:56 +00:00
Ludovic Marie b399f0ddc5 Merge pull request #11775 from Teiwazik/older-servers/sign-update-order
🤖 Agent: Checked against the 26.3 client jar: ServerboundSignUpdatePacket's codec is pos, fixedSizeList(4) of stringUtf8(384), then SignTextSlot as an idMapper VarInt (BACK=0, FRONT=1), so reading the side last and writing it as the old boolean after the position is right. It only runs in the older-server adapter, so 26.3 servers are untouched. I merged main (the #8718 conflict in translate.rs: kept #8718's arms plus this one); 440 horizon-protocol tests and clippy pass.
2026-11-08 01:18:50 +01:00
Horizon agentandClaude Opus 5.5 a3fd3597f4 Thorns: draw from random_source::entity(), which replaced dispenser::entity_random on main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:16:10 +01:00
Horizon agent 1d9f455dac Merge remote-tracking branch 'origin/main' into HEAD 2026-11-08 01:15:53 +01:00
Horizon agent f97dffe988 Merge remote-tracking branch 'origin/main' into pr-11775
# Conflicts:
#	crates/horizon-protocol/src/adapter/translate.rs
2026-11-08 00:15:30 +00:00
KBroneZandClaude Opus 5.5 c6f52257e2 fix: count 1.20 recipe results in the reason only when there are some
The partial reason of a carried 1.20 mod leaves out the recipe clause
when no result names `item`, and says "1 recipe result names" for one.
docs/modpacks.md says how the game treats each 1.20-shaped file, as an
offscreen run of a carried synthetic 1.20 jar showed: one file at a
time, never the whole start.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 02:12:22 +02:00
Florian ErnstandClaude Opus 5.5 07d0cfc344 Format the mannequin main-hand writer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:11:37 +01:00
Horizon agentandClaude Opus 5.5 949eef6772 Russian launcher strings: fix the JSON and a placeholder
A missing comma after the horizon-server line made ru.json unreadable, so
the whole launcher fell back to English (the i18n tests failed on it).
The {mods} placeholder had been translated, so the count would never be
filled in. Also "подключиться" for two "Couldn't connect" lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:08:05 +01:00
KBroneZandClaude Sonnet 5.5 ecce33154e Launcher: installing a data pack counts what the game reads today
datapacks::READ named four kinds, so a pack of only recipes, loot tables, advancements or
worldgen was refused as changing nothing, and the install note listed them as not read. READ
now lists the kinds the game loads (worldgen by folder, so world presets still count as not
read), the refusal goes through trf(), and docs/datapack-upgrade.md no longer says worldgen is
not applied.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-08 02:07:21 +02:00
itarqos5andClaude Sonnet 5.5 d6f88d2483 feat(render): GPU Culling gets an Auto mode that tries it where the CPU limits the frame
GPU Culling pays where the CPU is the slower of CPU and GPU and costs where the
GPU is, and a machine's parts don't say which. Auto watches the frames' median CPU
and GPU times, tries it where the CPU is clearly the slower one, and keeps it only
if the frame's busier side dropped under 97%; a trial that didn't help is undone and
waits 5 minutes, then 20, then for good.

gpuCulling in horizon-graphics.txt is now false|true|auto (older files read as before);
Settings > Performance steps Off/On/Auto; horizon-ctl gpu-culling takes auto. Off stays
the default and no preset sets it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-08 00:04:36 +00:00
TheMrZZandCodex gpt-6.1-sol 0cdc4a49da Preserve styled container names in the live block model
Carry admitted Component tags through name state, saves and drops; let modeled name removal override stale source names.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-08 01:02:29 +01:00
Horizon agentandClaude Opus 5.5 fee1085bec Merge main into fix-11722-old-pick-item
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:02:24 +01:00
Horizon agentandClaude Opus 5.5 272518c1f2 Merge main into fix-old-attribute-names
Main already carries the 1.21.1 and 1.20.6 attribute tables (consolidated in
d79a36101); kept main's generated tables and this branch's mob effect renumbering.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:02:00 +01:00
abit-foggyandCodex GPT-6 68459f0442 feat(studio): persist timeline markers
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 14:02:00 -10:00
Nikita Maximilian Grimm 2877c7fbfa Merge remote-tracking branch 'origin/main' into saved-ticks-startup 2026-11-08 01:01:52 +01:00
KBroneZandClaude Sonnet 5.5 5be2aba2ae style(port-pack): rustfmt the shaderpacks test
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-08 02:01:09 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 6b371cdc48 HDR float frame: the frame follows HDR and the pack picked at once
Under HORIZON_HDR_FRAME=float, when HDR, Smooth Gradients or the pack picked call for another
frame than the renderer's, the renderer is made again on the same device before the next frame
draws, and the world is meshed again, as after a lost device. Without the switch nothing changes:
the frame stays until the next start.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:00:38 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 1b63e0abc2 Startup waits for the forced chunks before the first tick, so saved ticks there fire on time
MinecraftServer.prepareLevels waits for the chunks the restored tickets hold
(ChunkLoadCounter) before the first tick, and the first tick unpacks their
saved block and fluid ticks at the game time they were saved at. Horizon's
dedicated and integrated servers started ticking while the forced chunks were
still streaming in, and the saved ticks were unpacked from the tick the chunk
arrived, one tick or more late.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:58:28 +01:00
Nikita Maximilian Grimm 9ab82c59cd Merge main into hdr-tone-curve: #11987 is on main now 2026-11-08 00:56:51 +01:00
Nemotron e0cbbdb50a Fix piston push not zeroing entity velocity in lazy chunks (#12022)
move_pushed applied the resolved movement to position only, leaving the
entity's velocity untouched. In a ticking chunk the entity's own tick
masks this (PrimedTnt::tick_among restitutes), but in a lazy chunk the
velocity survives: a primed TNT pushed by a piston into a wall, ceiling
or slime keeps 0.4 / -0.8 / -0.8 instead of vanilla's 0 / 0 / 0.64.

Add restitute_piston after apply_push: compare the post-piston-limit
movement (limited) with the post-collision result (resolved) and, as
Entity.move(MoverType.PISTON, …) does, zero blocked horizontal axes,
zero or slime-bounce the vertical axis, and update on_ground. Covers
Player, Guest and every Entity kind including PrimedTnt.

Add unit test a_pushed_tnt_loses_velocity_into_a_wall (needs prepared
resources) asserting the TNT's vx is zeroed by a wall.

Co-Authored-By: Nemotron <[email protected]>
2026-11-08 02:56:20 +03:00
KBroneZandClaude Sonnet 5.5 56ec50a875 fix(port-pack): an empty shaderpacks folder is not a shader pack to convert
The CurseForge app, and mods such as Sodium, create `shaderpacks/` in an instance
even when the player never installed a shader pack. Import counted the folder
itself as one "shader pack, conversion pending" file and listed it under "not yet",
so a client-only instance such as one with Sodium, Lithium, Jade, malilib and
Litematica reported 4 "not yet" rows for 3 mods. A folder instance now reports
shaderpacks/ only when it holds something, and a zip only when it has a file
under its shaderpacks/ (a bare directory entry no longer counts).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-08 01:54:47 +02:00
Florian ErnstandClaude Opus 5.5 066b6e9cef Save a mannequin's own fields and none of a Mob's
Mannequin extends Avatar extends LivingEntity; it is not a Mob.
Mannequin.addAdditionalSaveData always writes profile, hidden_layers,
main_hand and pose after LivingEntity's fields, and none of
Mob.addAdditionalSaveData's. Horizon left out the four unless they were
loaded, and wrote PersistenceRequired, CanPickUpLoot, LeftHanded and NoAI.

A mannequin now reads main_hand and hidden_layers into the main-arm and
hidden-layer state it already had (a list with an entry that is not a part
hides nothing, as the codec's failed read), and writes all four from its
live state: the empty profile when it has none, the hidden layers in
PlayerModelPart order, its arm and its pose. It neither reads nor saves
Mob.readAdditionalSaveData's flags, and its save drops the Mob-owned keys
over a stale loaded compound. Every other kind is unchanged.

Fixes #12135.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:53:14 +01:00
KBroneZandClaude Opus 5.5 172d7c51e6 fix: keep the 1.20 partial reason on one line of words
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:51:33 +02:00
Nikita Maximilian Grimm 5cf4a1dec8 Merge remote-tracking branch 'origin/main' into t3/scheduled-item-tick-pass 2026-11-08 00:49:17 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4bf9b604f7 Keep the contact drop range half-open
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:49:15 +01:00
Nikita Maximilian Grimm b95e34a990 Merge remote-tracking branch 'origin/main' into tnt-piston-ground
# Conflicts:
#	crates/horizon-client/src/game/piston_push.rs
2026-11-08 00:46:47 +01:00
GlockSmithandOpenAI Codex GPT-6 e86ce41888 Merge main and retain HDR emission on styled sign text
Resolve the overlap with #11863 in the styled span and glyph loops. Keep its emission tier on glyphs, outlines and decoration quads, with regression coverage.

Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-11-07 18:42:38 -05:00
Horizon agentandClaude Opus 5.5 458f0e4134 Native shader packs: blend the section fade by distance per pixel
The fade-in's 16-to-32-block distance blend was computed per vertex and
carried in the flat hdr_code, so it was one value per triangle (the
provoking vertex's) and near faces faded in steps. The flat bits now hold
only the section's own visibility (the same for the whole section), and
horizon_material_faded blends it by the interpolated spherical distance,
as terrain.wgsl's interpolated visibility does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:40:34 +00:00
Kacper 29b3374b5d Merge pull request #12098 from NikitaMGrimm/boat-drop-motion
A destroyed boat drops its item with vanilla's drop motion
2026-11-08 00:40:24 +01:00
Kacper 02042bf6b4 Merge pull request #12097 from NikitaMGrimm/boat-fire-timer
A boat burns: its fire timer counts down, fire hurts it and water puts it out, as in vanilla
2026-11-08 00:40:18 +01:00
Kacper 79e277451b Merge pull request #12095 from Web-mc1/fix/telemetry-scrub-word-boundaries
telemetry: match the user name exactly when lowercasing moves boundaries
2026-11-08 00:40:10 +01:00
Kacper e5c1ea0612 Merge pull request #12092 from Web-mc1/fix/java-properties-save-convert
java_properties: write a NUL and a non-BMP character as Java does
2026-11-08 00:40:03 +01:00
Kacper 2f39e246fe Merge pull request #12084 from KBroneZ/claude/windows-unused-mut
Fix Windows-only unused_mut on the TCP keepalive builders
2026-11-08 00:39:56 +01:00
Kacper 2cd9e12ecd Merge pull request #12078 from KBroneZ/claude/oracle-windows
Run tools/neoforge_oracle.py on Windows
2026-11-08 00:39:43 +01:00
Kacper 6d32a88a50 Merge pull request #12069 from senjinthedragon/horizon-uniforms-lights-tier
Shader packs: Horizon's dynamic lights as uniforms, horizonTier for terrain and entities
2026-11-08 00:39:37 +01:00
Kacper 124e88e731 Merge pull request #12068 from NikitaMGrimm/fix/diode-moving-piston-support
A repeater or comparator on a retracting piston stays, as in vanilla
2026-11-08 00:39:30 +01:00
Kacper 1a004614f8 Merge pull request #12056 from TheMrZZ/fix/h255-mob-piston-ground
Project ground after unoccupied server NoAI Mob piston movement
2026-11-08 00:39:23 +01:00
Kacper 1edf540479 Merge pull request #12054 from TheMrZZ/fix/h234-machine-recipe-placement
Route recipe-book placement into actual furnace menus
2026-11-08 00:39:16 +01:00
Kacper 9bccc1056f Merge pull request #12053 from TheMrZZ/fix/h245-sulfur-bucket
Pick up a sulfur cube with an empty bucket
2026-11-08 00:39:09 +01:00
Kacper d144af4a11 Merge pull request #12049 from ilguerro/perf/9996-load-loadlight
Load saved light directly from its layers instead of expanding and repacking
2026-11-08 00:39:03 +01:00
Kacper 616707f3d7 Merge pull request #12033 from Vaspyyy/feat/native-muffle-sounds
Native mod API: muffle the game's sounds for a while, as ears ringing after a blast (native_muffle_sounds)
2026-11-08 00:38:56 +01:00
Kacper 1c5744ac24 Merge pull request #12014 from Vaspyyy/fix/native-mod-image-slot
Native mods: a game built with horizon-mixins gets the mod image slot, native-mods.json or not
2026-11-08 00:38:49 +01:00
Kacper c124718cbc Merge pull request #12002 from MrCloudy2/fix/sprint-hit-slowdown
lan_e2e: sprint-hit-slowdown runs up instead of teleporting, and catches a missing slowdown
2026-11-08 00:38:43 +01:00
Kacper 0b8f18dd40 Merge pull request #11992 from TheMrZZ/fix/h275-test-defs-world-packs
Load a world's test instances from its data packs when it opens
2026-11-08 00:38:36 +01:00
Kacper e51c277db7 Merge pull request #11987 from NikitaMGrimm/hdr-hud-composite
HDR float frame: the HUD and menus lay over the world's light
2026-11-08 00:38:29 +01:00
Kacper fc61e4e9ff Merge pull request #11986 from NikitaMGrimm/hdr-entity-marks
HDR: mobs get HDR lighting and held light sources shine again
2026-11-08 00:38:23 +01:00
Kacper 2c300290d5 Merge pull request #11983 from TheMrZZ/fix/h207-effective-equippable
Honor supported pumpkin Equippable overrides on item use
2026-11-08 00:38:10 +01:00
Kacper 16260920c3 Merge pull request #11969 from Mik00000/perf/mob-ai-stress-bench
mobs: reuse pathfinding allocation buffers and add stress_mob_ai benchmark
2026-11-08 00:38:03 +01:00
Kacper d84428c781 Merge pull request #11968 from TheMrZZ/fix/h237-clone-block-ticks
Copy a clone's pending block ticks to its destination
2026-11-08 00:37:57 +01:00
Kacper e5f23f8b40 Merge pull request #11958 from TheMrZZ/fix/h095-allay-liked-player-ttl
Keep an Allay's liked_player memory time to live and expire it
2026-11-08 00:37:50 +01:00
Kacper 17e4124d04 Merge pull request #11956 from Stink-O/native/hold-world
Native mod API: hold the terrain's picture while the world changes (`native_world_hold`)
2026-11-08 00:37:43 +01:00
Horizon agentandClaude Opus 5.5 37eafb6c1b Older clients: a 1.21.1 client's information gains 1.21.2's particle status
The reverse step for 1.21.1 clients passed ClientInformation through, so a
host read past its end. The older-server adapter now writes 1.21.1's real
layout (no particle status), which made lan_bot's
older_clients_of_each_release_join_and_are_sent_chunks fail at 1.21.1;
real 1.21.1 clients were refused the same way. The step appends
ParticleStatus.ALL in configuration and play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:37:41 +00:00
Kacper 4df2329711 Merge pull request #11942 from glitchsnake/fix/11804-block-outline-entity-pick
client/render: omit block outline when an entity is targeted (fixes #11804)
2026-11-08 00:37:36 +01:00
Kacper 7ca3fdeedd Merge pull request #11900 from TheMrZZ/fix/h175-frozen-first-tick-water
Register a newly loaded chunk's tick queues before commands reach it
2026-11-08 00:37:29 +01:00
Kacper e4ca9aba95 Merge pull request #11899 from CocofireHD/fix/8390-some-block-entities-render-too-dark-or-have
Block entities: shade by the face normal so shader packs don't darken them twice
2026-11-08 00:37:22 +01:00
Kacper 9848261b44 Merge pull request #11897 from abit-foggy/codex/studio-b2-camera-path
Studio: free camera and camera paths
2026-11-08 00:37:15 +01:00
Kacper 7e66da154b Merge pull request #11896 from NikitaMGrimm/hdr-hud-layer
HDR float frame: HUD and menus record how much of the world they cover
2026-11-08 00:37:08 +01:00
Kacper 3b03674d14 Merge pull request #11893 from CocofireHD/fix/8875-resource-packs-doesn-t-work-well
Fonts: read a pack's missing font files from the game below the overlay
2026-11-08 00:37:01 +01:00
Kacper 3fe5ad0d19 Merge pull request #11892 from TheMrZZ/fix/h173-live-dungeon-spawner
Create missing live dungeon spawner block entities
2026-11-08 00:36:54 +01:00
Kacper a1926a4901 Merge pull request #11882 from CocofireHD/fix/10228-server-specific-resource-pack-not-loading-pr
HUD fonts: pack a bitmap sheet from any texture path and read it from the atlas
2026-11-08 00:36:47 +01:00
Kacper 8dd178353a Merge pull request #11878 from CocofireHD/fix/10095-crash-when-enabling-a-shader-pack-on-opengl
OpenGL: report the context's texture units and refuse a bad pack instead of crashing
2026-11-08 00:36:40 +01:00
Kacper 0845a33141 Merge pull request #11864 from TheMrZZ/fix/h150-direct-passenger-relations
fix: select direct mob passengers for execute relations
2026-11-08 00:36:33 +01:00
Kacper e05360eb74 Merge pull request #11863 from senjinthedragon/glow-ink-text-tier
HDR and shader packs: glowing sign text shines
2026-11-08 00:36:27 +01:00
Kacper b967051f4f Merge pull request #11861 from Joansitoh/fix/11849-home-tab-strip
fix(launcher): wrap Home's tab strip on narrow windows instead of scrolling it sideways
2026-11-08 00:36:20 +01:00
Kacper 6c723039a7 Merge pull request #11858 from TheMrZZ/fix/h170-place-feature-inline
Place valid inline features with /place feature
2026-11-08 00:36:13 +01:00
Kacper a2c4c2d4fc Merge pull request #11856 from Ragbie/fix/infested-hurt-draw-order
Infested draws at the hurt, before an enderman's or a shulker's own roll
2026-11-08 00:36:06 +01:00
Kacper 3b5b0d8a3c Merge pull request #11850 from GIGABAIT93/l10n/uk-nightly-1107
Ukrainian: the 445 strings uk_ua.json lacked (PvP & HUD, Studio, mods, cosmetics)
2026-11-08 00:35:59 +01:00
Kacper 15ebef6a69 Merge pull request #11847 from TheMrZZ/fix/h204-depleted-absorption
Remove a mob's Absorption effect once the absorption is used up
2026-11-08 00:35:52 +01:00
Kacper 40630164be Merge pull request #11839 from vnsmoreira/packs/polytone-legacy
Polytone: legacy math expressions, color and distance functions
2026-11-08 00:35:45 +01:00
Kacper c6a2fd5291 Merge pull request #11838 from vnsmoreira/packs/polytone-groups-fix
Polytone: leave keys beside the attribute groups alone
2026-11-08 00:35:37 +01:00
Kacper 29d41a899b Merge pull request #11836 from yahya-nusseibeh/fix/remote-view-distance-overflow
Multiplayer: a server's view distance and chunk centre can't overflow
2026-11-08 00:35:31 +01:00
Kacper 61d5d3bd8a Merge pull request #11835 from yahya-nusseibeh/fix/portmap-zero-lifetime
Play with Friends: refuse a port mapping granted no lifetime
2026-11-08 00:35:24 +01:00
Kacper 99bf985e0a Merge pull request #11834 from yahya-nusseibeh/fix/guest-tunnel-token
Play with Friends: the guest's loopback tunnel takes only its own client
2026-11-08 00:35:17 +01:00
Kacper dc098b606b Merge pull request #11833 from yahya-nusseibeh/fix/pack-overlay-prune
Resource packs: delete overlays no selection has used lately
2026-11-08 00:35:11 +01:00
Kacper 51f73ac425 Merge pull request #11832 from yahya-nusseibeh/fix/pack-overlay-budget
Resource packs: cap what one overlay build may unpack
2026-11-08 00:35:04 +01:00
Kacper ac9bd0527a Merge pull request #11827 from TheMrZZ/fix/h168-mob-fall-flying
Keep a mob's FallFlying flag and wear its glider as vanilla does
2026-11-08 00:34:57 +01:00
Kacper 8715111af1 Merge pull request #11825 from vnsmoreira/packs/polytone-expressions
Polytone: the scripting expression language
2026-11-08 00:34:50 +01:00
Kacper 790efa9cb7 Merge pull request #11818 from NikitaMGrimm/hdr-note-hud-first
HDR design note: the HUD's layer and the tone curve before light is written
2026-11-08 00:34:43 +01:00
Kacper ae46423a77 Merge pull request #11795 from Hiuoy/port-pack/renamed-tooltip-anvil
Port pack: RenderTooltipEvent.Color and AnvilRepairEvent under their 26.1 names
2026-11-08 00:34:36 +01:00
Kacper 9da1e6f55f Merge pull request #11794 from doodlyyousuf/251107-fix-entity-air-drag
fix: widen entity air and fluid drags from vanilla floats
2026-11-08 00:34:29 +01:00
Kacper e27e97e58b Merge pull request #11793 from TheMrZZ/fix/h149-local-coordinate-math-current
fix: match exact local command rotation arithmetic
2026-11-08 00:34:22 +01:00
Kacper 02b75ab1f3 Merge pull request #11788 from Teiwazik/render/taa-motion-clip
TAA upscaling smears the world under motion
2026-11-08 00:34:15 +01:00
Kacper 3be40a13a3 Merge pull request #11786 from Teiwazik/render/upscaling-sharpness
Upscaling Sharpness: sharpen the TAA-upscaled world
2026-11-08 00:34:07 +01:00
Kacper c69d499386 Merge pull request #11778 from Teiwazik/fix/framed-maps-slots
Framed maps: no slot taken from a map drawn this frame
2026-11-08 00:33:59 +01:00
Kacper 1a83468f0d Merge pull request #11776 from MSedgeMC/fix/frog-panic-speed
Frogs panic at AnimalPanic's 2.0 speed multiplier
2026-11-08 00:33:51 +01:00
Kacper 1abb582ab4 Merge pull request #11769 from MSedgeMC/port_map/stale-batch-two
port map: a row that survived a rename
2026-11-08 00:33:45 +01:00
Kacper d44210f9b4 Merge pull request #11765 from MSedgeMC/test/enchant-durability
test: compare enchantment durability effects
2026-11-08 00:33:38 +01:00
Kacper 3cefb44bba Merge pull request #11740 from Detimes/fix-11721-explode-radius
Older clients: explosions no longer fail for 1.21.8 and older; real radius for 1.21.1
2026-11-08 00:33:25 +01:00
Kacper 5c28d7f248 Merge pull request #11723 from TheMrZZ/fix/loaded-arrow-shot-game-event
Dispatch ordinary arrow shoot events on their first tick
2026-11-08 00:33:18 +01:00
Kacper b1dae52509 Merge pull request #11711 from MSedgeMC/test/falling-block
test: compare falling block landings
2026-11-08 00:33:11 +01:00
Kacper 341bd0ba5b Merge pull request #11709 from TheMrZZ/fix/connected-guest-attribute-commands
Route attribute commands through the selected guest body
2026-11-08 00:33:04 +01:00
Kacper 6e423b9caa Merge pull request #11701 from Alysia42/random-source
Parity: entity and RandomSource.create() randoms draw from the level's random or are hand-seeded
2026-11-08 00:32:57 +01:00
Kacper 604c3a6654 Merge pull request #11694 from TheMrZZ/fix/h289-mob-equipment-custom-data
fix: match typed custom data in mob equipment predicates
2026-11-08 00:32:51 +01:00
Kacper b229c37e45 Merge pull request #11691 from TheMrZZ/fix/warden-loaded-memory-slots
Bind loaded Warden memories to their runtime slots
2026-11-08 00:32:44 +01:00
Kacper dcb6409ebd Merge pull request #11681 from MSedgeMC/test/nether-portal-shape
test: compare nether portal frame search
2026-11-08 00:32:37 +01:00
Kacper 217b5e97f7 Merge pull request #11672 from TheMrZZ/fix/loot-kill-sheep-state
Bind actual sheep target state in loot-kill commands
2026-11-08 00:32:31 +01:00
Kacper 4fe3ed58b4 Merge pull request #11666 from Raydnde/fix/copper-chest-wax-and-viewers-11378
Waxing a double copper chest reaches both halves; a golem's open chest doesn't weather
2026-11-08 00:32:24 +01:00
Kacper c15fa6c55c Merge pull request #11659 from TheMrZZ/fix/camel-inherited-saved-fields
Save Camel inherited horse fields from modeled state
2026-11-08 00:32:17 +01:00
Kacper d6d3042086 Merge pull request #11656 from TheMrZZ/fix/h212-crafter-drop-name
fix: keep vanilla crafter drops unnamed
2026-11-08 00:32:10 +01:00
Kacper c929d96fa5 Merge pull request #11652 from ilguerro/perf/11126-structure-starts-jigsaw
Read saved jigsaw structure starts instead of regenerating them
2026-11-08 00:32:03 +01:00
Kacper 5555cd8cda Merge pull request #11634 from k3yh/cushion-selectors
Selectors find cushions
2026-11-08 00:31:56 +01:00
Kacper f85f824a27 Merge pull request #11633 from MSedgeMC/fix/old-stats-file
Move an old name-based stats file to the player's uuid
2026-11-08 00:31:49 +01:00
Kacper e1a8d50c79 Merge pull request #11625 from TheMrZZ/fix/loaded-sleeper-bed-position
Refresh loaded sleeper position at the bed on its first tick
2026-11-08 00:31:42 +01:00
Kacper b69935ff31 Merge pull request #11624 from TheMrZZ/fix/h290-pot-template-compounds
fix: save pot sherd string templates as item compounds
2026-11-08 00:31:35 +01:00
Kacper 900ce5e15b Merge pull request #11612 from k3yh/ride-vehicles
/ride takes armor stands, displays, interactions and markers as vehicles
2026-11-08 00:31:28 +01:00
Kacper 7435df4fae Merge pull request #11610 from TheMrZZ/fix/world-pack-enchantment-definitions
Load active world-pack enchantment definitions
2026-11-08 00:31:21 +01:00
Kacper afce5629b7 Merge pull request #11590 from TheMrZZ/fix/h145-required-function-tags
fix: reject function tags with unresolved required references
2026-11-08 00:31:14 +01:00
Kacper 7f22f76867 Merge pull request #11576 from TheMrZZ/fix/folder-datapack-metadata
Require folder data-pack metadata sections before discovery
2026-11-08 00:31:06 +01:00
Kacper 08cb78f385 Merge pull request #11573 from vnsmoreira/attributes/modifiers
Environment attributes: the registry and modifiers, checked against vanilla
2026-11-08 00:30:59 +01:00
Kacper ebaea75be6 Merge pull request #11565 from TheMrZZ/fix/function-command-admission
Reject functions containing invalid ordinary commands
2026-11-08 00:30:52 +01:00
Kacper ad2aabd6cf Merge pull request #11558 from Vaspyyy/perf/vault-players-lazy
Vaults: list the players only when a vault looks for them (−12.6% tick_block_entities)
2026-11-08 00:30:46 +01:00
Kacper 534feb4833 Merge pull request #11544 from voidsipp/fix/11328-trade-modifiers
Fix trade modifiers and potion payment matching
2026-11-08 00:30:39 +01:00
Kacper b7d134d2ef Merge pull request #11540 from voidsipp/fix/11330-explosion-item-health
Spill contents when explosions destroy dropped containers
2026-11-08 00:30:32 +01:00
Kacper 921f2fa118 Merge pull request #11533 from TheMrZZ/fix/loot-mine-executor-presence
Carry actual loot-mine executor presence into block drops
2026-11-08 00:30:25 +01:00
Kacper 0429cd882f Merge pull request #11526 from Vaspyyy/perf/cart-push-loot
Minecarts: keep the loot roll out of the copies a cart push makes (−52% tick_carts)
2026-11-08 00:30:17 +01:00
Kacper b68f5b5327 Merge pull request #11509 from voidsipp/fix/smithing-transform-11307
Fix smithing transform optional inputs and result templates
2026-11-08 00:30:10 +01:00
Kacper d21170f02d Merge pull request #11508 from TheMrZZ/fix/camel-loaded-pose-timestamp
Preserve loaded Camel pose timestamp boundaries
2026-11-08 00:30:03 +01:00
Kacper 6488524356 Merge pull request #11498 from TheMrZZ/fix/effective-item-component-validation
Reject stackable damageable item command inputs
2026-11-08 00:29:56 +01:00
Florian ErnstandClaude Opus 5.5 aee1b8b81c Adopt /data edits on a live interaction entity
EntityDataAccessor.setData loads the edited compound into the same entity,
and Interaction.readAdditionalSaveData reads its width, height, response
and the attack and interaction records. Horizon kept an interaction only
as a generic entity with its loaded compound, and its live data write
took markers alone, so every /data edit answered "Nothing changed".

A server interaction now holds its settings (InteractionState: width,
height, response and the two PlayerAction records) next to its position.
It reads them when it loads or is summoned, saves from them
(addAdditionalSaveData, a record only when set), and a /data edit decodes
the edited compound into the same entity, which keeps its id and UUID.
Every other kind is unchanged.

Fixes #12131.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:29:55 +01:00
Kacper c9e8e4e8e0 Merge pull request #11497 from voidsipp/fix/11316-stack-tool-mining
Respect stack tool mining rules and actual eye immersion
2026-11-08 00:29:49 +01:00
Kacper b0abfbd196 Merge pull request #11491 from DUzzL/fix/10041-adventure-placed-entities
Adventure mode: honor can_place_on for placed entities and fireworks
2026-11-08 00:29:42 +01:00
Kacper 2c0564db8c Merge pull request #11485 from voidsipp/fix/11317-block-crack-feedback
Match player mining crack stages and broadcast distance
2026-11-08 00:29:34 +01:00
Kacper 41a021365e Merge pull request #11483 from TheMrZZ/fix/llama-saved-temper
Preserve signed equine temper across load and save
2026-11-08 00:29:28 +01:00
Kacper ef36a05d90 Merge pull request #11480 from brasga-a/fix/11314-player-food-predicate
Player predicates test food level and saturation
2026-11-08 00:29:21 +01:00
Kacper ab4a0fd56e Merge pull request #11478 from DUzzL/fix/11379-copper-golem-name
Keep the copper golem's name on its statue
2026-11-08 00:29:14 +01:00
Kacper 0e12cc64de Merge pull request #11474 from voidsipp/fix/11319-gateway-item-motion
Preserve item motion through End gateways
2026-11-08 00:29:07 +01:00
Kacper 4a6da129ce Merge pull request #11467 from voidsipp/fix/round5-5-11321
Respect world borders for pistons and natural spawning
2026-11-08 00:29:00 +01:00
Kacper 29f5ef342c Merge pull request #11466 from MSedgeMC/fix/region-external
Region: a chunk's external stream reads even when its stub length disagrees or its .mcc is missing
2026-11-08 00:28:53 +01:00
Kacper 672c9a7974 Merge pull request #11465 from TheMrZZ/fix/h097-loaded-air-first-tick
Discard loaded air falling blocks on their first tick
2026-11-08 00:28:46 +01:00
Kacper 3d275f03bf Merge pull request #11459 from DUzzL/fix/11387-beacon-effect-overlap
Beacon: select players by bounding-box overlap
2026-11-08 00:28:38 +01:00
Kacper 1dd7423d00 Merge pull request #11457 from voidsipp/fix/round5-1-11333
Respect Silent on explosions and potion impacts
2026-11-08 00:28:31 +01:00
RINNECODERandClaude Opus 5.5 c56e9312ee Port pack: a mod's own rarity or map colour stays the template's, not a refused mod
NeoForge lets mods add item rarities (Immersive Engineering's masterwork) and some blocks carry a
map colour id past 26.3's 61 (Mekanism's gas blocks: 63). The game refuses the whole content
root for either, so one such file left every block of the mod out. They now keep the template's
and the report names them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:28:23 -06:00
Kacper 9c0c0c7210 Merge pull request #11456 from MrCloudy2/fix/dropper-container-entities
Droppers insert into chest boats and container minecarts
2026-11-08 00:28:23 +01:00
Kacper 671a85a7f2 Merge pull request #11455 from NikitaMGrimm/frame-identity-no-fall
Frame identity: keep the player on a block while the scenes are built
2026-11-08 00:28:15 +01:00
Kacper 63a8290dcb Merge pull request #11445 from MSedgeMC/test/furnace-tick
furnace: direct block-entity tick parity (burn table, XP, mid-cook reset)
2026-11-08 00:28:08 +01:00
Kacper fbd9b2ce23 Merge pull request #11434 from DUzzL/fix/11306-anvil-grindstone
Read stack names and durability components in anvil and grindstone
2026-11-08 00:28:00 +01:00
Kacper e290e2c196 Merge pull request #11433 from TheMrZZ/fix/falling-live-data-adoption
Adopt live FallingBlock data updates
2026-11-08 00:27:53 +01:00
Kacper 5671fc9619 Merge pull request #11432 from MSedgeMC/fix/phantom-anchor-save
Save a phantom's circling anchor with its chunk
2026-11-08 00:27:46 +01:00
Kacper 436e839ebf Merge pull request #11425 from TheMrZZ/fix/h051-kill-vehicle-contents
Spill explicit vehicle container contents on kill removal
2026-11-08 00:27:39 +01:00
Kacper 860f3f05d5 Merge pull request #11423 from DUzzL/fix/11311-scaffold-water
Read scaffolding as its stable shape in fluid wall tests
2026-11-08 00:27:33 +01:00
Kacper 88d7c0eb2c Merge pull request #11422 from voidsipp/fix/round5-6-11320
Preserve comparator output and hopper cooldown across saves
2026-11-08 00:27:26 +01:00
Kacper 2cf505bbd7 Merge pull request #11421 from DUzzL/fix/11324-spawn-heightmap
Fix natural spawn height bound off by one
2026-11-08 00:27:19 +01:00
Kacper cd218ea58e Merge pull request #11420 from TheMrZZ/fix/falling-block-state-default
Use modern falling-block state decoding and sand fallback
2026-11-08 00:27:12 +01:00
Kacper 161c4e5677 Merge pull request #11414 from voidsipp/fix/round5-3-11315
Honor block drops and RNG order for mining XP
2026-11-08 00:27:05 +01:00
Kacper a6dc594cdf Merge pull request #11413 from ilguerro/fix/w149
A server player's glide ends on a climbable (as_guest copies fall_flying back)
2026-11-08 00:26:58 +01:00
Kacper 013c3952a7 Merge pull request #11411 from ilguerro/fix/w167
Wait for the client's load after the End credits respawn
2026-11-08 00:26:51 +01:00
Kacper 5365f2be63 Merge pull request #11407 from ilguerro/fix/w157
Drop a converting piglin's inventory like vanilla
2026-11-08 00:26:44 +01:00
Kacper 0f23639844 Merge pull request #11400 from ilguerro/fix/w144
Search neighboring columns for dolphin breathing like vanilla
2026-11-08 00:26:38 +01:00
Kacper 997fe4515d Merge pull request #11390 from UMCEKO/dims/dragon-facts
The dragon fight follows has_ender_dragon_fight (#11282, 5/9)
2026-11-08 00:26:30 +01:00
Kacper 5d16154c0b Merge pull request #11366 from UMCEKO/dims/mob-facts
Mobs and spawn rules read the dimension type (#11282, 4/9)
2026-11-08 00:26:24 +01:00
Kacper 47e562e8b9 Merge pull request #11350 from AlfaRojo/fix/sky-darken-truncates
Sky darkening truncates the light level, as updateSkyBrightness does
2026-11-08 00:26:16 +01:00
Kacper 74b8550421 Merge pull request #11344 from UMCEKO/dims/gameplay-facts
Gameplay reads the dimension type, not the dimension (#11282, 3/9)
2026-11-08 00:26:09 +01:00
RINNECODERandClaude Opus 5.5 9f52bf9a83 Port pack: a block that can't occlude (noOcclusion) carries occludes=false
The oracle reports can_occlude per state, but the carry ignored it: a
full-cube block made with noOcclusion() (one its block entity draws) took
its template's occlusion and hid the faces behind it, leaving a hole.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:26:03 -06:00
Kacper 54c3a21b29 Merge pull request #11116 from Delete228/content-context-geometry-upstream
Native content: resolve block geometry by entity context
2026-11-08 00:26:02 +01:00
Kacper d6f0b3042f Merge pull request #11098 from Delete228/native-state-entity-factory-upstream
Native mods: create block entities from the actual state
2026-11-08 00:25:55 +01:00
Kacper 0422dbc79f Merge pull request #10559 from rison1337/perf/cache-trim-preparation
Reuse unchanged armor-trim preparation across frames
2026-11-08 00:25:48 +01:00
Kacper 914c1e9a3b Merge pull request #10552 from ilguerro/perf/9996-surface
Write surface rule runs of one block at once
2026-11-08 00:25:41 +01:00
Kacper 62dd244cb6 Merge pull request #10511 from TheMrZZ/fix/noai-nautilus-air
Keep superclass air refill for NoAI Nautilus
2026-11-08 00:25:34 +01:00
Kacper 0e446e4336 Merge pull request #10242 from TheMrZZ/fix/h297-potion-effect-icon
Save default icon flags in custom potion effects
2026-11-08 00:25:27 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 a9612bdabc Apply an item's gravity before pushing it out of a block
ItemEntity.tick applies gravity (or the fluid movement) first and then
moveTowardsClosestSpace, whose 0.75 scales the already-fallen vertical
speed. The push ran before tick_move_before_block_effects, so gravity
came after it. Pass the escape speed into the move and apply it right
after gravity; the native block-contact pipeline is unchanged.

Fixes #12018

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:25:22 +01:00
Kacper 48344d0a6b Merge pull request #10210 from TheMrZZ/fix/h103-trade-reputation-gate
Defer villager trade reputation until its AI phase
2026-11-08 00:25:20 +01:00
Kacper 87d4a87960 Merge pull request #10201 from DUzzL/fix/10147-projectile-catchup-stutter
Unstick arrows and tridents the server sets moving
2026-11-08 00:25:13 +01:00
Kacper 159b5c4be6 Merge pull request #8718 from TheApple05/feature/older-servers-1.21.8
Older servers: experimental native adapters for 1.20.2–1.21.8
2026-11-08 00:25:05 +01:00
Oloion 2a33c013d7 perf: branchless frustum culling, zero-copy Arc meshing regions and 3D upload priority 2026-11-08 02:24:31 +03:00
Nikita Maximilian GrimmandClaude Opus 5.5 b58576b523 Reload saved portal tickets and pause their expiry while their chunk loads
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:24:26 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 a20d3296b4 A sideways piston push no longer leaves an experience orb marked on ground
The orb kept its own vertical-only on-ground rule for a piston push. It
now takes the server's end-of-move rule that items, primed TNT and armor
stands use: on ground only when the push's downward part was blocked.

Fixes #12100

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:23:06 +01:00
RINNECODERandClaude Opus 5.5 eadbb41095 Shader packs: run a workGroups over 65535 per dimension
wgpu caps a dispatch at 65535 work groups per dimension and panics past it;
GLSL packs may ask for more (131072). A larger workGroups now runs as the
same number of groups in a shape within the limit, and main works out each
group's place in the declared dispatch, so gl_WorkGroupID,
gl_GlobalInvocationID and gl_NumWorkGroups read what they would there. The
shape uses every group exactly, so no group leaves before a barrier. A
count no shape holds skips the program with a note; an oversized
workGroupsRender dispatch is not run instead of panicking.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:22:07 -06:00
RINNECODERandClaude Opus 5.5 bc59734a29 Shader packs: copy a struct or array holding atomics as its declared type
naga's GLSL frontend turns a uint/int that an atomic*() call touches into
an atomic type after parsing. A load or store of a whole struct or array
holding such a member then had the upgraded type: `Cell c = buf.cells[i];`
and `uint copy[4] = shared_counts;` failed validation. A whole struct or
fixed-size array of integers in a buffer or shared memory is now loaded
and stored member by member, so each copy keeps its declared type.

Vendored-naga patch: third_party/naga/src/front/glsl/context.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:20:58 -06:00
RINNECODERandClaude Opus 5.5 626c60740d Shader packs: make an image in its layout qualifier's format
An image declared RGB10_A2, RGB10_A2UI or a 16-bit SNORM format was made
as an RGBA8, RGBA8UI or 8-bit SNORM texture, so a shader binding it as
layout(rgb10_a2) etc. was refused ("is rgb10_a2 but its texture is
Rgba8Unorm"). Each now gets the wgpu format of its GLSL qualifier; one the
GPU can't store into is left out with a note, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:20:58 -06:00
RINNECODERandClaude Opus 5.5 0bd5a4f201 Shader packs: an exact argument beats a conversion in GLSL overloads
naga's GLSL frontend never compared an exactly matching argument with the
previous best overload's conversion there, so greaterThanEqual(ivec3, uvec3)
found the uvec3 overload no better than the vec3 one and reported
"Ambiguous best function". GLSL 4.60 §6.1 ranks an exact match above any
implicit conversion: the uvec3 overload wins. Zephyr Starlight's
shadowcomp1-4 failed on it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:20:49 -06:00
RINNECODERandClaude Opus 5.5 cf0b0d02c8 Shader packs: a setting in a comment may end at its line
Packs list their const settings in a block comment one per line with no
';' (`const vec4 colortex6ClearColor = vec4(0.0, 0.0, 0.0, 1.0)`). The
reader looked for the next ';' and took every following line as the value,
so each such setting was refused as 'not a vec4' (Zephyr Starlight logged
85 multi-line notes) and its clear colour was lost. In a comment a
declaration now ends at the first ';' or line end; live code still reads to
the ';'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:20:49 -06:00
Nikita Maximilian GrimmandClaude Opus 5.5 60fff3987f Keep the drops of entities' block contacts on the next tick
tick_blocks applies the entities' block contacts (a boat breaking a lily
pad) before the scheduled ticks; vanilla runs them inside each entity's
own tick, so their drops still wait for the next tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:19:02 +01:00
Evan-lhmandClaude Opus 5.5 1b93764780 Launcher: a 0 px blur leaves the glass plates sharp (#12062)
With the blur at 0 the plates still read their backdrop bilinearly between
texel centres, and a texel is 3 to 6 logical pixels of pixel art, so the
scene behind the glass stayed slightly soft. With no blur, read the texel
under the point instead.

Fixes #12062

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:18:19 +01:00
Horizon agentandClaude Opus 5.5 52aad2205d Older servers: keep the particle markers inside the adapter's connections
The render crate read a trail duration of -1 as the pre-1.21.4 marker, and
the client read particle ids -2/-3 as the old entity-effect providers, for
any server: a 26.3 server sending those values got the legacy behaviour.
The client now decides from the connection's release (received_particle)
and passes an explicit legacy_trail_lifetime option; a current server's
-1 stays a lifetime of -1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:18:13 +00:00
KBroneZandClaude Opus 5.5 da4d26b00d feat: carry 1.20 jars' data under 1.21's folder names
A content mod jar whose data files are mostly in the folders 1.21
renamed (recipes/, loot_tables/, structures/, advancements/,
predicates/, item_modifiers/, functions/ and tags/blocks, items,
entity_types, fluids, game_events, functions) was refused by `carry`.
Now the carry moves each such file to the singular name 26.3 reads.
A file the jar also ships under the new name keeps that copy, and a
1.21 jar's stale plural folder is left as it was.

The JSON stays 1.20's: a recipe whose result names its item as `item`
(before 1.20.5), which 26.3 skips, is kept as it is instead of counted
as upgraded, and NBT in loot functions, ingredients and predicates is
not upgraded yet. The report therefore marks such a mod partial
whatever its harness says, and says why. `check` still refuses 1.20
jars until the JSON steps of #10262 land.

Part of #10262.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 01:14:06 +02:00
KBroneZandClaude Sonnet 5.5 342315073f Launcher: the data pack support text says what the game reads today
The browser's data pack note still said recipes, loot tables, advancements, other tags and
worldgen were not read. The game reads them now. The note goes through tr() and is in all
86 language tables (translated in 14, English elsewhere).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-08 01:13:03 +02:00
martipopsandClaude Opus 5.5 de0132b638 Controllers: the craft prompt says Quick Craft, and its setting's description is shorter
Two words, like the guide's other rows, and the same name as Quick Craft on
Servers. The exact rule (single player, LAN worlds from the server list) stays
in docs/controllers.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:12:25 -05:00
Florian ErnstandClaude Opus 5.5 31d39aec80 Count /loot kill under execute as a connected player as that player's kill
LootCommand.dropKillLoot takes the command source's entity as the killer:
a player is the last damage player, so killed_by_player holds, and its
own weapon counts for looting. Under `execute as <connected player>` from
another player, Horizon counted no player kill and used the issuer's
held stack.

For a connected player as the source, /loot kill now reads that player's
selected stack with it in place (for_player); an empty hand stays empty.
The loot roll, the output, the feedback and the result run as before,
after the issuer is back. Other sources keep their current rule.

Fixes #12122.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:11:21 +01:00
Nikita Maximilian Grimm 30f1fc145d Merge remote-tracking branch 'origin/main' into t3/scheduled-item-tick-pass 2026-11-08 00:10:09 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 f509e284bf Tick an item a scheduled block tick drops in that tick's entity pass
ServerLevel.tick runs the block ticks and block events before
entityTickList.forEach, so a dropper's item ticks on the tick it is
dispensed. The item cutoff was taken right after the tick functions;
take it just before the entity pass instead. Primed TNT and falling
blocks keep their own packet cutoff (#10063).

Fixes #12017

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:10:05 +01:00
itarqos5andClaude Sonnet 5.5 3d5216e717 perf(render): pass terrain's interpolants in 5 locations, not 12
Terrain's vertex stage exported 20 scalars in 12 interpolant locations. A GPU's
vertex stage pays per location (AMD gives each a parameter slot): on an RX 580 the
12 cost 3.2 ns per quad and the same values in 5 cost 1.6. TerrainOut stays the type
the shader code uses; TerrainWire crosses the stage boundary, each component
interpolated on its own and the integers flat, so a pixel gets the same values.

RX 580, Vulkan, 1080p, render distance 16: village 523.7 -> 590.8 FPS (+12.8%),
forest 267.6 -> 323.7 (+21.0%).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-07 23:09:46 +00:00
Horizon agentandClaude Opus 5.5 9bc110bae6 Riding: skip the extra rider pass when nothing rides the player
The second position_riders() call after the player moves rebuilt the mob
index every tick for every client; run it only when the riding graph has
something on this player. Also forget passengers whose vehicle is removed,
and add the new field to main's second (studio) Remote initializer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:08:57 +00:00
Florian ErnstandClaude Opus 5.5 8f9e984d63 Compare a mob's hurt cooldown on the incoming damage
LivingEntity.hurtServer compares a hit inside the hurt cooldown
(invulnerableTime > 10) with lastHurt by its incoming amount, before
armor and Resistance, and keeps that amount as lastHurt. actuallyHurt then
reduces only the difference. Horizon reduced the hit first, then compared
and stored the reduced amount. Under full Resistance every hit reduced to 0,
so a stronger hit inside the cooldown was refused. With armor, a second
hit dealt reduced(8) - reduced(4), not reduced(8 - 4).

The comparison now uses the amount after the helmet, freeze and infinity
changes. The existing armor, Resistance, protection, witch and wolf armor
steps get the accepted difference, and last_hurt keeps the incoming amount.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 00:07:47 +01:00
Ludovic Marie a392d4b3ba Merge pull request #12101 from horizon-rust/fix/anticheat-effect-attributes
🤖 Agent: the client keeps only the server's attribute modifiers, so Speed now ends one tick after RemoveMobEffect as in vanilla (ServerPlayer.doTick runs after sendDirtyEntityData); Paper+GrimAC bench 7-17 Simulation flags at expiry -> 0, 8 selected harnesses pass.
2026-11-08 00:07:47 +01:00
hunix 14ba691e53 Components: keep a translatable fallback literal instead of looking it up as a key
The component parser translated a missing key's fallback as if it were another key, so a fallback that named a
real key showed that key's translation. It now builds the text through Text::translate_with_fallback, as
TranslatableContents.decompose does (Language.getOrDefault(key, fallback)), which also keeps the fallback in the
component's JSON. Fixes #12042.
2026-11-07 13:02:44 -10:00
hunix 877cb37978 Web panel: accept LAN browsers when bound to 0.0.0.0 or ::
Bound to every interface, the panel's Host check accepts any IP address with the bind's port (and localhost),
which a rebound DNS name can never be, and the login link names this computer's network address instead of
0.0.0.0. Fixes #12039.
2026-11-07 13:02:44 -10:00
Nikita Maximilian Grimm 83535ddcc1 Merge remote-tracking branch 'origin/main' into minecart-drop-motion 2026-11-07 23:58:46 +01:00
Nikita Maximilian Grimm 2c672eae2d Merge remote-tracking branch 'origin/main' into tnt-piston-ground 2026-11-07 23:58:26 +01:00
bulkandClaude Opus 5.5 12273a4e8e Natural spawning: find the players near a spawn start by square
Every spawn start measured its distance to every player to keep those
within reach of its attempts. With more than 8 players, the level keeps them
by the 256-block square they stand in, built once a pass first asks, and a
start reads the 3×3 squares around it: every player within reach is in
them, and the nearest one is the same in any order. The spawn draws are
untouched.

(the player-square part of commit e3b70aace7; its region spawning is left
out)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:56:49 +00:00
sergue212fbrightandOpenAI Codex GPT-6 796047f1e8 Render styled sign text and animate obfuscated glyphs
Follow vanilla 26.3 sign layout and baked glyph geometry, preserving styles through clipping and rebuilding animated sign meshes. Extend the block-entity oracle with styled components and glyph vertices.

Co-Authored-By: OpenAI Codex GPT-6 <[email protected]>
2026-11-07 17:53:48 -05:00
Ludovic Marie 491710cd2f Merge pull request #11880 from horizon-rust/agent/native-build-sign
🤖 Agent: verifies the per-mod-set build signature with its own Ed25519 key (separate from the release key) before the launcher runs a prebuilt binary; independent review found it sound; opt-in stays off until the rollout steps.
2026-11-07 23:53:00 +01:00
Ludovic Marie 0932cefc79 Merge pull request #12055 from horizon-rust/vanilla-split/extras-feature
🤖 Agent: the default build resolves the same features as before (390 third-party units with identical fingerprints); every new gate is cfg!(), so CI's existing --no-default-features clippy already compiles the no-extras side and no stub can drift.
2026-11-07 23:49:40 +01:00
Kacpherek 34194ab7ec test: give the remote mining fixture's remote_break_restricted the position it now takes 2026-11-07 23:47:06 +01:00
Florian ErnstandClaude Opus 5.5 7803f1351e Cite AttributeMap.pack for the sulfur cube attribute save
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:46:38 +01:00
Florian ErnstandClaude Opus 5.5 c9b07b1ffb Pick /test pos's block with a connected source player's view
TestCommand.showPos takes the source player (getPlayerOrException) and
that player's own pick: its eyes and view, not the execute position. So
`execute as <connected player> run test pos` from another player answers
with the block that player looks at, and the issuer is told. Horizon
only accepted the player in place, so the command did nothing.

For a connected player as the source, /test pos now picks the block with
that player in place (for_player) and only the pick runs there; the test
is looked up in the command's level and the issuer told afterwards. Every
other source keeps its current rule, and the highlight packet is
unchanged.

Fixes #12102.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:45:41 +01:00
Kacpherek ec77214342 Send a bounce in the tick it happens: move the update clock to its next interval (ServerEntity.sendChanges) 2026-11-07 23:42:53 +01:00
PetroandOpenAI GPT-6 Luna bec6ac6448 Fix legacy Intel Metal startup and terrain rendering
Use compatible compute shaders and direct terrain draws on older Intel Metal.

Co-Authored-By: OpenAI GPT-6 Luna <[email protected]>
2026-11-07 23:42:25 +01:00
Kacpherek a2b3f669ef Swing at a wandering trader only for a named name tag, not a lead or an unnamed tag (NameTagItem, Entity.interact) 2026-11-07 23:42:05 +01:00
Kacpherek 7bfecd3fd1 Time the End flash by the End's own clock (ClientLevel.tick: getDefaultClockTime) 2026-11-07 23:41:38 +01:00
Kacpherek 43ef30f98c Draw wings and move the cape only for a chest-slot equippable (HumanoidArmorLayer.shouldRender) 2026-11-07 23:41:25 +01:00
Nikita Maximilian Grimm 2dbcb75cfa Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 23:41:15 +01:00
Kacpherek ce4f0d7074 Let shears in hand still take a camel's back seat or open a ridden mount's inventory (Entity.interact falls through to mobInteract) 2026-11-07 23:41:12 +01:00
Kacpherek 4ba236928d Run no heartbeat sound or random draw for the warden on the server thread (Warden.tick's client branch only) 2026-11-07 23:41:04 +01:00
Kacpherek GIT_COMMITTER_NAME=Kacpherek [email protected] [email protected] 34c5bb84e6 Keep a silent breeze's whirl and idle sounds quiet (EntityBoundSoundInstance.canPlaySound) 2026-11-07 23:40:45 +01:00
Kacpherek GIT_COMMITTER_NAME=Kacpherek [email protected] [email protected] e809bed768 Play the warden's heartbeat once in single player: the server thread's copy is a local sound too 2026-11-07 23:40:45 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 34f528ffcb Drop a destroyed minecart's item and contents with vanilla's motions
VehicleEntity.destroy spawns the cart item through spawnAtLocation, with an
ItemEntity's own motion (0.2 up, random sideways). A container cart's contents
go through Containers.dropContents: a spot in the cart's block, pieces of 10 to
30 with the level random's scatter and no pickup delay, every slot drawing its
spot, and chestVehicleDestroyed drops the emptied container again. Fixes #12099.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:40:28 +01:00
Kacpherek 031037cee5 Send stepped move paths only for living entities, as vanilla's SteppedInterpolationTracker does 2026-11-07 23:39:00 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 7f6f26a33a A shulker lid's push sets the pushed entity's on-ground flag the same way
ShulkerBoxBlockEntity.moveCollidedEntities pushes through Entity.move too,
so it ends in the same setOnGroundWithMovement as a piston's push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:30:46 +01:00
Kacpherek 70342ed68b Show your own crit particles on critical hits against other players on servers (Player.attackVisualEffects, LocalPlayer.crit) 2026-11-07 23:30:01 +01:00
Kacpherek 98d7553124 Unlock used_ender_eye advancements when throwing an eye of ender (UsedEnderEyeTrigger.trigger)
EnderEyeItem.use now fires the minecraft:used_ender_eye criterion with the squared horizontal distance to the located structure, matched against the distance bounds squared as MinMaxBounds.Doubles.matchesSqr does.
2026-11-07 23:29:23 +01:00
Kacpherek ef6fc399f5 List the blocks an item can break or be placed on in its tooltip (AdventureModePredicate.addToTooltip) 2026-11-07 23:29:04 +01:00
Kacpherek 94aeeac20d Flash the End sky on servers and pan its rumble toward the flash (ClientLevel.tick, DirectionalSoundInstance)
The End flash state now ticks on every client, so servers, LAN guests and single player show the sky flash and its light. Its weather.end_flash sound plays 30 ticks later ten blocks from the camera toward the flash, follows the camera, and is skipped under the credits.
2026-11-07 23:28:35 +01:00
Kacpherek 7f51885a85 Show mobs bouncing on slime blocks and beds to other players without cutting the bounce short (SteppedInterpolationTracker) 2026-11-07 23:28:17 +01:00
Kacpherek 5ecd155b76 Stop cracking blocks a can_break tool may not break in adventure mode on servers (Player.blockActionRestricted) 2026-11-07 23:27:33 +01:00
Kacpherek 5fe374444f Keep riders in the portal when they get off a vehicle standing in one (LivingEntity.dismountVehicle)
Getting off a boat, minecart or mount whose block is in #minecraft:portals now takes the free-position branch straight up from the rider, as for a removed vehicle, instead of the vehicle's side dismount spot.
2026-11-07 23:27:14 +01:00
Kacpherek c3f7e94775 Let foxes stroll into villages at night (Fox.FoxStrollThroughVillageGoal) 2026-11-07 23:26:35 +01:00
Kacpherek ff50de2553 Shear saddles off horses, striders, camels and nautili with a sound (Mob.attemptToShearEquipment)
Shears now take the saddle off any #can_equip_saddle mob, not only pigs, and play the saddle's shearing sound item.saddle.unequip unless the mob is silent. A mount wearing horse or nautilus armor is left alone, since vanilla shears that body armor first.
2026-11-07 23:26:20 +01:00
Kacpherek d4ea299fdd Let adventure-mode can_break and can_place_on predicates with nbt or components match their blocks (BlockPredicate.matches(BlockInWorld)) 2026-11-07 23:25:54 +01:00
Kacpherek 5383881562 Mark worlds with experimental features as Experimental in the world list (LevelSummary.createInfo) 2026-11-07 23:25:20 +01:00
Kacpherek 1c402c47a7 Anger piglins that see a player break a chest boat (ContainerEntity.chestVehicleDestroyed)
A survival player's hit that breaks a chest boat with entity_drops on angers the piglins that see it, as AbstractChestBoat.destroy does through chestVehicleDestroyed.
2026-11-07 23:25:04 +01:00
Kacpherek 6e5f7e88f7 Hold a lead at the right point while gliding, riptiding or swimming in third person (Player.getRopeHoldPosition)
The lead now hangs from the body's yaw, not the head's, follows the eased position, and uses vanilla's gliding/riptide and swimming hold points.
2026-11-07 23:24:48 +01:00
Kacpherek 75dd0dde7d Save and load a mob's hurt immunity as invulnerable_time (Entity.load / saveWithoutId)
A mob writes invulnerable_time while it has hurt immunity left and reads it back with a default of 0, so /data get shows it, /data merge sets it, and the immunity survives a save and reload.
2026-11-07 23:24:42 +01:00
Kacpherek 8c4e3bb507 Pour a water bottle onto dirt to make mud at once on servers instead of drinking it (PotionItem.useOn) 2026-11-07 23:24:38 +01:00
Kacpherek dabae908cd Keep hidden server list entries, so Direct Connection servers remember pack and Code of Conduct answers (ServerList.save, ServerList.add) 2026-11-07 23:24:26 +01:00
Kacpherek 211b53f598 Let a ravager's roar knock armor stands away when mob griefing is on (Ravager.roar) 2026-11-07 23:24:19 +01:00
Kacpherek 2d94afaa61 Keep counting time since last rest on the death screen (ServerPlayer.doTick)
time_since_rest now counts every tick the player is not sleeping, dead or alive, as ServerPlayer.doTick does; time_since_death still counts only while alive.
2026-11-07 23:23:39 +01:00
Kacpherek 14a5193b41 Use up a knowledge book and swing the arm at once on servers (KnowledgeBookItem.use) 2026-11-07 23:23:35 +01:00
Kacpherek 00d050bb8f Show each worn item's own camera overlay (Hud.extractCameraOverlays)
Items a server or datapack gives a camera_overlay now draw it in first person when worn in their equippable slot, and a carved pumpkin whose equippable was removed no longer blurs the screen.
2026-11-07 23:23:29 +01:00
Kacpherek 7225a84b10 Count thrown snowballs, eggs, pearls, potions and xp bottles as used (throwable Item.use)
Throwing a snowball, egg, ender pearl, splash or lingering potion or bottle o' enchanting awards the used statistic, as SnowballItem, EggItem, EnderpearlItem, ThrowablePotionItem and ExperienceBottleItem.use do.
2026-11-07 23:23:21 +01:00
Kacpherek 40afd73cd8 Draw wings and hide the cape by the chest stack's own equippable (WingsLayer.submit, CapeLayer.hasLayer)
A chestplate a server gives elytra wings now shows them and hides the cape, and an elytra whose equippable was changed or removed no longer shows elytra wings.
2026-11-07 23:23:03 +01:00
Kacpherek c54c8c0f42 Let a beached nautilus dry out instead of drowning, and spare NoAI ones (Nautilus.handleAirSupply) 2026-11-07 23:23:02 +01:00
Kacpherek 939d492535 Let sculk hear item frames break as a block change too (ItemFrame.dropItem)
A frame broken by a player, an explosion or losing its support posts block_change after its drops, as ItemFrame.dropItem(level, causedBy) does, on top of the entity_die it already posted.
2026-11-07 23:22:57 +01:00
Kacpherek 41934f3d93 Let sculk hear items knocked out of item frames (ItemFrame.hurtServer)
Punching the item out of a frame posts block_change with the attacker as source after the item drops.
2026-11-07 23:22:45 +01:00
Kacpherek 85a6a8b9dc Let sculk hear items put into or turned in item frames (ItemFrame.interact)
Putting an item in a frame or rotating it posts block_change with the player as source, after the item or rotation changes.
2026-11-07 23:22:36 +01:00
Kacpherek 4ca3d55e78 Draw worn armour from the stack's own equippable asset (HumanoidArmorLayer.renderArmorPiece)
Armour a server or a command gives a different or removed equippable now renders with that asset, or not at all, and only in its own slot, on players, mobs and armour stands. The head layer draws a helmet item when its equippable has no asset.
2026-11-07 23:22:27 +01:00
Kacpherek b97f0f61d7 Let sculk hear endermen pick up and put down blocks (EnderMan block goals)
EndermanTakeBlockGoal.tick posts block_destroy and EndermanLeaveBlockGoal.tick posts block_place, each with the enderman and the block state as context.
2026-11-07 23:22:05 +01:00
Kacpherek 0c40da1a82 Let an opening shulker lift and push you on servers too (Shulker.onPeekAmountChange on the client) 2026-11-07 23:22:04 +01:00
Kacpherek e01adc67e3 Let Thorns hurt a player who sweeps its wearer in PvP (Player.doSweepAttack's doPostAttackEffects) 2026-11-07 23:21:48 +01:00
Kacpherek 32137ae44f Answer pandas' clicks on servers as vanilla does: scared ones pass, ones on their back swing, adults leave bamboo to the off hand (Panda.mobInteract) 2026-11-07 23:21:39 +01:00
Kacpherek 9503a3cc7d Let sculk hear copper golems open and close chests (ContainerOpenersCounter)
The first golem to open a chest half posts container_open with the golem as source, the last to close it posts container_close (incrementOpeners / decrementOpeners), and the openers recheck posts a sourceless container_close when the golems have walked off (recheckOpeners).
2026-11-07 23:21:26 +01:00
Kacpherek 7c4a6d1ea6 Use the held item on a pig or strider someone already rides on servers (Pig/Strider.mobInteract) 2026-11-07 23:21:14 +01:00
Kacpherek 3c4d936707 Say "formerly known as" when a renamed player joins a server (PlayerList.placeNewPlayer) 2026-11-07 23:21:11 +01:00
Kacpherek 44f5a37e64 Open a wandering trader's trades on servers without swinging the arm (WanderingTrader.mobInteract) 2026-11-07 23:21:01 +01:00
Kacpherek 5ffd5c6515 Use the held item on a sleeping villager on servers instead of swallowing the click (Villager.mobInteract) 2026-11-07 23:20:45 +01:00
Kacpherek 8cd7616054 Let breezes whirl and idle on servers too (Breeze.tick and Breeze.playAmbientSound on the client) 2026-11-07 23:20:44 +01:00
Kacpherek ea99e6572c Let sculk hear glass bottles filling and count their uses (BottleItem.use)
Filling a bottle from water now plays the fill sound at the player and posts fluid_pickup at the water block; both the water and the dragon's breath branch award the used statistic (BottleItem.turnBottleIntoItem), and collecting dragon's breath fires player_interacted_with_entity on the cloud.
2026-11-07 23:20:32 +01:00
Kacpherek c5dbb620c2 Let sneak-clicks and clicks on occupied cushions reach the held item on servers, without an arm swing (Cushion.interact) 2026-11-07 23:20:24 +01:00
Kacpherek 642ac0d489 Play the warden's heartbeat on servers too (Warden.tick's client branch) 2026-11-07 23:19:51 +01:00
Kacpherek 92bedb26d0 Show team colours and prefixes in /list on LAN worlds (ListPlayersCommand.listPlayers uses Player.getDisplayName) 2026-11-07 23:19:40 +01:00
Nikita Maximilian Grimm 3d8924dcc8 Merge remote-tracking branch 'origin/main' into tnt-piston-ground 2026-11-07 23:15:42 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8ad82f9ac0 A piston push sets an item's or primed TNT's on-ground flag as vanilla does
Entity.move(MoverType.PISTON) ends in setOnGroundWithMovement on a server,
for every push: a sideways push clears the flag, and so does a free push
down. The ArmorStand-only repair in move_pushed becomes a trait method that
items, primed TNT and armor stands share.

Fixes #12029

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:15:41 +01:00
Web-mc1 77290ac3c1 telemetry: match the user name exactly when lowercasing moves boundaries
`replace_word` lowercased the text to find the user's name in any case, and
used that copy whenever it had the same byte length as the text. Equal
lengths don't imply equal boundaries: `İ` (U+0130) grows from 2 bytes to 3
and `K` (U+212A) shrinks from 3 to 1, so a text holding both keeps its
length while its offsets move. A match then sliced the original text
mid-character and panicked.

Use the lowercased copy only when the two share their character boundaries.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-08 11:12:29 +13:00
Nikita Maximilian Grimm daa43de927 Merge remote-tracking branch 'origin/main' into boat-drop-motion 2026-11-07 23:10:52 +01:00
Nikita Maximilian Grimm 3fb98b187f Merge remote-tracking branch 'origin/main' into boat-fire-timer 2026-11-07 23:10:51 +01:00
Ludovic Marie 77af961663 Merge pull request #11444 from voidsipp/fix/round5-2-11340
🤖 Agent: A helmet is damaged by falling-block damage before the reduction, as LivingEntity.hurtHelmet does in damages_helmet.
2026-11-07 23:09:40 +01:00
martipopsandClaude Opus 5.5 76db917ded Controller settings: a Nintendo Buttons row (By Position / By Letter)
The Device page gets a row beside Button Glyphs that toggles
PadSettings::nintendo_letters, so players no longer edit
horizon-controller.txt to make A confirm and jump on a Nintendo pad.
Its description says what A and B do in each mode. Always shown, like
the DualSense rows. docs/controllers.md points at the row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:09:01 -05:00
Florian ErnstandClaude Opus 5.5 39701a9334 Adopt /data edits on a live experience orb
EntityDataAccessor.setData keeps the entity's UUID around Entity.load of
the edited compound, and ExperienceOrb.readAdditionalSaveData reads its
Age, Health, Value and Count into the same orb. Horizon had no experience
orb case, so `data merge entity <orb> {Age:42s}` answered "Nothing
changed" and the orb kept its age.

The orb case decodes the edited compound with the existing orbs_from_nbt
and copies its saved fields into the resident orb, which keeps its
runtime id, join order, tick count and UUID. As Entity.load does, the
saved PortalCooldown is read while a live portal process stays. The
edited compound is kept like the other cases', so Tags follow it.

A falling block's Time is handled separately (#11424).

Fixes #12090.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:04:52 +01:00
VaspyyyandClaude Opus 5.5 1ba320ca73 Vaults: list the players only when a vault looks for them
with_vault built the list of every player (lan_present: eye heights, effects,
omens) for every loaded vault every tick. VaultBlockEntity.Server.tick reads the
players only when the vault's state may change (stateUpdatingResumesAt, once a
second), so the list is now built at the first ask, from the same game state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:01:12 +01:00
Horizon agent 2f60199b75 Merge remote-tracking branch 'origin/main' into fix/anticheat-effect-attributes 2026-11-07 22:49:45 +01:00
Horizon agentandClaude Opus 5.5 033b4a9e60 Test: the player's base speed is 0.1F
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:49:44 +01:00
Vinícius MoreiraandClaude Opus 5.5 721d4cfa44 Resource packs: a pack's core/sky and core/stars shaders
A resource pack's core/sky now draws the overworld's sky disc and its
core/stars the stars, as vanilla's SkyRenderer does with the SKY and
STARS pipelines: the disc of buildSkyDisc(16) with the sky colour in
ColorModulator and the sky's fog, the stars with their brightness, both
turned with the camera only. Without such a pack the sky draws natively.

Packs use this to tint the night sky or add a haze to the horizon. With
Visual Effects+'s sky shaders alone, Horizon's sky now reads within a few
units of the Java client's at noon, sunset and night.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:47:59 -03:00
martipopsandClaude Opus 5.5 78b21e4afb Graphics moves onto the settings shell
Part of #11487. The graphics screen now describes its pages and rows
(`GraphicsScreen`), and the shell draws them, takes the clicks, the wheel,
the stick, the keys and the pad's focus, as it does for Extras. What each
setting does, its saved value, its cost and its description stay in
graphics_ui as they were; Undo puts back the same settings.

What changes beyond what the shell gives every page: the HDR shortcut row
shows its key on a key button, as Extras' Sort key does, instead of between
arrows. The screen's own keyboard, scrolling and drawing code goes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:47:11 -05:00
martipopsandClaude Opus 5.5 26496db7c3 Merge upstream/main into controller/direct-craft
Main moved key_pressed to game/play_keys.rs and inventory_press to
game/inventory_input.rs; the craft's stop on a key or mouse press moves
with them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:46:48 -05:00
Ludovic Marie 6a0b9e435b Merge pull request #11549 from horizon-rust/pvp-2/item-view
🤖 Agent: item components read by reference instead of cloned per frame (frame prepare 217→193 µs); earlier parity failures were harness bugs since fixed on main; parity-main covers it after merge.
2026-11-07 22:46:44 +01:00
Ludovic Marie 485226a6fd Merge pull request #11545 from horizon-rust/pvp-2/gui-glint-lookup
🤖 Agent: HUD glint sprite looked up once per item instead of per quad (HUD 84→72 µs); render-only, packet harness 16/16; parity-main covers it after merge.
2026-11-07 22:46:41 +01:00
Web-mc1 37b79d1b63 java_properties: write a NUL and a non-BMP character as Java does
saveConvert pushed one UTF-16 unit at a time and marked a surrogate half with
a NUL sentinel, then ran fix_surrogates over the whole value, which split every
NUL in the result. A \u0000 that is part of a value (legal in a .properties
file) was therefore taken for a marker: "k=\u0000\uD83D\uDE00x" came back from
store with the NUL and both halves of the pair replaced, and since
server.properties is rewritten on every start the value could not survive a
round trip. Walk chars() instead: a Rust str's UTF-16 units always pair up, so
Java's per-char loop needs no marker, and a character past U+FFFF is still
escaped as the two units Java escapes it as.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
2026-11-08 10:44:43 +13:00
martipopsandClaude Opus 5.5 d5f931ca64 Controllers: LT + A on a recipe crafts it, held to keep crafting
LT + A on a crafting book recipe crafts it into the inventory: a tap makes
one, each further LT + A while one is in flight makes one more, and held past
the repeat delay it keeps going until a button comes up, the items run out
or the inventory is full. One request at a time, each waiting for the
server's answer; a result moves only with room for all of it as the client
last saw the inventory. A result that isn't the recipe's stops the craft with
nothing moved, and two seconds with no answer stops it with nothing more
sent. A refused LT + A (a carried stack, another recipe still finishing, not
allowed here) is plain A.

It's free in single player and on LAN worlds joined from the server list's
LAN entries; everywhere else, joins by address included, it waits for Quick
Craft on Servers. On any server but single player's own, placements keep
250 ms apart, under Paper's default limit on recipe placements (5 a second
over 4 seconds); on servers that need the option, every request also keeps
a tick apart.

On the crafting book LT changes the category as it comes up unless A was
pressed meanwhile; RT, and both triggers on the furnace books, change it as
they go down, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:47 -05:00
KyttuXDandClaude Haiku 5.5 702b0895bd Draw vanilla terrain flat on Metal at any section count
On Metal, vanilla terrain drew flat only past 1600 listed sections. Cave
culling can drop the listed count below that on a normal view, and the
per-section path then drops cutout leaves on Intel Metal: the canopy above
trunks vanished. Flat draws keep them, so Metal now always draws flat.

Co-Authored-By: Claude Haiku 5.5 <[email protected]>
2026-11-07 16:41:30 -05:00
martipopsandClaude Opus 5.5 a08827d6d5 Settings shell: Undo, a row's cost and why it is greyed
Part of #11487. What the graphics screen has that the shell couldn't say
yet, for any screen that wants it:

- Undo over Done once a row changed something, putting back what the
  screen says it changes (`Screen::undoes`, `Screen::undo`), from the
  settings as the screen found them. Tab and the arrows reach it.
- A row's cost, a square and the cost in its colour after its name in the
  strip.
- Why a greyed row can't be used, in the strip, muted, instead of what it
  does.
- `Shell::show` opens a screen at one of its pages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:37:34 -05:00
martipopsandClaude Opus 5.5 4465238c9c Settings shell: build rows once, Down from search to the list, reveal clipped rows
Part of #11487.

- The shell keeps the rows it built and builds them again only when what
  they read changes: the screen's stamp of its settings, a press or slide,
  the search, the page, the size or the language. On Extras, a pad session
  of 727 frames built them twice instead of 751 times.
- On a screen with no page list, Down from the search field goes to the
  first row instead of Done, for the shell's keys and, through a declared
  focus link, for the pad and the keyboard's focus.
- A row the list hides part of scrolls fully into view when the pad or the
  keyboard focuses or presses it (AbstractSelectionList.setSelected), so A
  presses it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:35:56 -05:00
Horizon agentandClaude Opus 5.5 e4ce68e271 Client: take effect attribute modifiers from the server only (#12040)
A server's client applied its effects' attribute modifiers itself, so it
slowed down as soon as ClientboundRemoveMobEffectPacket arrived. Vanilla's
server removes a Speed that runs out in ServerPlayer.doTick, after the
tick's sendDirtyEntityData, so the movement speed without the effect's
modifier reaches the client a tick later and vanilla's client runs fast for
that tick. GrimAC's Simulation flagged the Horizon client on that tick
(0.051 off with Speed II, decaying over the next ticks).

LivingEntity.onEffectAdded/onEffectUpdated/onEffectsRemoved change
attributes on a server only; a server's client now keeps the modifiers
ClientboundUpdateAttributesPacket sends.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:35:44 +01:00
Ludovic Marie 151aa6c82b Merge pull request #11159 from Ragbie/fix/tnt-fuse-any-number
🤖 Agent: Minecart numeric NBT reads any numeric tag through NumericTag int/short/byte/double values.
2026-11-07 22:35:05 +01:00
Nikita Maximilian Grimm 43943df1ef Merge remote-tracking branch 'origin/main' into boat-drop-motion 2026-11-07 22:29:00 +01:00
KBroneZandClaude Opus 5.5 8906102eb9 chore(launcher): add the found list's string before each table's last entry
Inserting "Found on this computer" before the last entry of each language
table, instead of after it, adds one line per file and leaves the last line
alone, so other pull requests that append strings don't conflict with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:23:19 +02:00
KBroneZandClaude Sonnet 5.5 0a51d6dc5c fix: shadow the TCP keepalive instead of mutating it, so Windows builds are lint-clean
`let mut keepalive` was only assigned under
`#[cfg(not(any(target_os = "windows", target_os = "openbsd")))]`, so on
Windows and OpenBSD clippy reported `unused_mut` and `-D warnings` failed.
Shadow the binding under the same cfg instead; behaviour is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:22:22 +02:00
KBroneZandClaude Opus 5.5 21ca2fdd1d refactor(launcher): drop the found list's caption; check slugs against import
The "Found on this computer" heading says enough on its own, and the caption
cost a string in every language table. The test now checks that each found
instance's slug is the one importing it gives, so the Open button keeps
pointing at the pack an import made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:15:51 +02:00
Nikita Maximilian Grimm 89a515a8d7 Merge remote-tracking branch 'origin/main' into boat-fire-timer 2026-11-07 22:14:37 +01:00
TheApple05andCodex GPT-6 b5406e647a Preserve legacy item normalization across upstream integration
Cover dropped components, pot decorations, instrument holders and nesting limits in the combined codecs; retain cross-platform keepalive settings without lint warnings.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:14:23 -08:00
KBroneZandClaude Sonnet 5.5 a0f9368edf feat: the launcher plays a profile that only has data packs
Imported.datapacks counts the packs in a profile's home/new-world-datapacks,
`can_play` and `apply_profile` accept a profile with only data packs, and
`outcome` and `modpack list` count them.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:13:52 +02:00
KBroneZandClaude Sonnet 5.5 1f86e14b9a feat: port data-only mod jars as data packs for a modpack's new worlds
A mod jar with no classes, a pack.mcmeta and a data/ folder is a data pack.
For a 26.2 or 26.3 modpack, `port` now upgrades such jars to 26.3 (the
upgrade-datapack code; 26.3 ones are copied) into the profile's
home/new-world-datapacks/ instead of carrying them as content mods, which
dropped their data/minecraft overrides. The game copies that folder into
each new world it creates.

Before its jar is read, a mod whose Modrinth project also ships as a data
pack counts as data tier, so `fetch` gets the jar; the jar then decides.
The coverage report lists such a jar as partial.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 23:13:51 +02:00
SenjinandClaude Opus 5.5 5ec521bb7b Shader packs: Horizon's dynamic lights as uniforms, horizonTier for terrain and entities
horizonDynamicLightCount and horizonDynamicLight0..15 give a pack Horizon
Extras' dynamic lights, nearest first (camera-relative position, light
level). horizonTier gives terrain, entity and hand programs a face's HDR
tier: 0 none, 1 to 4 low to very bright, plus 4 on terrain when the whole
face shines. Opt-in: a pack that declares none of them sees no change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:13:03 +01:00
Nikita Maximilian Grimm 80bbced3fc Merge remote-tracking branch 'origin/main' into fix/diode-moving-piston-support 2026-11-07 22:12:23 +01:00
TheApple05andCodex GPT-6 2d72b0b260 Merge remote-tracking branch 'upstream/main' into codex/viahorizon-upstream
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:12:23 -08:00
Nikita Maximilian GrimmandClaude Opus 5.5 c30b863128 Let water a boat drops into along its move put its fire out
Entity.collidedWithFluid sweeps the box along the whole move, so water reaches
the boat down to the lower of its start and end bottoms in every step.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:12:17 +01:00
TheApple05andCodex GPT-6 d79a36101f Merge upstream main and reconcile native protocol adapters
Preserve both older-server and older-client translation paths, consolidate protocol 766 tables, retain verified legacy codecs and resource-pack generations, and reject unsupported reverse protocols.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 13:12:16 -08:00
KBroneZandClaude Opus 5.5 690d434afa feat(launcher): list other launchers' modpacks on the Modpacks page
The Modpacks page now opens with "Found on this computer": the modpack
instances the CurseForge app, Prism Launcher, MultiMC, the Modrinth App,
ATLauncher and the official launcher keep in their usual folders, read with
`other_launchers::scan` on a thread. Each row shows the app, Minecraft
version and loader, and imports the instance with one click through the
same path as "Import an instance folder…" (a Prism instance from its own
folder, so its `mmc-pack.json` still names the version). A pack already
imported opens instead.

Only instances with a `mods` folder of their own are listed: the official
launcher's profiles that play in its own folder are the player's everyday
game, not a pack. The two new strings are in every language table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:10:54 +02:00
Ludovic Marie 35f3dad207 Merge pull request #11409 from ilguerro/fix/w151
🤖 Agent: Setting a lead holder clears the saved, still unresolved holder, as LeashData.setLeashHolder does.
2026-11-07 22:09:23 +01:00
Florian ErnstandClaude Opus 5.5 c0806b9ee3 Keep a failed wildcard set's edit in command storage
StorageDataAccessor.getData returns the stored compound itself, and
DataCommands.manipulateData edits it in place. A wildcard set on a
non-empty byte, int or long array clears the array before it rejects a
string, so the command fails with nothing counted and the storage keeps
the cleared array. Horizon edited a copy and dropped it on the failure.

On that failure, a /data modify on storage now puts the edited compound
back with CommandStorage::set_unsaved: only an id that is stored is
replaced, and the storage is not marked for saving, as vanilla calls no
setData. The error, the result, the success path and the entity and
block copies are unchanged.

Fixes #12057.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:08:42 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 f57811dd0d Drop a destroyed boat's item with an item entity's own motion
VehicleEntity.destroy spawns the boat item through spawnAtLocation, so it
leaves with ItemEntity's default motion (0.2 up, random sideways), not
straight up at 0.1. Chest contents keep their own scatter. Fixes #12026.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:05:45 +01:00
KBroneZandClaude Opus 5.5 70bc17720c fix(launcher): keep a Prism root's instances folder from reading as CurseForge's
Where folder names ignore case (Windows, macOS), a Prism or MultiMC root's
`instances` folder is also `Instances`, so `in_folder` took it for the
CurseForge app's modding folder and found nothing. It now reads that folder
as CurseForge's only when none of its instances has an `mmc-pack.json`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 23:03:37 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 73e054f827 Apply a boat's fire block effects in the order its move meets the blocks
Entity.checkInsideBlocks meets the blocks of the box a move starts in before
the ones it reaches, each once, and each step lights before it puts out: a
boat that leaves water for fire in one move ends up burning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:01:24 +01:00
Ludovic Marie 5de5b6fd91 Merge pull request #12046 from horizon-rust/mods/stamp-chunk-tickets
🤖 Agent: stamps the existing chunk_tickets fragments (API 412/413, NATIVE_HOOKS_API 413); tests.test_stamp_native_hooks 10 OK, verify green, and this exact head built into the SMP engine on playhorizon (rtp2), whose /rtp proof passes with no slow tick. The SMP 0.35.1 PR in horizon-mods needs it.
2026-11-07 22:01:20 +01:00
Mik00000 e1434f2ee4 perf(game): reduce dimension swap overhead and cache raycast chunk lookups 2026-11-07 22:58:20 +02:00
bulkandClaude Opus 5.5 25ea320338 Streaming: rank decoration by rank alone
Decoration scheduling measured, sorted and ranked every proto chunk each update.
Ranks are unique, so only chunks without one need their distance; the rest
pop from a heap until the workers are busy. A server with 200 players spread
over the map held ~57,000 protos and spent a fifth of its tick here.

The heap pops in the order the full sort gave; unranked chunks at the same
distance now go by position instead of where the unstable sort left them.

(cherry picked from commit d20e7bf74e)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:57:34 +00:00
KBroneZandClaude Opus 5.5 6fa7f8896a feat(launcher): find instances in the CurseForge app's Instances folder
`instance others --folder` and `copy-from --folder` now accept a folder whose
children are CurseForge instances, the `Instances` folder the app shows the
player, as well as the modding folder above it. Before, that folder found
nothing.

When nothing is found, the message says where it looked (the folder named, or
every default folder the scan reads) and which folders it expects. The
CurseForge folder is joined one component at a time, so its path no longer
shows mixed separators on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 22:57:04 +02:00
Florian ERNSTandCodex 67a39ecaac Project ground after unoccupied server NoAI Mob piston movement
Co-Authored-By: Codex <[email protected]>
2026-11-07 21:56:55 +01:00
KBroneZandClaude Sonnet 5.5 199d9d74f8 fix: run tools/neoforge_oracle.py on Windows
Windows users cannot create symlinks without a privilege (WinError 1314), and NeoForge's
unix_args.txt gives "Illegal char <:>" there. Link the server's libraries folder with a
junction and the mod jars with a hard link, copying when that fails; use win_args.txt on
Windows. Linux and macOS keep the symlinks and unix_args.txt. Adds tests/test_neoforge_oracle.py.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 22:55:49 +02:00
Horizon agent ca17c0c5ba Merge remote-tracking branch 'origin/main' into vanilla-split/extras-feature
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 21:55:27 +01:00
Ludovic Marie 23c3faedc1 Merge pull request #11947 from horizon-rust/docs/license-history-notices
🤖 Agent: notices list the three cases (Iris, OpenJDK math, OpenJDK HashMap); both clean-room rewrites (#11944, #12043) are merged, so the notice now describes main accurately.
2026-11-07 21:51:19 +01:00
TheMrZZandCodex 1005076a59 Route recipe-book placement through actual furnace menus
Reuse the existing machine menu cache, shared placement and authoritative
slot writeback for ordinary PlaceRecipe packets. Preserve inventory/table
placement and the separate Horizon recipe-transfer route.

Fixes #12009

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
2026-11-07 21:49:12 +01:00
bulkandClaude Opus 5.5 94ed2f4cc8 Beehives: ask once per hive turn whether bees stay in
bees_stay_in_hive built the mob world (every mob and player) for each occupant
whose time was up. The night and rain it reads hold for the hive's turn.

(cherry picked from commit 3430283af0)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:48:44 +00:00
bulkandClaude Opus 5.5 517f54d076 Streaming: know the extra cover is current instead of comparing it
Every in-range check compared the cover's copy of the extra list (about 200
entries on a spread server) with the live one, for each waiting chunk each
tick. The cover is refreshed at the start of update; update now says so and
the checks trust it, with a debug assertion that they still match.

(cherry picked from commit f61e6ed98d)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:48:44 +00:00
bulkandClaude Opus 5.5 9626c7446e Creaking hearts: build the mob world only for the heart's update
A heart built the mob world (every player, the teams, the weather) each tick,
but reads it only in the update about once a second. server_tick is split at
that point; the game builds the world for the update only. The heart's
creaking, asked for a few times a tick, is found through the mob index
instead of a scan.

(cherry picked from commit a1cf6491dd, with the mob index
in place of its own slot map)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:48:44 +00:00
bulkandClaude Opus 5.5 da2b437f49 Spawners: count the delay down without building a level
BaseSpawner.serverTick only decrements a running delay. The level (every
player, the mob world) was built for each spawner near a player every tick
first.

(cherry picked from commit 1f06b720c9)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:48:44 +00:00
bulkandClaude Opus 5.5 6e0f482e83 Structure starts: keep four times as many chunks cached
Players spread far apart read more start chunks than the cache held, so
outposts' and trial chambers' jigsaws were built again on the server thread
whenever natural spawning asked about them.

(cherry picked from commit da2c8d2d48)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:48:44 +00:00
Nikita Maximilian Grimm 8189734106 Merge remote-tracking branch 'origin/main' into fix/diode-moving-piston-support 2026-11-07 21:46:30 +01:00
KBroneZandClaude Sonnet 5.5 1f29c88a49 feat: copy a game folder's new-world-datapacks into every new world
A game folder can hold a `new-world-datapacks` folder. Create New World now
seeds its temporary data pack folder from it and selects those packs, and a
world opened without the Create screen (`--world`, the control socket) gets
a copy when it has no level.dat yet. Each world keeps its own copy. Without
the folder nothing changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 22:46:25 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 bf0f0dd811 Wear off a driven boat's damage before it burns, and let water it left put it out
AbstractBoat.tick decays the damage before Entity.baseTick burns the boat, also
for a boat its rider's client moves, and the block effects follow the whole
move, so water the boat started in still puts it out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:44:44 +01:00
Ludovic Marie ba3c3feebf Merge pull request #11951 from horizon-rust/split/resource-tables
🤖 Agent: regenerated from main after #11871/#11940/#11961 landed; still a pure move of the three impl blocks (199 lines), structs stay in game.rs.
2026-11-07 21:29:31 +01:00
Ludovic Marie ec298576f6 Merge pull request #12043 from horizon-rust/fix/java-hash-map-clean-room
🤖 Agent: clean-room java_hash_map (CLRS tree, slab); java_hash_map and java_identity_map oracles identical over 1.18M lines plus 3,028 edge fixtures; 11/12 caller harnesses pass (structure_layout lacks oracle data on the desktop).
2026-11-07 21:29:28 +01:00
Ludovic Marie 97c9d1a14f Merge pull request #11944 from horizon-rust/fix/jmath-fdlibm-provenance
🤖 Agent: clean-room jmath; JVM-identical on ~2.5M inputs per function (log/log10/cbrt), 5.9M hypot pairs, 3.05M sin/cos, all 2^32 floats for round_f32; parity-main covers the ~420 dependent harnesses after merge.
2026-11-07 21:29:24 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 007458a627 A repeater or comparator on a retracting piston stays, as in vanilla
DiodeBlock.canSurviveOn asks isFaceSturdy(level, pos, UP, RIGID) of the
block below, in canSurvive and in updateShape with the neighbour's new
state. A retracting piston base is a moving_piston whose block entity
shape holds the extended base, so its top face is rigid. The diode checks
read the raw state's exported facts instead, saw no support, and broke the
repeater or comparator. Both paths now go through the contextual rigid
support, which takes the state a shape update hands over.

The redstone catalog harness gains fixed lever/piston/diode contraptions
for both piston and both diode kinds (#12019).

Fixes #12019

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:29:19 +01:00
Nikita Maximilian Grimm 9c30028167 Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 21:28:43 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0d49b282c0 Read a block predicate's NBT with the argument's own SNBT grammar
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:28:40 +01:00
Florian ErnstandClaude Opus 5.5 099fb70e6a Pick up a sulfur cube with an empty bucket
SulfurCube.mobInteract ends with Bucketable.bucketMobPickup, and a sulfur
cube is picked up with an empty bucket (canBePickedUpWithBucket), not a
water bucket. Horizon's bucket pickup only knew the water bucket's mobs,
so a click with a bucket left the cube where it was.

After the cube's own baby, primed, shears and swallow branches, an empty
bucket now plays the sulfur fill sound, fills a sulfur_cube_bucket, fires
filled_bucket and player_interacted_with_entity, drops the leash, discards
the cube and gives the bucket through ItemUtils.createFilledResult, as the
other bucketable mobs do. The native bucket hook hears it.

The filled bucket carries SulfurCube.saveToBucketTag: the cube's age and
age_locked in bucket_entity_data, and its body block as
sulfur_cube_content when it has one. Other species keep their bucket data.

The command harness gets a mob_interact op: the connected player's real
Interact packet on a mob, on both sides.

Fixes #12044.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:26:20 +01:00
Florian ErnstandClaude Opus 5.5 38a1babc5a Use the archetype data's float for the knockback modifier amount
The archetype table keeps knockback resistance as a decimal double for
the physics code. The data's modifier amount is the float widened
(0.4000000059604645 for slow_bouncy), so /attribute read 0.4 and 0.7
where vanilla reads 0.4000000059604645 and 0.699999988079071.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:26:16 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 c7aef39dd5 Burn boats: live fire timer, fire damage and both block-effect passes
A boat now keeps Entity.remainingFireTicks: Entity.baseTick burns it down a
tick at a time and hurts it every 20 ticks, and the two applyEffectsFromBlocks
of AbstractBoat.tick light it on fire blocks, hurt it once per fire block,
let water, powder snow and rain put it out and clear the cooldown. Saves,
/data, portals and the root vehicle write the live Fire; loads and /data
merge read it. Fixes #12025.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:24:20 +01:00
TheMrZZandCodex gpt-6.1-sol d7c3fefcfa Preserve Nautilus cooldown memory presence and lifetime
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 21:23:19 +01:00
Horizon agentandClaude Opus 5.5 9099175f8a Move the shared tables impl blocks to game/resource_tables.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:22:09 +00:00
Ludovic Marie e1e39361db Merge pull request #11961 from horizon-rust/split/loading-screen
🤖 Agent: pure move of 512 lines (Loading, App, run_event_loop, the start checks) to game/loading_screen.rs; the Windows-only hwnd stays in game.rs and stays visible to the moved code.
2026-11-07 21:20:15 +01:00
Ludovic Marie ef19179033 Merge pull request #11940 from horizon-rust/split/inventory-input
🤖 Agent: pure move of 506 lines (keys and clicks over a container screen) to game/inventory_input.rs; click_inventory stays because an open draft changes it.
2026-11-07 21:20:12 +01:00
Ludovic Marie b189c72fcc Merge pull request #11871 from horizon-rust/split/resource-load
🤖 Agent: pure move of the startup resource load (614 lines) to game/resource_load.rs; load_world_rest stays because #11574 changes it.
2026-11-07 21:20:08 +01:00
Florian ERNSTandCodex 8b48b0d74e test: checkpoint H255 piston ground regression before claim-cap decision
Qualified old-source failure only; no production repair or fixed-pass claim.

Co-Authored-By: Codex <[email protected]>
2026-11-07 21:19:53 +01:00
Horizon agentandClaude Opus 5.5 3a229da6b3 mod API: stamp chunk_tickets (412) and chunk_tickets_in_level (413)
python3 tools/stamp_native_hooks.py --only chunk_tickets chunk_tickets_in_level.
The SMP's /rtp moves from /forceload (which waits on the server thread
until its chunk is generated) to native_force_chunk_in, so it needs a
number to declare.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:19:46 +01:00
Florian ErnstandClaude Opus 5.5 3e5e8c4841 Give a sulfur cube its body archetype attribute modifiers
SulfurCube.collectEquipmentChanges runs in LivingEntity.tick
(detectEquipmentUpdates) when the body item changed. It removes the
previous body's archetype modifiers and adds the new body's as transient
modifiers: knockback_resistance, explosion_knockback_resistance and
bounciness (add_value), friction_modifier and air_drag_modifier
(add_multiplied_total). /attribute read the defaults for these five on a
cube with a body.

A sulfur cube now has its own attribute map from its type's defaults.
The five attributes go to that map, and the tick adopts the body's
modifiers there with the existing AttributeInstance operations. A
transient modifier shadows a permanent one with the same id, and the
removal takes both, as in vanilla. The archetype table also gets its
explosion knockback resistance, from the 26.3 data.

The save writes the base and the permanent modifiers of these instances,
not the transient ones. A command change of the knockback base or of a
permanent modifier also goes to the store that the bodiless physics
reads, so that effect stays as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:18:07 +01:00
Horizon agentandClaude Opus 5.5 e22ccc17d2 Notices: add java_hash_map.rs, replaced in #12043
The same check for #6741 found that java_hash_map.rs was translated from
OpenJDK's HashMap.java. List it with the other two, and say so in
how-horizon-is-built.md.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:17:46 +01:00
Nikita Maximilian Grimm 5664a950e8 Merge remote-tracking branch 'origin/main' into execute-block-nbt 2026-11-07 21:17:10 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 538422f3c7 Match the NBT part of a block predicate against the block entity
execute if|unless block, fill ... replace and clone ... filtered tested only
the block state; BlockPredicateArgument also compares the {...} part with the
block entity's full saved data (NbtUtils.compareNbt), and a block without a
block entity fails it.

Fixes #12030

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:17:09 +01:00
Ludovic Marie 493dd75acf Merge pull request #12007 from horizon-rust/split/play-keys
🤖 Agent: pure move of key_pressed (290 lines) to game/play_keys.rs; AGENTS.md's Input row now names play_keys.rs for it.
2026-11-07 21:13:55 +01:00
Horizon agentandClaude Opus 5.5 f4481233b0 Move the loading screen and the pre-game App out of game.rs into game/loading_screen.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:13:07 +00:00
Ludovic Marie 63cbec701e Merge pull request #12036 from horizon-rust/split/cauldrons
🤖 Agent: pure move of cauldron_outcome and use_on_cauldron (96 lines) to game/cauldrons.rs; cauldron_at and cauldron_extinguish stay because an open draft changes them.
2026-11-07 21:06:30 +01:00
Ludovic Marie a597387a66 Merge pull request #12035 from horizon-rust/split/key-codes
🤖 Agent: pure move of 117 lines (GLFW key/button codes, follow_named_keymap, the key routing rules) to game/key_codes.rs; game.rs re-imports the names other game modules reach through super::.
2026-11-07 21:06:26 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 80e3532c4a docs: HDR design note: step 4 writes light a whole frame at a time
A blended draw over a surface of the other kind leaves one mark for both, so a frame that mixes
marked and written light would clip the written light and lift it again, or leave the marked part
unlifted. Step 4 now goes path by path: the frame-case rebuild first, then every marking shader on
the default path in one pull request, then Improved Transparency and the visibility buffer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:06:12 +01:00
Florian ErnstandClaude Opus 5.5 7339c9057a Run a /kill-ed mob's death during the command
LivingEntity.kill is a generic_kill hit that calls LivingEntity.die before
the command returns. Horizon set the mob's health to 0 and left the death
to the next tick's creature phase, so its entity_die vibration reached a
sculk sensor one tick late.

The /kill mob arm now runs the existing death routine (mob_died) at once
for a mob that this kill takes from alive to dead, behind the existing
`died` guard, as a frog's bite already does. The native living_death veto,
the vibration, the catalyst, loot, experience and kill credit now happen
during /kill instead of in the next creature phase. A second kill and the
next tick do not run it again. The body's removal timer is unchanged.

Fixes #12037.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 21:04:50 +01:00
Horizon agentandClaude Opus 5.5 2d306dc52c Move container screen key and mouse input out of game.rs into game/inventory_input.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:01:15 +00:00
Florian ErnstandClaude Opus 5.5 b528247824 Merge origin/main into fix/h275-test-defs-world-packs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:59:27 +01:00
Florian ErnstandClaude Opus 5.5 ed1ce7495e Merge origin/main into fix/chorus-projectile-drops
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:59:09 +01:00
Horizon agentandClaude Opus 5.5 4f6be12c2e Move the in-world key press handler out of game.rs into game/play_keys.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:54:16 +00:00
Ludovic Marie 8c93f81143 Merge pull request #12015 from horizon-rust/split/core-tests
🤖 Agent: moves 575 lines of game.rs's inline tests to game/core_tests.rs with unchanged bodies; the horizon-client test count stays 1159, and the tests open PRs touch stay put.
2026-11-07 20:51:01 +01:00
Ludovic Marie a20e9ce2d0 Merge pull request #12013 from horizon-rust/split/player-spawn
🤖 Agent: pure move of spawn_player (117 lines) to game/player_spawn.rs; test_world_input_capture's loading-completion check reads it from the new file and still sees the player set before the mouse capture.
2026-11-07 20:50:57 +01:00
Horizon agentandClaude Opus 5.5 aba1a90e6f java_hash_map: clean-room rewrite (#6741)
The previous implementation was translated from OpenJDK's
java/util/HashMap.java (GPL-2.0), which Horizon's MIT licence cannot
carry. This one was written in a two-person clean room: the implementer
worked from a behaviour spec, the Java SE API documentation, CLRS
chapter 13 (red-black trees) and runs on a real JVM, and never read
OpenJDK's source or the old file.

Entries live in a slab of nodes addressed by u32 index. A bucket holds
the first node of its bin's list and, for a tree bin, the root of a
red-black tree over the same nodes, whose links sit in a side table the
map allocates with its first tree bin. The public API is unchanged.

Unit tests pin iteration orders printed by java.util.HashMap on OpenJDK
25: a doubling from 16 to 32 buckets, a tree bin made at 64 buckets
after two doublings below 64, a tree-bin insertion after its tree
parent, movable and cursor removals from a tree bin, copies, and the
cursor's error order.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:50:56 +01:00
Ludovic Marie 73a3c0348d Merge pull request #11995 from horizon-rust/split/item-actions
🤖 Agent: pure move of 190 lines (startUseItem, interactOn, the placement/bucket cancel hooks) to game/item_actions.rs; the Player actions docs rows now point fn use_item and fn attack at their files.
2026-11-07 20:50:54 +01:00
MSedgeMC d1f8cccb7c A mob stops chasing a target outside its home
`MeleeAttackGoal.canContinueToUse`'s follow branch returns
`isWithinHome(target.blockPosition()) && !(target is a spectator or
creative player)`. Horizon's `MeleeAttack::can_continue` only refused a
creative player: a mob with a home kept chasing past its radius, and a
player who became a spectator was still chased.
2026-11-07 19:46:00 +00:00
MSedgeMC e8da3a8e21 A drowned stops the trident attack when it loses the trident
`DrownedTridentAttackGoal` overrides `canUse` with the trident test and
inherits `RangedAttackGoal.canContinueToUse`, which calls `canUse()` again
(and falls back to a target it is still walking towards). Horizon's
`DrownedTridentAttack::can_continue` asked only the base ranged goal, so a
drowned whose trident was gone kept the trident attack running once its
path was finished.
2026-11-07 19:42:47 +00:00
Horizon agentandClaude Opus 5.5 c95f24bc32 Move using an item on a cauldron out of game.rs into game/cauldrons.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:41:54 +00:00
Horizon agentandClaude Opus 5.5 ca0b945857 Move the GLFW key and button code helpers out of game.rs into game/key_codes.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:41:00 +00:00
Mateusz KorniakandClaude Opus 5.5 3004b64927 Fix speckled players with Improved Transparency on Metal
fs_oit_cull writes the nearest opaque see-through depth into the depth
buffer, and the later OIT stages test GreaterEqual against it, so an opaque
fragment is compared with its own depth. On Metal, position.z can differ from
the rasterizer's depth by an ulp, which dropped random pixels of players and
mannequins (horizontal dashes, text behind showing through). Write the depth
4 ulps farther: the fragment still passes, anything visibly behind is culled.

Checked on macOS (Metal, Apple M1) with a mannequin and a text display; not
yet on Linux (Vulkan).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:38:40 +01:00
Ludovic Marie 33be2a611a Merge pull request #12004 from horizon-rust/split/autosave
🤖 Agent: pure move of 90 lines (autosave interval, tick-rate change, player autosaves, level.dat metadata) to game/autosave.rs; save_game_with and save_level stay because open PRs change them.
2026-11-07 20:37:30 +01:00
Ludovic Marie 1199c117f2 Merge pull request #12003 from horizon-rust/split/render-settings
🤖 Agent: pure move of 109 lines (renderer_for, apply_settings, translucent layers, far terrain distances) to game/render_settings.rs; prepare_lod and all_sections_changed stay because an open draft changes them.
2026-11-07 20:37:27 +01:00
Ludovic Marie 2e749948f4 Merge pull request #11979 from horizon-rust/split/world-session
🤖 Agent: pure move of 234 lines (title actions, open_world, load_world, save_for_exit, close_world) to game/world_session.rs; finish_open_world and finish_close_world stay because an open draft changes them.
2026-11-07 20:37:24 +01:00
Horizon agent f5f96b1be4 Merge remote-tracking branch 'origin/main' into split/core-tests 2026-11-07 19:36:59 +00:00
Florian ERNSTandCodex 60a2c3f164 Preserve loaded NoAI Enderman absolute anger timestamps
Co-Authored-By: Codex <[email protected]>
2026-11-07 20:36:12 +01:00
VaspyyyandClaude Opus 5.5 abdf311e3b Native mod API: muffle the game's sounds for a while (native_muffle_sounds)
After a close blast a mod can now dull and quieten the world's sounds while
its own ringing plays clearly: native_muffle_sounds(amount, lowpass, ticks,
exempt) turns every sound down by `amount` and low-passes it by `lowpass`
(down to 300 Hz), both fading back over `ticks` client ticks and held while
the game is paused. The sound events in `exempt` play through untouched,
those already playing too.

The mixer mixes the muffled sounds into a bus of their own, runs two
one-pole low-pass stages and the duck over it, and adds it to the exempt
ones. The level glides over 20 ms so tick-sized steps don't click. While no
muffle is set or once it has faded, sounds mix straight into the output as
before. The sound engine marks a channel exempt as it starts, and marks the
playing ones again when the list changes.

Helper-only fragment muffle_sounds. Fixes #12006

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:34:54 +01:00
Florian ErnstandClaude Opus 5.5 2979cf024d Resolve /clone's source and target dimensions
/clone from <sourceDimension> ... to <targetDimension> ignored both
arguments: an unknown name cloned in the current level, and a valid
other dimension wrote into the current level instead of its own world.

As CloneCommands does, sourceDimension is resolved as begin is read and
targetDimension as destination is read, each position checked loaded in
its own level; an unknown name fails with argument.dimension.invalid
(the built-in dimensions, as execute in checks them), the source first.
The overlap check applies within one level only. The blocks are read in
the source level and placed in the target level through the existing
level swap (enter_command_level), each entry checked; the coordinates
are resolved in the command's own context first. The command's own
level is put back on every path, before the command reports.

Fixes #11997

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:34:36 +01:00
MSedgeMC f96e0fba0f A tamed cat stops being tempted
`CatTemptGoal` overrides `canUse` with `!cat.isTame()` but does not
override `canContinueToUse`; `TemptGoal.canContinueToUse` ends with
`return this.canUse()`, so taming a cat mid-tempt ends the goal. Horizon's
`CatTempt::can_continue` ran only the base `Tempt` continue, which never
asked whether the cat was tamed, so a cat tamed while following a fish
kept following it.
2026-11-07 19:32:24 +00:00
Horizon agentandClaude Opus 5.5 522634ca7f Move placing the local player in a world out of game.rs into game/player_spawn.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:31:48 +00:00
Horizon agentandClaude Opus 5.5 f7543ccffa Move using an item at nothing or at an entity out of game.rs into game/item_actions.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:30:54 +00:00
Horizon agentandClaude Opus 5.5 1fd2f02081 Move most of game.rs's inline tests into game/core_tests.rs
Pure move (#11857): the tests no open PR changes go to their own test module; the ones open PRs touch (the module header, the scaffolding fall test, the integrated save test) and the tests next to them stay. Test bodies are unchanged; rustfmt re-indented them one level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:30:29 +00:00
VaspyyyandClaude Opus 5.5 653a4a99fe Native mods: a game horizon-mixins built has the mod image slot
The HUD atlas reserved `horizon/modimage` (native_mod_image, API 355) only
when the launcher's native-mods.json listed mods, so a bundle applied with
horizon-mixins by hand or by a mod's own script built a game whose
native_mod_image always returned false.

horizon-mixins leaves .horizon-mixins-stage.json at the root of every
workspace it changes, naming each injection's and module's mod. The build
script turns a record that names mods into HORIZON_MIXIN_MODS, and
built_with_native_mods counts it and installed trait mods as well as
native-mods.json. A release or Nightly has none of them and keeps no slot.

Fixes #12005

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:26:53 +01:00
Ludovic Marie 8d8c250f48 Merge pull request #11996 from horizon-rust/split/block-particles
🤖 Agent: pure move of 110 lines (block face and particle sprites, honey particles, destroy particles) to game/block_particles.rs; particle_sprite and crack_particle stay because open PRs change them.
2026-11-07 20:24:09 +01:00
Mik00000 65c35da0dc mobs: reuse pathfinding allocation buffers across queries 2026-11-07 21:21:07 +02:00
Ludovic Marie d21bc19b84 Merge pull request #11993 from horizon-rust/split/melee
🤖 Agent: pure move of 186 lines (attack strength, crits, sweeps, Player.attack) to game/melee.rs; melee_damage and attack_mob stay because open PRs change them.
2026-11-07 20:18:27 +01:00
Ludovic Marie 3afc51e740 Merge pull request #11972 from horizon-rust/split/window-handler
🤖 Agent: pure move of the game window's ApplicationHandler (295 lines) to game/window_handler.rs; the input tests read window_event/device_event from the new file first, since Loading and App have handlers with the same names.
2026-11-07 20:18:23 +01:00
Ludovic Marie 17778b9722 Merge pull request #11957 from horizon-rust/split/inventory-slots
🤖 Agent: pure move of 359 lines (slot layout, hit test, item and decoration drawing) to game/inventory_slots.rs; slots/slot_count/draw_inventory stay because open PRs change them.
2026-11-07 20:18:18 +01:00
Horizon agentandClaude Opus 5.5 abaa2a9f68 Put Horizon's extras behind a horizon-extras feature for forks
One Cargo feature, on by default, holds the entry points of Horizon's own
extras so a fork can build the game without them
(`--no-default-features --features audio,controllers`) or delete them:
cosmetics, emotes and the "Horizon player" presence (one gate,
game::cosmetics::enabled), the official and partner servers added to
servers.dat, the Options screen's Horizon Settings and Mods buttons, and
the existing discord, friends and heartbeat features it now turns on.

The default build resolves the same features as before and every gate is
cfg!(), so both sides compile in every build. A build without the extras
names the official build on Credits & Attribution. docs/forking.md lists
the extras, their files and their gates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:18:11 +01:00
Horizon agentandClaude Opus 5.5 d1c1684fb5 Move opening and closing a local world out of game.rs into game/world_session.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:17:05 +00:00
Florian ErnstandClaude Opus 5.5 46c6bdc8ac Let /attribute add and remove see a baby zombie's minecraft:baby modifier
Zombie and Piglin.setBaby put a transient minecraft:baby speed modifier
on a baby. AttributeInstance refuses to add an id it already holds and
removes a modifier whatever its kind; the next setBaby removes
minecraft:baby again and, for a baby, adds the transient one back. The
transient modifier was computed from the baby state, so /attribute
modifier add stacked a permanent one on it and modifier remove found
nothing to remove.

The add now counts the transient modifier, and the remove takes it away
until the next setBaby (a flag on the mob's attributes, not saved: a
load runs setBaby again). setBaby clears that flag and removes a
permanent minecraft:baby, as vanilla's does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:15:12 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4fa237a12e HDR layered frame: a tone curve eases the light into the peak
After the post effects and before the camera overlays, ITU-R BT.2390's EETF in PQ rolls the
light off into the peak, on its brightest channel so it keeps its hue. It takes over the HDR
lighting's easing and the light sources' cap at the peak, which the resolve pass no longer
applies. The knee stays where the lighting's was, 60% of the peak.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:14:01 +01:00
MrCloudy2andClaude Opus 5.5 772088790c lan_e2e: sprint-hit-slowdown builds its sprint with a run-up
An absolute /tp zeroes the player's speed, as vanilla's does
(TeleportCommand's relatives carry no DELTA), so the check's teleport
right before the hit left the guest at walking speed and the check
failed on main. The guest now runs 19 ticks from 8 blocks out and
clicks in reach. The slowdown assertion is tightened: the first
post-hit sample must be under 0.75 of the sprint speed and then climb,
which fails with the client's 0.6 cut removed (the old 0.9 bound did not).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 20:14:01 +01:00
Ludovic Marie fdc87aecdb Merge pull request #11978 from horizon-rust/split/using-item
🤖 Agent: pure move of 237 lines (an item in use, tick by tick) to game/using_item.rs; apply_consume_effect and drop_bundle_content stay in game.rs because an open draft changes them.
2026-11-07 20:03:38 +01:00
masa-masa-0314andZCode aaa674f54b Realms: the screens follow vanilla's layout, state and input
The parity work after the per-screen PRs (#9714, #9836, #9909, #9926,
#9968, #9982, #10032, #10391, #10448, #11640), as verified in the local
playtest builds:

- Main list: RealmsMainScreen's header and footer geometry, row content
  and status light, scrolling, notices, selection narration, and the
  60 s/10 s/300 s data-fetcher periods.
- Configuration screen: the four tabs (MenuTabBar), GridLayout columns,
  slot options, region preference, invite codes and their edit screen,
  the players tab (invites, operators, removal), the subscription tab
  and the tab keyboard.
- World screens: RealmsResetWorldScreen's tiles, the template list, the
  upload selection and upload screen, and the PDP rows and keyboard.
- Backups: RealmsBackupScreen/RealmsBackupInfoScreen on the current wire
  (lastModifiedDate, metadata as strings, a local changeList), restore
  as RestoreTask (retries, abort), and the complete Download Latest flow:
  DownloadTask, RealmsDownloadLatestWorldScreen's size check, bar, speed
  and cancel, and FileDownload's tar.gz unpack (name cleanup, world/
  entries only, level.dat renamed, resource pack SHA-1 checked).
- Settings: RealmsSettingsTab's GridLayout/ScrollableLayout, the
  distance sliders, the name validation, and tooltips drawn after every
  widget.
- Service: the ops/invites endpoints, the invite answer's players, the
  backup age on the wall clock, null gameMode, and news persistence.
- Integrated server: the owner and the memory connection's login use the
  account profile (IntegratedServer.setSingleplayerProfile,
  ServerboundHelloPacket), so a world without singleplayer_uuid (a Realms
  download) loads the owner's players/data file; saveAllChunks'
  saveDataTag(profile.id()) is mirrored by save_owner.

Tests: 2166 lib tests pass (65 realms), scoped clippy clean.

Co-Authored-By: ZCode <[email protected]>
2026-11-08 03:00:28 +08:00
Florian ErnstandClaude Opus 5.5 ad475b0573 Load a world's test instances from its data packs when it opens
/test only knew the test instances of the game's own data: read_test_def
read options.resources, and the selector ids were the exported built-in
ones, so a test a world's data pack defined was "No matches for
selector". Vanilla 26.3 loads Registries.TEST_INSTANCE from the packs the
world enables when it loads: later packs override earlier ones and the
built-in data, and /reload or /datapack do not change it.

When a world opens (the end of load_world_state), the built-in pack's and
the initially enabled packs' test_instance files are read once, in pack
order, through the existing test instance decoder. They become the
world's test definitions (test_defs) and its minecraft:test_instance ids,
replacing the previous world's. /reload and /datapack leave them as they
are. A game with no world opened reads the game's own data as before.

Fixes #11976

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:59:12 +01:00
Horizon agent 52343380b3 Merge remote-tracking branch 'origin/main' into split/melee 2026-11-07 18:58:14 +00:00
Florian ERNSTandCodex aa019cfaa4 Canonicalize two saved adventure predicate forms
Co-Authored-By: Codex <[email protected]>
2026-11-07 19:58:10 +01:00
Horizon agent dd0050e268 Merge remote-tracking branch 'origin/main' into cg65v-10559
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 19:56:17 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 9e962f6350 HDR layered frame docs: say what a screenshot shows without promising the tone curve changes it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:55:14 +01:00
Horizon agent e2680a5450 Merge remote-tracking branch 'origin/main' into split/window-handler 2026-11-07 18:53:48 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 78ea4b6d02 HDR layered frame: drop the coverage debug flag the display pass no longer reads
The coverage view is chosen once, when the shader is wrapped; nothing reads the field after.
Also lets a test fill the coverage with its own f64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:51:26 +01:00
Florian ErnstandClaude Opus 5.5 56a578f0a6 Merge origin/main into fix/h170-place-feature-inline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:50:54 +01:00
Horizon agentandClaude Opus 5.5 5e7d248148 Move the autosave timer and world metadata save out of game.rs into game/autosave.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:49:36 +00:00
Horizon agent 67f34e0fdd Merge remote-tracking branch 'origin/main' into split/inventory-slots 2026-11-07 18:49:36 +00:00
Florian ErnstandClaude Opus 5.5 e2367a4b6a Skip the full Mob tick tests without the vanilla data
CI has no prepared resources, so the assert on the test world failed
there. Do what the other test-world tests do: check nothing when the
data is missing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:49:30 +01:00
Horizon agentandClaude Opus 5.5 17a68d7b97 Move the renderer settings and far terrain distances out of game.rs into game/render_settings.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:48:42 +00:00
Horizon agentandClaude Opus 5.5 90b30ee17b Move block particle sprites and destroy particles out of game.rs into game/block_particles.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:47:47 +00:00
Ludovic Marie 5578753b58 Merge pull request #11981 from horizon-rust/perf/portal-ticking
🤖 Agent: requeued after merging main: the only conflict was the module list in game.rs (`pointer_capture` from main beside `portal_ticking_tests`), resolved by keeping both; no code change since the checks.
2026-11-07 19:46:09 +01:00
Horizon agentandClaude Opus 5.5 6faf3ef3f3 Move the player's melee attack out of game.rs into game/melee.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:45:59 +00:00
Horizon agentandClaude Opus 5.5 ab44611814 Move the game window's ApplicationHandler out of game.rs into game/window_handler.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:44:52 +00:00
Horizon agent 8c612bc687 Merge remote-tracking branch 'origin/main' into perf/portal-ticking
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-07 19:44:39 +01:00
Ludovic Marie 9debdf26d4 Merge pull request #11982 from horizon-rust/perf/mod-hook-spans
🤖 Agent: queued: spans only, no behaviour change; pr_ready --quick green. Point: the guards wrap the mods' hooks outside every level span, so the SMP's 1-2 s ticks get a name in horizon-report.
2026-11-07 19:43:54 +01:00
Horizon agentandClaude Opus 5.5 27a2eb565e Move container slot layout and drawing out of game.rs into game/inventory_slots.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:43:45 +00:00
Florian ErnstandClaude Opus 5.5 596fda5618 Overlap note: check heads in one git call, diff in parallel
The workflow's clone is blobless, so `git cat-file -e` on a head it
does not hold fetches it from the server, one round trip per check.
On busy days the job ran into its 10-minute timeout. Check every head
in one `cat-file --batch-check` with lazy fetching off, and run the
diffs on 8 threads so their blob fetches overlap. A note for #11723
now takes 13 s instead of 170 s; `show` over every open pull request
29 s instead of 785 s, with the same notes.

Closes #11984

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:42:34 +01:00
TheMrZZandCodex gpt-6.1-sol 46b257b1d5 fix: honor supported Equippable overrides on item use
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 19:38:39 +01:00
Ludovic Marie d7ae75b441 Merge pull request #11967 from horizon-rust/split/play-input
🤖 Agent: pure move of 390 lines (toggled keys, mapped keys, hotbar wheel, play_mouse) to game/play_input.rs; the two hook tests that compile mapped_key_event and the wheel branch read the new file.
2026-11-07 19:36:04 +01:00
Ludovic Marie bc017eec9b Merge pull request #11946 from horizon-rust/split/screen-backgrounds
🤖 Agent: pure move of 170 lines (title panorama, menu blur, border wall, the screens' render-post hooks) to game/screen_backgrounds.rs; regenerated on main after #11939 since the blocks were adjacent.
2026-11-07 19:36:01 +01:00
Horizon agentandClaude Opus 5.5 693178dc36 Move the startup resource load out of game.rs into game/resource_load.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:34:57 +00:00
Florian ErnstandClaude Opus 5.5 9f0b1d99c1 Save a mob's movement_speed instance only once it has ticked
AttributeMap.pack saves the attribute instances that exist. A fresh
living entity has none (attributes:[]); its first tick creates
movement_speed (LivingEntity.aiStep -> removeFrost -> getAttribute).
The mob writer added movement_speed to every save, so a mob that had
not ticked yet already saved it.

The writer's invented movement_speed entry now waits for the mob's first
tick (its tick count). The instances the load itself makes stay:
Zombie and Piglin setBaby the speed, Rabbit setVariant the attack damage.
Instances the mob was loaded with are kept as before, and the list is
written even when empty.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:34:14 +01:00
MSedgeMC 8b0c8e40a6 A trader stops trading when hurt or out of reach
`TradeWithPlayerGoal.canUse` refuses a trader that `wasHurtRecently` or
whose trading player is more than four blocks away, but Horizon's
`TradeWithPlayer::can_use` checked neither. A wandering trader that took a
hit, or whose customer walked off, stayed in the stand-still trade goal
where vanilla ends the trade.
2026-11-07 18:34:07 +00:00
MSedgeMC d980cd810d A happy ghast on a still timeout stops floating
`HappyGhast.HappyGhastFloatGoal` overrides `canUse` with
`!isOnStillTimeout() && super.canUse()`. `FloatGoal` does not override
`canContinueToUse`, so `Goal.canContinueToUse` asks that same overridden
`canUse` on every tick: a player standing on the ghast makes it stop
floating. Horizon's `HappyGhastFloat::can_continue` re-ran only
`Float.can_use`, so the goal kept hopping in water under a player who was
holding it still.
2026-11-07 18:31:37 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 196bea6f1a HDR marks: entities, the hand and translucent particles mark at the entity pipeline's depth bias
#7820 gave the entity pipeline a constant depth bias of 128 so entities win the tie with
coplanar terrain. HDR output marks what it draws by drawing the same quads again, alpha only,
GreaterEqual against the depth it left, but without the bias: every mark tested a hair behind
that depth and failed. Mobs lost their HDR lighting, light sources in the hand stopped shining,
and the empty hand only looked right by reading the ground's mark behind it.

The marks after entities, the hand (unmark and mark) and translucent particles now draw at the
same bias. Translucent items, additive draws and the OIT stage follow unbiased pipelines and
keep the unbiased mark.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:27:20 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 cdabdddb6b HDR float frame: the HUD and menus lay over the world's light
On the float HDR frame the last pass guessed which pixels still showed the world
(`shows_world`) and clipped everything else at white, so the world's light dropped to
SDR under the chat's see-through background, a menu's backdrop and the coloured camera
overlays. Where the device records HUD coverage (#11896) the frame is now drawn in
layers, as the HDR note's step 3 specifies:

- After the world and the hand, `Hdr::resolve` turns the frame's marks into light once.
  The camera overlays, post effects and entity outlines draw on light after it.
- `keep_world` becomes a pass: the world clipped to white, alpha 0, into a HUD target in
  the frame's format, which the HUD and menus draw on with their unchanged blends.
- The last pass lays the HUD over the world's light by its coverage, in gamma as the HUD
  blends (H = P - (1 - c) B, then H ui^(1/2.2) + (1 - c) W). UI Brightness scales the
  HUD's own part.
- The menu blur blurs that composite, light included, and makes it the world the menu
  draws over.

Until the tone curve, a screenshot is the HUD target and the world icon the world, both
clipped at white; the -hdr.png is the full picture. The legacy path, the 16-bit frame and
HDR off are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:27:07 +01:00
abit-foggyandCodex GPT-6 e5fe17622c chore(studio): merge main into B2 branch
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 08:20:11 -10:00
Horizon agentandClaude Opus 5.5 b19634003c Server tick: spans for the mods' tick hooks, the config poll and idle levels
A dedicated server's slow tick lines on the SMP show 1-2 s ticks whose
time is in no span: it is spent outside every level, where only the mods'
ServerTickStart/End hooks, the config poll and the idle-level close run.
Each gets a span, so a slow tick line and horizon-report name it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:17:09 +00:00
Florian ErnstandClaude Opus 5.5 78efabcfa7 Copy a clone's pending block ticks to its destination
/clone placed the blocks but not the block ticks pending in its source
box, so a strict clone of fresh sand never fell, while vanilla 26.3's
CloneCommands.clone ends with LevelTicks.copyAreaFrom over the whole
source box.

cmd_clone now calls the existing LevelTicks::copy_area with the source
box and the destination offset, after the placements and block
entities and before the empty-count failure, as vanilla does. The core
copy (the box filter, trigger times, priorities, sub-tick reordering
and dedup) is unchanged. Block ticks only, in the same level.

Fixes #11963

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:16:16 +01:00
MSedgeMC 8d783c0ff0 A cube mob keeps still while it is a passenger
`AbstractCubeMob.CubeMobKeepOnJumpingGoal.canUse` is
`!this.cubeMob.isPassenger()`, but Horizon's `CubeKeepJumping::can_use`
returned true unconditionally. A slime or magma cube riding a boat, a
minecart or another mob therefore kept hopping and steering its vehicle
every tick, where vanilla leaves the vehicle alone.
2026-11-07 18:14:53 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 cbe9ddfa3f HDR marks: entities, the hand and translucent particles mark at the entity pipeline's depth bias
#7820 gave the entity pipeline a constant depth bias of 128 so entities win the tie with
coplanar terrain. HDR output marks what it draws by drawing the same quads again, alpha only,
GreaterEqual against the depth it left, but without the bias: every mark tested a hair behind
that depth and failed. Mobs lost their HDR lighting, light sources in the hand stopped shining,
and the empty hand only looked right by reading the ground's mark behind it.

The marks after entities, the hand (unmark and mark) and translucent particles now draw at the
same bias. Translucent items, additive draws and the OIT stage follow unbiased pipelines and
keep the unbiased mark.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:14:43 +01:00
Stink-OandClaude Opus 5.5 749c5405f6 World hold: its import on a line of its own, apart from the shared mod_api list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:13:53 +00:00
Horizon agent 8711aabd75 Merge remote-tracking branch 'origin/main' into split/play-input 2026-11-07 18:09:24 +00:00
Horizon agentandClaude Opus 5.5 76a5bd8a22 Move item use over time out of game.rs into game/using_item.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:08:23 +00:00
Florian ErnstandClaude Opus 5.5 08ce93b6f1 Keep the shared inventory helpers crate-private and test the delegation
add_to_container and load_container are only used inside the crate. Two
unit tests pin VillagerState::add_item as add_to_container with eight
slots, and a later same-kind stack merging while eight kinds fill the
slots.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:06:20 +01:00
Horizon agentandClaude Opus 5.5 453d8b89ce Move the menu backgrounds out of game.rs into game/screen_backgrounds.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:05:14 +00:00
Ludovic Marie 845132a56b Merge pull request #11945 from horizon-rust/split/loading-window
🤖 Agent: pure move of 415 lines (first renderer, start_game) to game/loading_window.rs; the Windows-only hwnd stays in game.rs, so no cfg'd re-import is needed.
2026-11-07 19:03:20 +01:00
Mik00000 bc8912c13f bench: add stress_mob_ai scene for active mob AI and pathfinding 2026-11-07 20:02:57 +02:00
Horizon agentandClaude Opus 5.5 051e3dd546 Portals: entities past the simulation distance neither cross nor cool down
Entity.handlePortal runs in the entity's own tick, and ServerLevel.tick
ticks only the entities in entity-ticking chunks. handle_portals and
find_portals walked every loaded entity, so a mob in a nether portal past
the simulation distance went through it and its cooldown counted down.
They now skip entities outside entity-ticking chunks and keep the portal
state of the ones waiting there.

Fixes #11960

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:01:59 +00:00
Horizon agentandClaude Opus 5.5 ab5dfae985 Move play key and mouse mappings out of game.rs into game/play_input.rs
Pure move (#11857). Private methods and items become pub(super), the same reach they had in game.rs (the game module and its children). Port map rows, source-reading tests and the docs tables follow the code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:01:11 +00:00
Florian ERNSTandCodex 408e2606d8 Preserve weaker Mob effects through countdown and promotion
Keep active tuples authoritative and carry only fallback EffectInstance subtrees through Mob load/save and the audited mutation boundaries. Reuse the existing pure duration operation after server work.

Co-Authored-By: Codex <[email protected]>
2026-11-07 18:59:12 +01:00
Ludovic Marie 19a6e9b81b Merge pull request #11943 from horizon-rust/split/world-entry
🤖 Agent: pure move of the world entry impl block (367 lines) to game/world_entry.rs; the world_items!/world_recipes! macros stay defined before the mod list, so the moved code still sees them.
2026-11-07 18:54:04 +01:00
Ludovic Marie de118feb4e Merge pull request #11948 from horizon-rust/perf/entity-inside-ticking
🤖 Agent: merging #11948 (smp-200). The one risk was the filter cutting block contact for entities that do tick: it keys on the same entity_ticking_area test the mob and item ticks use. The new test shows a zombie inside simulation distance 4 still presses its plate while one past it doesn't (and fails without the change). server_entities, mob_ai, cauldron, raid and dedicated_server harnesses pass locally; CI parity covers the rest.
2026-11-07 18:51:29 +01:00
Horizon agentandClaude Opus 5.5 e508e7919f rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:49:45 +00:00
Ludovic Marie f222616f7e Merge pull request #11939 from horizon-rust/split/creative-screen
🤖 Agent: pure move of 440 lines (the creative screen) to game/creative_screen.rs; no open PR touches them, visibility stays within the game module.
2026-11-07 18:48:49 +01:00
Ludovic Marie 0f89fd7e00 Merge pull request #11937 from horizon-rust/split/pointer-capture
🤖 Agent: pure move of 324 lines to game/pointer_capture.rs; test_world_input_capture's method() now also finds pub(super) methods, so the capture fixture still compiles the real code.
2026-11-07 18:48:46 +01:00
Horizon agentandClaude Opus 5.5 6889d9366b Notices: point the OpenJDK entry at its replacement, #11944
Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:46:18 +01:00
Florian ErnstandClaude Opus 5.5 40ad3a91c9 Keep an Allay's liked_player memory time to live and expire it
An Allay loaded with a finite minecraft:liked_player memory kept it
forever and saved it without its ttl: the loader read only the memory's
presence and value, the save wrote the value, and nothing counted the
memory down.

- The memory's time to live is kept next to the existing liked-player
  state (None for a memory that never expires), read on load and
  written on save as MemorySlot.visit does.
- The Allay's AI step, which NoAI skips like customServerAiStep, ticks
  it as MemorySlot.tick does: it counts down, and is cleared on the
  tick after it reaches 0.
- Giving an item sets the memory without a time to live
  (Brain.setMemory), taking it back erases it, and the /data path that
  keeps the memory keeps its time to live too.

Fixes #11936

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:42:50 +01:00
SenjinandClaude Opus 5.5 fb3f1bce90 Shader packs: LabPBR maps for entities, items and the hand
The shared atlas's textures (items, entities, block entities) now bring
their _n and _s maps to shader packs as paged maps: 16x16 pages of the
atlas, only those holding a mapped texture stored, found through a page
table at the map's start. The programs drawing that geometry read
normals and specular through hz_pagedTexture.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:39:19 +01:00
abit-foggyandCodex GPT-6 114b09babe feat(studio): autocomplete studio commands
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 07:37:39 -10:00
Horizon agent 0804e2219a Merge remote-tracking branch 'origin/main' into split/loading-window 2026-11-07 17:35:51 +00:00
Florian ErnstandClaude Opus 5.5 181ca3d833 Merge origin/main into fix/h052-vehicle-container-save
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:35:40 +01:00
Horizon agentandClaude Opus 5.5 1c6a22db1a java_hash_map: remove the implementation translated from OpenJDK
JavaHashMap reproduced java.util.HashMap iteration order by translating
OpenJDK's java/util/HashMap.java (GPL-2.0 with the Classpath Exception):
its tree-bin routines, their signatures and their quirks (#6741). This
keeps the public API and removes the body, which panics with todo!() until
the next commit writes it again from a behaviour description, the Javadoc
and the real JVM's results, by an agent that has not read either source.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:34:10 +01:00
Florian ErnstandClaude Opus 5.5 b2d87410fc Merge a Piglin's and a Villager's saved inventory stacks on load
InventoryCarrier.readInventoryFromTag loads the saved stacks with
SimpleContainer.fromItemList: clear, then addItem each one, which fills
earlier stacks of the same item and components up to the limit before it
takes an empty slot, and drops what does not fit in the eight. Piglins
and villagers took the first eight saved stacks as they were, so 2 + 3
stone stayed two stacks and 40 + 40 did not become 64 + 16.

The villager's existing SimpleContainer.addItem port becomes a free
add_to_container, unchanged; both loads add each decoded stack through it
at the type's stack limit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:33:30 +01:00
LokiandAntigravity 0a85491338 client/render: omit block outline when an entity is targeted (fixes #11804)
Co-Authored-By: Antigravity <[email protected]>
2026-11-08 02:31:51 +09:00
Horizon agent 8a9a6d5e3f Merge remote-tracking branch 'origin/main' into split/world-entry 2026-11-07 17:28:44 +00:00
Ludovic Marie d17e8fa343 Merge pull request #10330 from MSedgeMC/fix/bullet
🤖 Agent: kacpherek's Dir point is fixed (from3DDataValue wrap, Steps unclamped, tested); on top of MSedgeMC's main merges I only moved the no-target doc comment back onto its test; shulker_space and the save harnesses pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 18:28:07 +01:00
Ludovic Marie 6f374959b7 Merge pull request #10288 from MSedgeMC/port_map/seven-dead-citations
🤖 Agent: port_ledger map-check on the four changed TSVs reads 519 rows, no problems, and kacpherek's PackSelectionScreen.Watcher row is restored (rdbtCVS's fix); TSV-only, no game code.
2026-11-07 18:28:03 +01:00
Horizon agentandClaude Opus 5.5 15a8b2a77c jmath: rewrite the fdlibm functions from Sun's C sources (#6741)
jmath.rs held ports of OpenJDK code: fdlibm::{log, log10, cbrt, hypot}
from OpenJDK's FdLibm.java (GPLv2 with the Classpath Exception),
aarch64::{sin, cos} from HotSpot's AArch64 trig intrinsics (GPLv2), and
round_f32 from OpenJDK's Math.round(float). Horizon is MIT.

Rewritten in a two-person clean room from Sun's fdlibm 5.3 C sources, a
behaviour-only description of the AArch64 sin/cos, and the Java SE API
documentation:

- jmath/fdlibm.rs (new, Sun's notice at the top): log, log10, cbrt and
  hypot ported from e_log.c, e_log10.c, s_cbrt.c and e_hypot.c; sin_fma
  and cos_fma, fdlibm's sin and cos (s_sin.c, s_cos.c, k_sin.c, k_cos.c,
  e_rem_pio2.c) with fused multiply-adds, as Math.sin/cos on AArch64.
- jmath.rs: round_f32 from the Java SE specification of
  Math.round(float); the module doc says what each platform computes;
  jmath::fdlibm and jmath::aarch64::{sin, cos} keep their paths.
- geometry/trig.rs: Sun's notice above its fdlibm asin/atan/atan2 ports.
- THIRD_PARTY_NOTICES.md: an fdlibm section.

Bit for bit against JDK 25: StrictMath log, log10, cbrt and hypot on
2.5M, 2.5M and 2.7M inputs and 5.9M pairs; Math.sin and Math.cos on an
Apple M4 on 3.05M arguments, compiled and -Xint; Math.round(float) on
all 2^32 floats.

Refs #6741

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:24:45 +01:00
Florian ErnstandClaude Opus 5.5 833bb5cab4 Merge origin/main into fix/h155-fork-function-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:23:43 +01:00
IwantHappiness 5d9d9fb3ab Merge branch 'main' into translate-ru-lang 2026-11-07 20:18:57 +03:00
Florian ErnstandClaude Opus 5.5 65e2abae80 Count Absorption in a mob's max_absorption and clamp its amount to it
Absorption adds 4 per level to max_absorption (an ADD_VALUE modifier),
and LivingEntity clamps the absorption amount to getMaxAbsorption: when
the effect starts (setAbsorptionAmount) and when it goes
(onAttributeUpdated). A mob's max_absorption lived only in the command's
per-target attribute map, without the effect's modifier; /effect clear
left the amount, and expiry subtracted 4 per level, so a mob with a
raised base lost absorption it keeps in vanilla.

The mob's max_absorption now uses the attribute map it already has for
armor; its effective value counts the effect in the ADD_VALUE phase. The
add clamps the amount to it, /effect clear and expiry clamp to what is
left. The command-set base and modifiers stay unsaved, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:17:00 +01:00
Horizon agent 1206818cde Merge remote-tracking branch 'origin/main' into split/creative-screen 2026-11-07 17:17:00 +00:00
Ludovic Marie 12157a2d67 Merge pull request #11901 from horizon-rust/feat/shop-stand-in
🤖 Agent: the stand-in is painted from the website's own palette and boxes (no Mojang texture); test pins its face pixels and transparent outer layers.
2026-11-07 18:12:33 +01:00
Horizon agentandClaude Opus 5.5 b029b9d04f Docs: list code removed for its license, correct "No Minecraft files"
THIRD_PARTY_NOTICES.md gets a "Code removed for its license" section for
anyone holding an older version: the shader pack code written from Iris's
source (LGPL-3.0, removed in #1561) and the math functions translated from
OpenJDK and HotSpot (GPL-2.0, #6741). In those versions that code is not
covered by Horizon's MIT license.

CONTRIBUTING.md says OpenJDK's own source is GPL too, so it is off limits
like any GPL project; Java's behaviour comes from its documentation, a JVM's
results, or a permissive original such as Sun's fdlibm.

docs/how-horizon-is-built.md no longer says Horizon holds no Minecraft data
and never downloads it: the datafix tables come from vanilla's code, and the
launcher downloads 26.3 from Mojang for a signed-in account that owns it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:11:12 +01:00
Horizon agent 9b89ff00ee Merge remote-tracking branch 'origin/main' into split/pointer-capture 2026-11-07 17:10:59 +00:00
Horizon agentandClaude Opus 5.5 2e13076a93 List game/loading_window.rs in the docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:21 +00:00
Horizon agentandClaude Opus 5.5 9974322832 Launcher Shop: dress Horizon's stand-in player when there is no skin
Without a skin the launcher can read (signed out, offline), the Shop's pictures
and its 3D view drew the plain grey figure. They now use the stand-in the
website's shop draws (its standInSkin, original art painted from rows, not
Mojang's Steve or Alex), so both shops show the same player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:09:34 +01:00
Horizon agent b21f0c92f2 wip loading_window 2026-11-07 17:08:53 +00:00
Florian ErnstandClaude Opus 5.5 5f2e3cb14d Register a newly loaded chunk's tick queues before commands reach it
In a frozen level, water a command placed in newly forceloaded chunks
before the next world tick never flowed: the chunks' tick containers
were only registered by tick_blocks (a running world tick) or by
postprocessing, so the water's flow tick found no container and was
dropped. Vanilla registers them when the chunk becomes a full chunk
(LevelChunk.registerTickContainerInLevel), before any command sees it.

update_world now runs the existing sync_tick_chunks once the streamed
chunks are resident and the unloaded ones are saved with their ticks,
so registration comes before the tick's commands and world tick. Only
an authoritative game (not a server's client) registers.

Fixes #11873

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:06:05 +01:00
CocofireHDandopencode a61d272adc Block entities: shade by the face normal so packs don't darken them twice
A block entity's faces were baked with `minecraft_mix_light` and their
normal left zero. A shader pack's `gbuffers_block` (or its fallback)
receives the unshaded colour and the face normal from Iris, then applies
its own lighting, so Horizon's baked shade was applied a second time:
chests, shulkers, banners and the rest came out darker than the terrain
around them (#8390).

`draw_part` now emits the tint alone and the face's world normal
(`normal.w` = 127), and the entity shader applies `minecraft_mix_light`
from it, as it already does for mobs. Lights that are not the level's (an
item preview's `Lighting.Entry`, which the entity shader cannot know)
stay baked into the colour with `normal.w` 0, which is the path
`game/item_draw` gives block-entity items in inventories and hands.

The parity harness compares the tint and the face normal; the loom's GUI
preview keeps its baked `ITEMS_FLAT` colour. The chest cache test now
checks the nether flag reaches the shader instead of changing a baked
colour, so the cached geometry serves both dimensions.

Fixes #8390

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 18:05:25 +01:00
abit-foggyandCodex GPT-6 d607c2f137 feat(studio): add freecam and camera paths
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-07 07:04:37 -10:00
SenjinandClaude Opus 5.5 dddf351a05 Shader packs: at_velocity for the player's own model
Its vertices fell in the unmarked run that closes the mobs', so in third
person (and in a pack's shadow pass) its at_velocity read zero.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:03:19 +01:00
Ludovic Marie 31545199d4 Merge pull request #11894 from horizon-rust/feat/shop-fortnite
🤖 Agent: pictures are drawn on background threads and cached by item hash and screen scale, so the Shop never renders on the UI thread; prices and the checkout call are the same rules as before (two live checkouts cancelled via Paddle).
2026-11-07 18:03:10 +01:00
Delete228andCodex e54dcc07cf Merge Horizon main into the stateful block entity PR
Co-authored-by: Codex <[email protected]>
2026-11-07 20:00:52 +03:00
Horizon agentandClaude Opus 5.5 9a8781468e rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:56:54 +00:00
Horizon agentandClaude Opus 5.5 1117b98d15 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:56:19 +00:00
Horizon agentandClaude Opus 5.5 8fe0e40d68 Block contact only for entities in entity-ticking chunks
Entity.applyEffectsFromBlocks runs in the entity's own tick, and
ServerLevel.tick ticks only entities in entity-ticking chunks, so a mob or
item past the simulation distance presses no plate and is not hurt by a
cactus. tick_blocks swept every loaded entity: 2.5 ms a tick on the SMP,
where ~2,900 of 4,600 overworld mobs sit outside entity-ticking chunks.

Fixes #11881

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:55:32 +00:00
Florian ERNSTandCodex 6079903ffc Create missing live dungeon spawner block entities
Carry the already selected mob through an optional creation-only level hook. Preserve existing entries and the generation initializer and RNG order.

Co-Authored-By: Codex <[email protected]>
2026-11-07 17:54:52 +01:00
CocofireHDandopencode 3a7f276eab Fonts: read a pack font file the overlay lacks from the game
A pack that adds font providers over the game's own can leave the overlay without the vanilla font sheets its merged font/default.json names. Every glyph then became the missing box. Read a bitmap sheet, TrueType file or Unihex file the overlay lacks from the game's own root, as vanilla's resource stack keeps the game below every pack. Font definitions stay the overlay's, so a pack filter can still hide one.

Fixes #8875

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:54:37 +01:00
Horizon agentandClaude Opus 5.5 f68c335eb2 Point the port map and source-reading tests at game/pointer_capture.rs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:53:05 +00:00
MrCloudy2andClaude Opus 5.5 570d53a780 A move packet counts from the position the server last reported
ClientPacketListener.handleMoveEntity decodes a move's delta from the
entity's position codec base, which AddEntity, a move and a position sync
set and a TeleportEntity leaves alone. Horizon counted it from the
tracked position, which the teleport had moved: when a command teleported
a vehicle, its rider was told the teleport and then the tracker's delta,
and saw the vehicle (and themselves) as far again past where it was.

lan_e2e gains `vehicle-teleport`: a strider the guest rides, teleported
3 blocks, is where the server has it in the guest's game.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:51:29 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 beaad0e79d HUD coverage: drop a pack's colour outputs the HUD has no target for
A pack text shader may declare colour outputs past location 0. The HUD's
pipelines have one target, so they went nowhere, but the coverage wrapper kept
them, and one at location 1 collided with the coverage and dropped a shader
that works today.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:50:19 +01:00
Horizon agent 12dad67f8c Merge remote-tracking branch 'origin/main' into feat/shop-fortnite
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-07 17:49:15 +01:00
Horizon agentandClaude Opus 5.5 922f00f52e Launcher Shop: a game-style item shop with drawn items
Ludovic (2026-11-07): "In the launcher you just don't see the items." The Shop
now opens on the season pack across the top (its looks drawn side by side, name,
item count, price, Buy), then rows of big tiles: Looks, each collection's items,
the founder set and the Supporter badge. A tile is the item drawn, its name and its
price; it lifts under the pointer. A tile opens it big on the account's own player
in 3D (an emote plays), with a look's items, Buy and Gift.

- Pictures: the Wardrobe's CPU renderer, framed on the item (preview::fitted_view),
  at device pixels supersampled 2x, on background threads, kept by item hashes.
- Buy and Gift are one checkout of that offer through the same web checkout;
  prices still come from the catalogue and the service's set rules.
- Cut: the page's line, the basket and its notes; three new strings (Buy, Gift,
  Looks) in all 86 tables, nine unused ones removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:48:35 +01:00
Horizon agentandClaude Opus 5.5 ab37cfb5aa Merge origin/main into #11497
items.rs tests: keeps this branch's tool-rules test and main's command
loot zero-count test, which both landed at the same place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:48:34 +01:00
Horizon agentandClaude Opus 5.5 92b2977e3b Merge origin/main into #11474
end.rs: keeps this branch's live gateway items; main's only change there
(item_entity_to_nbt_with_items) was inside the saved-item path this
branch removes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:46:53 +01:00
Ludovic Marie 1fe21fb861 Merge pull request #11885 from horizon-rust/feature/ms-account-picker
🤖 Agent: opens the code page in the default browser's private window (flag per browser, pure-function tests); no Azure change, Safari/unknown get Copy link + hint
2026-11-07 17:45:36 +01:00
Horizon agentandClaude Opus 5.5 68b03b5184 docs: how the launcher adds another Microsoft account (#3786)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:43:47 +01:00
Horizon agentandClaude Opus 5.5 200615a30a Move world entry and the world content tables out of game.rs into game/world_entry.rs
Pure move (#11857): defer_world_entry to join_world_rest (the deferred world entry, world creation and rest polling, the world's item/recipe/station tables). Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 1ba0db3c3c Move the creative inventory screen out of game.rs into game/creative_screen.rs
Pure move (#11857): the creative screen's tabs, pages, tooltips, trash, search and clicks. Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 0ce0e67282 Move the mouse look and pointer grab out of game.rs into game/pointer_capture.rs
Pure move (#11857): look_options to regrab_on_refocus (mouse look, pause when inactive, held keys, the pointer grab and cursor icon). Private methods become pub(super), the same reach they had in game.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:41:27 +00:00
Horizon agentandClaude Opus 5.5 00d1e1a46e Launcher: clippy, and the Linux program table only where Linux reads it
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:35:45 +01:00
CocofireHDandopencode f178cbe003 HUD atlas: pack a bitmap font's sheet from any texture path
A bitmap provider's file may be any texture, and HUD packs keep sheets outside textures/font. HudAtlas already received the sheet from the font commit, but only if the texture was packed: WALKED_TEXTURE_FOLDERS did not include custom paths such as textures/lumiere/hud, so those sheets still fell back to per-glyph run-time copies. Collect every bitmap provider's file while building the atlas, deduplicated against the walked textures, so its glyphs use the sheet's atlas rect.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:35:29 +01:00
CocofireHDandopencode 0478b4deec Fonts: use an atlas sheet outside textures/font for server HUDs
A bitmap provider may name any texture, and server HUD packs keep their glyph sheets under their own namespace or a path such as textures/lumiere/hud. The HUD atlas already holds those textures, but Font only looked up sheets under font/, so every glyph was decoded from disk and copied into the 512-row run-time region one by one. When that region filled, a glyph became the 6-pixel missing glyph, shifting the rest of the HUD line.

Look the sheet up by its atlas name for every namespace and keep every atlas sprite available to the font. Glyphs now use the sheet's atlas rect, so their advances come from the original texels and the run-time region is not involved.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 17:35:29 +01:00
TheMrZZandCodex gpt-6.1-sol 8db7c3ddc0 fix: preserve plain forked function feedback order
Prepare starts for eligible actual sources before executing their owned function calls. Keep the existing body, callback, store and fallback paths, and consume or clear invocation slots around leaf execution.

Add frozen Java/Game command parity and causal store, return, error and fallback controls.

Fixes #11824

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 17:33:05 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b5a1d40dc6 HDR coverage view: magenta at paper white, not PQ's 10 000 nits
In PQ output the debug view mixed toward PQ (1, 0, 1), so any covered pixel
blew out. Mix toward BT.709 magenta at the paper white instead, encoded like
the picture.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:32:22 +01:00
IwantHappinessandNemotron 4cc2a04c25 Translate remaining Russian launcher and game strings
Translate still-English UI strings in launcher/lang/ru.json and the game's
crates/horizon-client/assets/horizon/lang/ru_ru.json (particle culling
settings). Keep proper nouns, acronyms, key names, paths and env vars in
English.

Co-Authored-By: Nemotron <[email protected]>
2026-11-07 19:20:43 +03:00
Horizon agentandClaude Opus 5.5 c794b87841 Signatures for prebuilt native-mod builds (#11859)
horizon-release-sign gets `native_build`: a build's request (target, base commit, each mod's
id, version and registry file SHA-256, sorted) and its key, the release and asset names in
horizon-mods, and the signed message `horizon native build <key> <target>\n<sha256>  <asset>\n`,
which no release or Nightly signature can pass for. The keys are their own
(native-build-keys.txt): horizon-mods' `native-builds` environment holds the private one,
because the registry compiles other people's code there.

tools/ci/sign_native_build.sh signs with OpenSSL as sign_release.sh does, refuses a key whose
public half isn't listed, and checks its own signature; tests sign with it and verify as the
launcher will.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:19:02 +01:00
MrCloudy2andClaude Opus 5.5 a61825404a A vehicle's teleports leave its rider's look alone
ClientPacketListener.handleMovePlayer applies a position packet only when
the player is no passenger, and still accepts the teleport. Horizon's
client applied it to a rider too, look included: a map whose datapack
teleports the vehicle every tick (`tp @s ~ ~ ~ 0 0` on a strider) snapped
the rider's view back 20 times a second. A rider now keeps its own
position and look and answers the teleport with them.

lan_e2e gains `rider-look`: a tick function teleports the strider the
guest rides, and the guest's view stays where they turned it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:18:26 +01:00
Horizon agentandClaude Opus 5.5 1ee069b6ba Launcher: private-window strings in every language, rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:18:12 +01:00
Horizon agentandClaude Opus 5.5 c30dfd55e5 Launcher: add another Microsoft account from a private browser window (#3786)
The device code page goes ahead with the account the browser is signed in to.
Open it in the default browser's private window (Windows ProgId, macOS
LaunchServices, Linux xdg-settings), or give the link to copy, or sign the
browser out of Microsoft first. No Azure app change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:15:21 +01:00
Florian ErnstandClaude Opus 5.5 1a9c87059b Merge origin/main into fix/connected-guest-attribute-commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:15:12 +01:00
Florian ErnstandClaude Opus 5.5 ce25d100ef Merge origin/main into fix/mob-effect-amplifier-codec
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:15:10 +01:00
Florian ErnstandClaude Opus 5.5 4d8e70da2c Merge origin/main into fix/h297-potion-effect-icon
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:59 +01:00
Florian ErnstandClaude Opus 5.5 a191dcd2b2 Merge origin/main into fix/h052-vehicle-container-save
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:59 +01:00
Florian ErnstandClaude Opus 5.5 8efbe1c75e Merge origin/main into fix/h051-kill-vehicle-contents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:58 +01:00
Florian ErnstandClaude Opus 5.5 df25b701c5 Merge origin/main into fix/loaded-sleeper-bed-position
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:54 +01:00
Florian ErnstandClaude Opus 5.5 8b577fc24c Merge origin/main into fix/h032-jukebox-saved-clock
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:51 +01:00
Florian ErnstandClaude Opus 5.5 974305151d Merge origin/main into fix/summoned-leash-knot-lifecycle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:50 +01:00
Florian ErnstandClaude Opus 5.5 e6941baa5a Merge origin/main into fix/h195-command-damage-range
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:49 +01:00
Florian ErnstandClaude Opus 5.5 1bf526ca7f Merge origin/main into fix/h193-effective-component-presence
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:48 +01:00
Florian ErnstandClaude Opus 5.5 635f14ea0e Merge origin/main into fix/h151-boat-passenger-command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:14:47 +01:00
TheMrZZandCodex gpt-6.1-sol 3890e57c13 fix: select direct mob passengers for execute relations
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 17:14:06 +01:00
Florian ErnstandClaude Opus 5.5 2282116773 Place valid inline features with /place feature
/place feature refused every inline feature value with "An unexpected
error occurred", although vanilla 26.3 places it like a named one
(PlaceCommand.placeFeature over a ResourceOrHolderArgument value) and
reports commands.place.feature.success.inline.

The inline value is read with the existing SNBT-to-JSON pair, decoded
by the same feature codec as a named feature (feature_with_blocks,
uncached), and placed through the same Feature.place path with the
level random. A feature that does not place reports
commands.place.feature.failed; a value that does not decode keeps the
plain failure.

Fixes #11843

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:54 +01:00
MrCloudy2andClaude Opus 5.5 d72b2a555b execute on passengers finds the players riding a vehicle on a server
Entity.getPassengers holds players as well as mobs. Horizon's `on
passengers` named the player in place when a mob had a rider flag, and no
server player: a map's datapack, running with no player in place, never
found the player riding its strider, and a command the host ran named the
host instead of the guest riding. It now gives the player in place when
they ride the entity, and each other player of the level riding it.

Mob and boat passengers are left to #11810.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:47 +01:00
Florian ErnstandClaude Opus 5.5 7cd9dbc7a2 Let Health Boost raise a mob's healing maximum and clamp when it goes
Health Boost is a max_health attribute modifier. The attribute getter
already counted it, but Mob::heal clamps to the cached attrs.max_health,
which the effect paths never refreshed, and nothing lowered Health when
the effect went (LivingEntity.onAttributeUpdated). Adding the effect,
/effect clear and its expiry now refresh the cache and lower Health above
the new maximum; the add still heals nothing.

Sulfur cubes and wolves write their maximum straight into the cache
(sulfur_cube::set_size, tame::set_tame), outside the attribute model,
and keep their current behavior. Other effect removal paths are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:10:03 +01:00
Horizon agentandClaude Opus 5.5 0f8cf58043 Launcher: the Wardrobe preview can read a saved shop catalogue
HORIZON_COSMETICS_CATALOGUE=<file of GET /v1/catalogue> gives the development preview
real prices, sets and collections, so Shop screenshots show what players see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:08:41 +01:00
c74ff0070b HDR float frame: every HUD and menu draw writes its coverage of the world
With HORIZON_HDR_FRAME=float, HDR on and no shader pack, on devices with independent blending, each
HUD and menu pipeline gets a variant with a second output into an R8 target: how much of the world
behind it the draw hides (docs/design/hdr-linear-light.md, The HUD and menus). The fragment shader
is wrapped as it is, so the primary colour, its blend and alpha are unchanged, and the coverage
follows the draw's final colour and blend: layering by its alpha, additive draws not at all,
darkening ones by how much they darken, inverting and replacing ones fully. HORIZON_HDR_DEBUG=coverage
tints it magenta in the last pass; nothing else reads it yet. A resource pack text shader without a
colour output is dropped as an unusable one is, instead of panicking.

Co-Authored-By: GPT 6.1 Sol <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:05:50 +01:00
JoansitohandClaude Sonnet 5.5 6d1746d729 launcher: the Home patch log and console logs keep the wheel to themselves
Their overflow_y_scroll lists lacked the on_scroll stop_propagation every other
inner scroller has, so a wheel turn the list could not use (up at the top, or
down at the end) scrolled the whole page behind it.

Fixes #11853

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 17:04:07 +01:00
JoansitohandClaude Sonnet 5.5 af13271b86 fix(launcher): wrap Home's tab strip on narrow windows instead of scrolling it sideways
Fixes #11849

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 16:58:59 +01:00
GIGABAIT93andClaude Opus 5.5 0df4792709 Ukrainian: the 445 strings uk_ua.json lacked (PvP & HUD, Studio, mods, cosmetics)
PvP & HUD, Replay Studio, the music page, Browse Mods, the resource pack
browser, cosmetics and emotes, moderation and tempban, the controller page,
graphics tooltips and recipe transfer. Placeholders, leading and trailing
spaces match en_us; durations use abbreviations (хв, год, дн.) so one form
reads right for every number.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:56:20 +02:00
Ludovic Marie 2ebf00a028 Merge pull request #10305 from martipops/controller/guide
🤖 Agent: requeued at 8cd24bc4e5: the merger stopped on a main conflict, and the author's main merge resolves it. verify is green on this head; the run-parity label reruns parity on it before the merge.
2026-11-07 16:56:15 +01:00
Florian ERNSTandCodex 2dd6879bc4 Keep actual bell block entities for command reads
Co-Authored-By: Codex <[email protected]>
2026-11-07 16:55:24 +01:00
Evrim CicekandClaude Opus 5.5 51eae8d435 rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 18:50:56 +03:00
CocofireHDandopencode 7142128a7a wgpu-hal GL: report the context's texture units per stage
The GL backend reported MAX_TEXTURE_SLOTS (16, WebGL2's minimum) as max_sampled_textures_per_shader_stage even on desktop GL, where drivers usually offer 32. A native shader pack's sky pipeline layout declares the terrain's 10 sampled textures and the frame's 9, so it was refused at 16 although the driver accepts it (#10095).

Query GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS and GL_MAX_TEXTURE_IMAGE_UNITS, take the lower, and cap at the arrays' new 32 slots (MAX_SAMPLERS likewise). On WebGL2 the query still yields 16.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 16:49:30 +01:00
CocofireHDandopencode 81df2a1864 Renderer: refuse a native shader pack instead of panicking
A layout or pipeline the backend cannot take (a stage's bindings past its sampled-texture limit, say) reaches the device's error scope. A native pack built its frame, stages and passes outside one, so wgpu's validation error hit the default handler and crashed the game (#10095). Build the pack in a scope and return the first error, which the shader pack screen already shows as a refusal.

The test asks for OpenGL's minimum 16 sampled textures per stage and builds a sky-only pack whose layout needs 19: the pack is an error, not a validation panic.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-07 16:49:30 +01:00
SenjinandClaude Opus 5.5 edb550ac32 HDR and shader packs: glowing sign text shines
Glow ink text was full-bright in the lightmap only: HDR output and shader packs that light
with their own GI (and read the HDR tier, horizonTier) showed it as dark as plain text.
Its glyphs now carry the low HDR tier, like other things that glow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:48:02 +01:00
yahya-nusseibehandClaude Opus 5.5 f2e081520f Resource packs: take the unpack budget without fetch_update
CI's stable Rust deprecates AtomicU64::fetch_update in favour of
try_update, which the workspace's minimum Rust (1.95) doesn't have, so
clippy -D warnings failed. A compare_exchange_weak loop does the same
on every version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:47:21 +02:00
Ludovic Marie 821426be75 Merge pull request #11812 from horizon-rust/cg61/flame-test
🤖 Agent: test-only: Flame is Ignite(constant(100)) seconds, so 2000 ticks; a_flame_crossbow_shoots_burning_arrows passes with prepared resources on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:22 +01:00
Ludovic Marie 7325d8811b Merge pull request #10365 from MSedgeMC/fix/warden-anger
🤖 Agent: a re-angered suspect keeps its saved anger as increaseAnger does (test now expects 54); carries #10290 for the shared corpus; sculk, wolf_anger_writes and the save harnesses pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:19 +01:00
Ludovic Marie 5bbfe0f311 Merge pull request #10290 from TheMrZZ/fix/h183-spectral-arrow-duration
🤖 Agent: added the hit's Glowing from the arrow's own Duration (SpectralArrow.doPostHurtEffects reads this.duration; Horizon used a fixed 200); carries #10344 since both extend the command corpus; spectral_duration and command_exec pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:15 +01:00
Ludovic Marie f5c936af5e Merge pull request #10344 from TheMrZZ/fix/h053-item-health-short
🤖 Agent: merged main; the Health doc line now sits on item_entity_from_nbt next to main's numeric-UUID line instead of above the UUID helper; item_health_load and item_numeric_uuid pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:11 +01:00
Ludovic Marie 0a898f21b0 Merge pull request #10291 from TheMrZZ/fix/saved-empty-entity-tags
🤖 Agent: merged main, keeping main's arrow_owner_tests next to this PR's saved_tag_tests in data.rs; saved_empty_tags, arrow_owner and the explicit-empty ignored test pass on the union of this gate's PRs on main 1d63e0a5a (playhorizon: fmt, clippy -D warnings, client + client-lib tests, 26 harnesses all pass).
2026-11-07 16:42:06 +01:00
Florian ErnstandClaude Opus 5.5 f1ea540f08 Remove a mob's Absorption effect once the absorption is used up
AbsorptionMobEffect.applyEffectTick, asked on every tick, answers false
when the absorption amount is 0, so MobEffectInstance.tickServer drops
the effect before its duration counts down, finite or infinite. A mob's
effect tick had no Absorption case: a used-up Absorption stayed until it
expired. It now ends through the existing removal path; other effects,
infinite ones included, are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:34:19 +01:00
Stink-OandClaude Opus 5.5 aa2220fcf6 Native mod API: hold the terrain's picture while the world changes (native_world_hold)
A client mod asks once a frame (`hold.hold()`, or `hold.hold_within(x, z, radius)` for circles):
sections already on screen that are meshed again are uploaded as ever but wait beside the meshes
drawn, and letting go swaps them all in on one frame with nothing left to upload. Smaller circles
let go of what they leave. In single player, far terrain waits too for the columns the server
changes beyond the view inside the hold, and takes them in when it lets go of them. Bounded: 30 s,
1 GiB of waiting meshes, the arena running out; ended so, it stays off until no mod asks for a
frame. Clearing the sections (another world or dimension) and unloading columns drop what waits;
arena compaction moves waiting meshes too. Renderer::upload keeps its order on the normal path
(before_mesh / upload_mesh / after_mesh). Tests in gfx/hold.rs; docs in native-mods.md; fragment
world_hold.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:34:09 +00:00
8cd009e149 Read saved template structure starts before regenerating them
Part of #11126. Builds on #11652.

Decode shipwreck, ocean ruin, igloo, ruined portal, woodland mansion,
nether fossil and end city pieces from saved starts. Rebuild template
boxes and placement settings like vanilla's tag constructors, retaining
the whole-start regeneration fallback for unsupported or broken pieces.

Cover every new type with resource-free round trips and extend the
prepared-resource generation/read comparison across all three dimensions.

Piece ids go through #11652's saved_piece_id first; any other id is
matched with or without the minecraft: namespace, not lowercased, as
Identifier.parse reads it.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:33:44 +01:00
Vinícius MoreiraandClaude Opus 5.5 12dd25d421 Polytone: legacy math expressions, color and distance functions
Older packs write expressions in the legacy syntax: uppercase variables
(AGE, DISTANCE_SQUARED, RAIN...), pi and e, and bare rand(), gaussian()
and noise(). Each variable now runs as the scripting call the wiki maps
it to, and a legacy expression keeps the old argument order of lerp,
smoothstep and color. The color functions (red, color, withAlpha,
lerpColor, hsb, lerpHsb...) follow vanilla's ARGB channel rules, and
dist/distSquare join the math table. All expressions of Visual
Effects+ now compile.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:25:21 -03:00
SenjinandClaude Opus 5.5 ebfcad98f6 Shader packs: at_velocity for blocks a piston moves
A moving block (and an extending or retracting head) is drawn with the entities while it
moves, and read zero at_velocity: flying machines and piston contraptions smeared under a
pack's TAA. Each marks its own run, (6 << 32) | the cell it moves to, which stays the same
while it moves; a finished one keeps that key until the chunk mesh shows the block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:25:09 +01:00
Ludovic Marie 6cc93d13ce Merge pull request #11587 from voidsipp/fix/11123-native-hud-mode
🤖 Agent: F1 stays the vanilla two-state toggle and resets the mod's hand override; set_hud_mode is a no-op on servers and during a guest's borrowed turn; hud_mode helper fragment passes tests.test_stamp_native_hooks with main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 16:23:42 +01:00
Vinícius MoreiraandClaude Opus 5.5 c6c0a97e57 Polytone: the scripting expression language
Expressions such as `o.age() * 0.1`, ternaries, `&&`/`||`, strings,
local variables, `if` blocks and calls on context objects are parsed
once when the pack loads, with every name resolved, so a typo is a load
error. A Host answers the context objects (o, p, c, g, r, v), their
methods and the game's global functions; the math functions follow
Java's Math. 1461 of the 1520 distinct expressions of Visual Effects+
compile; the rest use the legacy uppercase names, which come next.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:22:21 -03:00
Vinícius MoreiraandClaude Opus 5.5 edab7b313f Polytone: leave keys beside the attribute groups alone
An attributes_modifiers object with a group (`base`...) no longer refuses
the file over a key that is none of the modifier's groups: it is left
alone, so a pack Polytone loads isn't rejected over an entry neither reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:21:34 -03:00
yahya-nusseibehandClaude Opus 5.5 d94fdae0b9 Agent control: no shared /tmp socket path, and bench refuses bad numbers
Without XDG_RUNTIME_DIR (containers, some WSL and minimal setups) the
control socket was /tmp/horizon-control.sock, a fixed name in a folder
every user shares. Another user who bound it first made --control fail
to start, and horizon-ctl then talked to their socket.

When the temporary directory is shared (world-writable, as /tmp), the
default endpoint is now horizon-<uid>/horizon-control.sock in it. The
game makes that folder 0700, and both the game and horizon-ctl refuse
it unless it is a real folder, owned by this user, that no one else may
enter. A temporary directory of the user's own (macOS's $TMPDIR) is
used as before, so nothing moves there. The launcher's copy of the
default endpoint (game_tools.rs) follows the same rule.

Separately, `bench -1` or `bench nan` panicked the game in
Duration::from_secs_f64; bench now takes 0 to 86400 seconds and refuses
anything else.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:20:50 +02:00
Florian ERNSTandCodex e16378accd Use world RNG for literal command predicate chances
Co-Authored-By: Codex <[email protected]>
2026-11-07 16:19:02 +01:00
Florian ErnstandClaude Opus 5.5 4924374fa5 Keep a mob's FallFlying flag and wear its glider as vanilla does
A mob loaded or summoned with FallFlying:1b kept the flag even when it
could not glide, and a gliding mob's elytra never wore. The flag was
only the raw tag's value: the loader did not read it, the save only
supplied a default, and the mob tick had no updateFallFlying step.

- mob_from_nbt reads FallFlying into the mob, and mob_to_nbt writes the
  mob's current flag (LivingEntity.read/addAdditionalSaveData).
- The mob's aiStep, with or without AI, runs updateFallFlying while the
  flag is set: canGlide (ground, vehicle, levitation, a glider whose
  next damage does not break it) clears the flag; every 20 glide ticks
  Util.getRandom picks a glider from the mob's own random.
- The game applies that wear with the existing durability path
  (Unbreaking from the level's random) to the mob's kept stack.
- LivingEntity.tick counts fallFlyTicks after aiStep.

Glide travel, checkFallDistanceAccumulation and the ELYTRA_GLIDE game
event are not part of this change.

Fixes #11809

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:14:52 +01:00
Florian ErnstandClaude Opus 5.5 4adf2afffb Drop passenger entries that load nothing, keep riders through /data merge
EntityType.loadPassengersRecursive mounts only the Passengers entries that
load, and Entity.saveWithoutId saves the riders the entity has.
take_mob_passengers kept every non-mob entry raw, so an unknown or missing
id, or a non-compound value, stayed in the saved Passengers. It now drops
an entry that is not a compound or has no registered type (entity-types,
EntityType.by), and keeps valid non-mob entries raw as before.

Entity.load does not read Passengers either. apply_entity_nbt stored the
whole merged compound as raw, so a mob, boat, minecart or TNT saved copies
of its live riders and any merged entries after /data merge. It now keeps
the Passengers the load kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:09:01 +01:00
iseaprojectsandDeepSeek V4.1 Flash d1dd934be2 Spanish: finish the launcher and es_es strings
The launcher's es.json still held 91 English values: the mod-port status sentences, release notices, look-and-feel settings, the SSH hosting panel and several install/update results. Translate them, and fix Back from the body part (Espalda) to Atrás.

es_es.json was 20 keys short of en_us.json: every /studio replay message, Play in Background and the controller-focus note. Add them so Spanish screens no longer fall back to English.

Closes #11814.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
2026-11-07 11:08:01 -04:00
Horizon agentandClaude Opus 5.5 b86fa679b9 function_tags harness: run without --work-dir
tools/parity.py runs every harness with no arguments, so the harness's
required --work-dir made it fail on every CI run before reaching the
oracle. Without the flag it now captures into a fresh
.local/function-tags-parity; an explicit --work-dir must still be new.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:05:53 +01:00
Horizon agentandClaude Opus 5.5 9a292609eb Merge origin/main into #11587
hand_geometry: keeps this branch's ui.hands_drawn (native HUD modes) and
main's rule that a studio replay never draws the hands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:04:18 +01:00
Horizon agentandClaude Opus 5.5 45da47c08b Merge origin/main into #11666
game.rs: keeps this branch's wax_on_sound (honeycomb) beside main's
transform_copper_chest_neighbour (axe/hoe/shovel transformers, #33ec06ab12).
test_use_item_on_parity.py: keeps both the honeycomb double-chest cases
and main's block transformer cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:03:22 +01:00
yahya-nusseibehandClaude Opus 5.5 04449d8b73 Multiplayer: a server's view distance and chunk centre can't overflow
view_distance and chunk_center come straight from Login,
SetChunkCacheRadius and SetChunkCacheCenter, any i32 a server sends.
remote_chunk_in_range added 3 to the radius and subtracted the centre,
and the streaming radius and sent_radius added 1: a server sending
i32::MAX or i32::MIN panicked a debug build, and in release (no
overflow checks) the wrapped range dropped every loaded chunk.

The storage range is now in_storage_range: the same ClientChunkCache
rule (radius at least 2, plus 3), with a saturating add and abs_diff,
so nothing changes for any radius a real server sends. The +1s
saturate too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 17:00:32 +02:00
Horizon agentandClaude Opus 5.5 da0b4899ff Merge origin/main into #11480
Keeps both imports in command_exec_corpus.groups(): this branch's
player_food_predicate_corpus and main's treasure_map_copies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 16:00:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 50b1d6232e docs: HDR design note: marks turned into light once in step 3, tests for mixed light and TAA
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:52:07 +01:00
yahya-nusseibehandClaude Opus 5.5 706cf3b3fa Resource packs: delete overlays no selection has used lately
Each distinct pack selection gets its own overlay folder under
.horizon-cache/resourcepacks, holding a full unpacked copy of its zip
packs, and nothing removed old ones. Every server pack, and every new
version of one, left a permanent copy the player can't see: ten
servers with 100 MiB packs kept about a gigabyte.

resource_root now marks the overlay it returns as used by this process
(a shared lock on <overlay>.overlay-cache.use, held until the process
exits, whose time is the last use), then keeps the 8 most recently used
overlays in the folder and deletes the rest. An overlay another
process uses or is building is skipped, so two games sharing a game
folder never pull one out from under each other; the marker goes before
the folder, so a half-deleted overlay is never reused. The lock files
stay, as build() already requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:45:03 +02:00
yahya-nusseibehandClaude Opus 5.5 439b609a94 Play with Friends: the guest's loopback tunnel takes only its own client
After the encrypted join, the guest offered the session on a random
127.0.0.1 port for 30 s and handed it to the first loopback connection.
Any other local user or process that found the port first got the
authenticated session to the host's server, and a web page aiming
fetch at local ports could send HTTP into the tunnel and make the
player's own join fail.

serve_loopback now returns a Loopback with the port, a random 16-byte
token and the thread. A connection must send the token within a second
of connecting (compared in constant time); anything else is dropped and
the port keeps waiting until its deadline. The client's join_friend
passes the token on, and run() writes it raw before the handshake.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:42:17 +02:00
Nikita Maximilian GrimmandClaude Opus 5.5 537291b26d docs: HDR design note: the HUD's layer and the tone curve before light is written
Light written into the frame can't be taken back out from under the HUD, and without the tone curve it clips at
the peak, so step 3 now holds the HUD's own layer and the tone curve, and step 4 the shader groups. The look
target names the differences allowed (rounding, light behaving as light under glass, water and anti-aliasing)
and the artifacts that aren't.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:33 +01:00
MrCloudy2andClaude Opus 5.5 a95c0e022d lan_e2e shield: each player's own game shows the disabled shield's cooldown
The shield check now also asks each player's window what its hotbar
draws over the shield (a new `shield_cooldown_shown` in the control
state: ItemCooldowns' percent for minecraft:shield). Right after the
vindicator's axe disables it the cooldown shows, and after the 100 ticks
it is gone. On main both players' games showed none (#11805).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:24 +01:00
Horizon agentandClaude Opus 5.5 fd4d709dc2 Merge #10290 into #10365 (both edit the same lines)
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:13 +01:00
Horizon agentandClaude Opus 5.5 c34114751a Put the no-target doc comment back on its own test
The Dir/Steps test had taken the doc comment of the no-target test; each test now has its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:12 +01:00
Horizon agentandClaude Opus 5.5 dd93b0164a Merge #10290 into #10330 (both edit the same lines)
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 ffef7b9c7e Merge #10344 into #10290 (both edit the same lines)
Conflicts kept from both sides: tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 7622c02706 Put the no-target doc comment back on its own test
The Dir/Steps test had taken the doc comment of the no-target test; each test now has its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:11 +01:00
Horizon agentandClaude Opus 5.5 744fcd7e01 A spectral arrow gives Glowing for its own Duration
SpectralArrow.doPostHurtEffects builds the effect from this.duration, which this PR now loads and saves; the hit still used a fixed 200.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:10 +01:00
Horizon agentandClaude Opus 5.5 e760415d7b Merge main into #10290
Conflicts kept from both sides: tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:10 +01:00
Horizon agentandClaude Opus 5.5 95f1756a8d Merge main into #10344
Conflicts kept from both sides: crates/horizon-client/src/entity_nbt.rs tools/command_exec_corpus.py tools/test_command_exec_parity.py

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:08 +01:00
Horizon agentandClaude Opus 5.5 e8c56045c9 Merge main into #10291
Conflicts kept from both sides: crates/horizon-client/src/game/commands/data.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:04 +01:00
Horizon agentandClaude Opus 5.5 5ee3b9da9a Run the NoAI nautilus harness without --binary, as parity.py does
parity.py and parity-main call tools/test_*_parity.py without --binary or --work-dir, so the harness failed in 0 s there. Use harness_args.ensure_work_dir, as a98ba0273 did for the dolphin and spell-ticks harnesses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:01 +01:00
Horizon agentandClaude Opus 5.5 307d82bb63 Merge main into #10511
Conflicts kept from both sides: crates/horizon-client/src/mobs/ai.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:41:01 +01:00
Florian ERNSTandCodex 282a7005c8 Fix chorus-flower drops from arrow and snowball impacts
Co-Authored-By: Codex <[email protected]>
2026-11-07 15:40:50 +01:00
Florian ErnstandClaude Opus 5.5 5440872e93 Refuse /summon trees that hold a live entity's UUID
ServerLevel.tryAddFreshEntityWithPassengers checks every UUID of the
summoned tree against the loaded entities before anything joins, and
fails with commands.summon.failed.uuid. summon_with_nbt now walks the
same tree the loaders admit (the root, then the mob riders that
take_mob_passengers takes under mob, boat, minecart and TNT roots) and
refuses it when an entity in the level holds one of its UUIDs.

Duplicate UUIDs inside the new tree are still admitted (vanilla skips
the later one but keeps it under its vehicle's Passengers); the
command harness keeps them as --summon-uuid-internal-diagnostic and
the Game test as a known-unfixed ignored test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:39:15 +01:00
yahya-nusseibehandClaude Opus 5.5 1e38170d49 Play with Friends: refuse a port mapping granted no lifetime
In PCP and NAT-PMP a lifetime of 0 means the mapping is deleted, but a
success reply carrying 0 was taken as a mapping. Mapping::due() was
then always true, so the host renewed on every 100 ms wake-up of its
loop (a UDP exchange each time, under the reach mutex) while
advertising a direct address that no longer existed.

pcp_map and natpmp_map now refuse a zero lifetime when they asked for
one (a delete, which asks for zero, still expects zero), and due()
never fires sooner than 30 s after the last renewal, so a router that
grants a few seconds isn't asked again ten times a second either.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:36:25 +02:00
yahya-nusseibehandClaude Opus 5.5 b0600cec3c Resource packs: cap what one overlay build may unpack
Applying packs builds an on-disk overlay: every file under assets/ in
every zip pack was inflated (up to 1 GiB each, whole in memory) and
written out, on every worker thread at once. A gigabyte of zeros
deflates to about a megabyte, so a server pack at the 250 MiB download
limit could write some 240 GiB to the player's disk and hold a 1 GiB
buffer per thread, once the player accepted the pack prompt. Vanilla
never unpacks a pack.

Zip::copy_to now streams an entry to its file 64 KiB at a time, taking
each piece from a byte budget shared by the whole build: 16 times the
zips' size on disk, at least 64 MiB, at most 8 GiB. Past it the build
fails with FileTooLarge (so the reload drops the server packs, as for
any failed build) and the half-written overlay folder is removed. An
unreadable entry still costs only that file. Memory per thread is now
the compressed entry plus one piece.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 16:31:41 +02:00
Evrim CicekandClaude Opus 5.5 c8753a2ea4 Infested's throw in floats as JOML does; the unit test hurts through Mob::damage
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:31:04 +03:00
MrCloudy2andClaude Opus 5.5 2f3743d282 An axe disabling a shield sends the defender the shield's cooldown
BlocksAttacks.disable adds the item cooldown (ItemCooldowns.addCooldown
on the shield's group), which ServerItemCooldowns sends a player as
Cooldown(minecraft:shield, 100) and ends with 0. Horizon only set its own
shield counter, so a guest's client never heard of it: no cooldown on the
hotbar, and a held right click could show the shield raised while the
server had it down. The disable now also goes through the item cooldowns.

Fixes #11805

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:25:58 +01:00
SenjinandClaude Opus 5.5 381c5a1e62 Shader packs: at_velocity for boats, minecarts, armor stands, frames, paintings, TNT, falling blocks and block entities
They were drawn in runs marked only by type, so a pack's at_velocity read zero on them even
while they or the camera moved (TAA smeared them). Each now marks its own motion run:
(3 << 32) | entity id, (4 << 32) | id for an item frame's held item, (5 << 32) | position for a
block entity, with a closing mark after each group; maps stay unmarked (keyed by map id).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:25:24 +01:00
Evrim CicekandClaude Opus 5.5 3fc5044868 Infested draws at the hurt, before an enderman's or a shulker's own roll
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:21:38 +03:00
Evrim CicekandClaude Opus 5.5 e44fc5af01 mob AI parity: an effect flag, an infested enderman in the rain and a poisoned shulker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:21:38 +03:00
Ludovic Marie 1d63e0a5a3 Merge pull request #11640 from masa-masa-0314/feat/realms-service-fixes
🤖 Agent: Realms subscription URL (subscriptions/$WORLD_ID), JSON-bool settings, InviteCodeList array/result parsing and the JSON Content-Type now match the 26.3 RealmsClient; same Microsoft session, no other auth (Mojang rules OK); footer buttons wired to the selected code. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 15:20:47 +01:00
martipopsandClaude Opus 5.5 8cd24bc4e5 Merge main into controller/guide
Creative draws main's mod browser, then the pad's menu guide over it, as
the inventory does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:15:10 -05:00
itarqos5andClaude Opus 5 6a634e49e2 feat(render): a compact layout for far tiles' quads, a quarter of the arena
Chunk sections already put their solid quads in the arena as one 24-byte
record each instead of four 24-byte vertices (`mesher::packed`). Far tiles
never could: that record's coordinates are a block grid of 16, and a far
tile's quads are merged rectangles of a voxel column's exposed side,
hundreds of blocks wide. So they stayed at four vertices a quad, and at
`lodDistance` 256 they were by far the largest thing the renderer held.

A far quad is much more regular than a chunk's, though -- a rectangle in an
axis-aligned plane, one colour, one light and one layer across all four
corners, with UVs `lod::mesh::emit_quad` derives from the positions -- so a
record of its own holds it in the same 24 bytes, and terrain.wgsl's
`far_corner` does what that function did. The two layouts share the arena
and the pipeline; a draw's base vertex says which it is (`FAR_BASE`).

Measured on a settled LodTest at `lodDistance` 256, render distance 16, the
same binary with `HORIZON_FAR_PACK=0` and without, each run settled on the
same 4424 tiles and the same quad count to within four:

                      far vertices   arena pages   GPU allocated
    four vertices        492.5 MiB       960.0        1241.7 MiB
    a record a quad      127.4 MiB       640.0         921.8 MiB

365 MiB less vertex data, and 320 MiB less video memory actually allocated
rather than merely accounted for. 24.8 bytes a quad: a record each, and the
few quads that spill. Both figures repeated exactly over two pairs of runs.

Frame time is unchanged. Over the same flight, the mean of two pairs:

                      avg FPS   1% low   GPU avg
    four vertices       171.0     95.8    5.390 ms
    a record a quad     170.3     94.9    5.420 ms

The pairs disagree on the sign -- the compact layout was 1.5% slower in one
and 0.7% faster in the other -- so the difference is run variance, not the
cost of expanding a record. Worth stating because one pair alone read as a
regression.

Nothing here reasons about which record is right for a quad: `pack` builds
one, rebuilds the four vertices from it, and keeps it only if they come back
bit for bit. A quad no record reproduces is kept as its four vertices, as
`mesher::packed` keeps one; in practice that is the fluid faces, whose
surface sits at 8/9 of a block, and about 1% of quads.

`lod::mesh`'s `every_quad_the_mesher_emits_packs_and_comes_back` holds the
layout and the mesher to each other over real terrain, which the layout's
own tests cannot: they check its records against its own copy of what
`emit_quad` does, and would pass just as well if the two had drifted.

A same-build control says the picture does not change: two launches of the
identical build differ in 50.7% of pixels (16.0% above the animation noise
floor, worst channel delta 76), because far-tile refinement does not settle
identically between launches. Packed against unpacked differs in 40.5%
(12.8% above the floor, worst 77) -- less than the control.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011paghWkrnyQRBhJRodZpG2
2026-11-07 14:13:34 +00:00
Teiwazik 1eb3b7f6c3 Merge remote-tracking branch 'origin/main' into tmp-seat/follow-plugin-seat-chains 2026-11-07 15:12:38 +01:00
TeiwazikandClaude Opus 5.5 43e106cece Upscaling Sharpness: English texts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:08:56 +01:00
TheMrZZandCodex gpt-6.1-sol ce3a7a7587 Project live boat passengers in command NBT
Reuse the existing runtime tree projection before the live leash writer. Preserve the original full-body diagnostic and add an independently admitted typed-tree command gate.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 15:07:30 +01:00
umcekoandClaude Opus 5.5 1c8113ce32 Merge origin/main into #11344
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:05:06 +03:00
TeiwazikandClaude Opus 5.5 4b5b667560 Upscaling Sharpness: German and Russian texts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:54:09 +01:00
HiuoyandClaude Opus 5.5 f898655ae6 Port pack: RenderTooltipEvent.Color and AnvilRepairEvent under their 26.1 names
NeoForge 26.1 has neither. A tooltip is drawn from a style's textures, picked
in RenderTooltipEvent.Texture, and the anvil take is AnvilCraftEvent, whose
.Pre comes before the inputs are used and the anvil is damaged (no break
chance). `events` now looks the 1.21.1 names up under those, so ATM10's 6
Color and 4 AnvilRepair users are answered by the hooks that exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 17:54:03 +04:00
73fa431ac5 fix: widen entity air and fluid drags from vanilla floats
Vanilla Entity.getAirDrag is 0.98F. PrimedTnt, FallingBlockEntity and
ItemEntity scaled motion by the f64 0.98, so after four ticks a TNT's
Motion x was 0.092236816 instead of 0.09223682318…. Item underwater
and lava drags, LlamaSpit air drag, and hurting-projectile inertia
were the same class of float-widened constants.

Fixes #6814

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 13:53:30 +00:00
Ludovic Marie c78187dfff Merge pull request #11763 from TiggerTakkyTTT/codex/launcher-latest-version
Launcher: make the latest Nightly obvious
2026-11-07 14:49:32 +01:00
MSedgeMC 2f7a46dfd3 port_map: the Camel.canMate row no longer records the canParent gap 2026-11-07 13:48:54 +00:00
MSedgeMC dfe4c2cb51 Test that a wounded or ridden camel cannot breed
Two camels in love in a built world: the breed goal starts, then one loses a
point of health and is refused, and a rider also refuses it. Without the
canParent guard the wounded camel still starts the goal.
2026-11-07 13:48:54 +00:00
MSedgeMC 3ed00f4071 A wounded or ridden camel no longer starts breeding
`Camel.canMate` is `partner instanceof Camel && this.canParent() &&
camel.canParent()`, and `AbstractHorse.canParent` requires no vehicle or
passenger and full health (a camel's `isTamed` is always true). The shared
`Breed` goal applied those checks only to horses, donkeys and llamas, so a
camel stayed free to breed while hurt or carried.

Camels still pair only with camels: the `camel` flag feeds the canParent
guard only, not the partner species rule.
2026-11-07 13:48:54 +00:00
TheMrZZandCodex gpt-6.1-sol e135804a52 fix: match exact local command rotation arithmetic
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 14:46:33 +01:00
MrCloudy2andClaude Opus 5.5 36add718c0 Spears stab other players: the charge and the jab reach players on a server
KineticWeapon.damageEntities and PiercingWeapon.attack hit every entity
along the view, players included (PiercingWeapon.canHitEntity:
canHarmPlayer, not the same vehicle). Horizon's stab targets were the
mobs only. A guest's spear now also reaches the other players of its
level; each stab is worked out as the attacker in their turn and lands
through the PvP path once the hub is back, with the spear's damage type,
the 0.4 push and the weapon's Knockback, the dismount, thorns and the
kill score, then the attacker's wear, stats, exhaustion and hit feedback.

The kill score and thorns of pvp_land move into pvp_aftermath, shared by
both. lan_e2e gains a `spear` check: a jab and a sprinting charge hurt
the host, and with the pvp game rule off neither does.

Fixes #11790

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:42:20 +01:00
MSedgeMC 077164ff2f port_map: the camel RandomSitting row no longer records the pose gap 2026-11-07 13:39:44 +00:00
MSedgeMC c953fe79cb Test that a sitting camel cannot stand under a two-block ceiling
The test builds a world with a stone ceiling cell inside the standing box
but above the sitting box, then asks RandomSitting, the arm the goal
selector calls: 200 asks must all be refused. A second camel with open sky
fires within the same 200, so the refusal is the pose check and not the
state setup. Dropping the can_change_pose conjunct fails the first loop.
2026-11-07 13:39:44 +00:00
MSedgeMC 92282c7476 A sitting camel stands up only when its standing box fits
`CamelAi.RandomSitting.checkExtraStartConditions` ends with
`body.canCamelChangePose()`, which is
`wouldNotSuffocateAtTargetPose(isCamelSitting() ? STANDING : SITTING)`.
RandomSitting did not ask, so a camel sitting under a ceiling that blocks
its 2.375 tall standing box (its sitting pose is 0.945 tall) stood up into
it. The `can_change_pose` helper already existed for the elastic leash pull.
2026-11-07 13:39:43 +00:00
TeiwazikandClaude Opus 5.5 6689a656a0 TAA: clip history to the neighbourhood spread, weigh new frames more under motion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:39:08 +01:00
TeiwazikandClaude Opus 5.5 fc64f0ee27 Upscaling Sharpness: CAS sharpen after TAA upscaling
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:38:31 +01:00
Ludovic Marie fc2868b2b2 Merge pull request #11773 from horizon-rust/agent/recipes-11443-main-merge
🤖 Agent: #11443 by GabrielNichols with main merged (its fork refuses pushes): the Recipes panel keeps its editable search and query when a recipe returns to an empty result, and opens in Creative inventory. The only resolution is THIRD_PARTY_NOTICES, where the pinned JEI paragraph now names the GUI sprites under the MIT LICENSE.txt shipped beside them. Verify and launcher green; clippy -D warnings OK.
2026-11-07 14:37:29 +01:00
TeiwazikandClaude Sonnet 5.5 052c2eb962 Native shader packs: sections fade in toward the fog colour like vanilla terrain
The fade-in rides the high 16 bits of the flat hdr_code varying (no new
location); solid and translucent colours mix toward the fog colour after
the pack's material, alpha kept.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:35:56 +01:00
Florian ERNSTandCodex 1066efbafb Normalize note-block instruments during fill
Co-Authored-By: Codex <[email protected]>
2026-11-07 14:35:22 +01:00
TeiwazikandClaude Sonnet 5.5 4a20af007c Framed maps: no slot taken from a map drawn this frame
The atlas holds 256 map pictures. Every framed map in range claimed one, seen or not, so past 256 the maps took slots from each other within one frame and showed each other's pictures. Now a frame's outline is culled before its map claims a slot, the nearest claim first, and a slot shown this frame is never given away: past the slots, the far maps go undrawn.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:33:28 +01:00
MSedgeMC aadec58092 port_map: the FrogAi.initCoreActivity panic gap is closed 2026-11-07 13:32:24 +00:00
MSedgeMC a90c5f8179 Test the panic speed a hurt frog's move control reads
Drives the real goal selector on a built test world: hurt the frog with a
PANIC_CAUSES damage, tick the goals once, and assert the panic goal started
and left vanilla's 2.0 in `Navigation.speed_modifier`, the value
`SmoothSwimmingMoveControl` multiplies. Reverting the constant fails it with
1.25 against 2.0.
2026-11-07 13:32:24 +00:00
MSedgeMC c1e649f61b Frogs panic at AnimalPanic's 2.0 speed multiplier
`FrogAi.initCoreActivity` builds the brain's core activity with
`AnimalPanic(2.0F)`, but the goal list used 1.25.

`PanicGoal.start` hands the multiplier to `PathNavigation.moveTo`, and
`SmoothSwimmingMoveControl.tick` computes
`speedModifier * getAttributeValue(MOVEMENT_SPEED)` and then multiplies by
0.1 on land or 0.02 in water. A frog's movement speed is 1.0, so a fleeing
frog should move 0.2 blocks a tick on land and 0.04 in water; at 1.25 it
moved 0.125 and 0.025, 60% slower.
2026-11-07 13:32:24 +00:00
TeiwazikandClaude Sonnet 5.5 fdc056c8ac Older servers: send sign_update with the side before the lines
26.3 ends sign_update with the side as a VarInt; 26.2 and older read it as a
boolean right after the position. Forwarding the packet unchanged made the
server misread it and kick the player on Done.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 14:23:33 +01:00
VaspyyyandClaude Opus 5.5 eff292b1e2 Minecarts: keep the rolled loot out of the copies a cart push makes
push_from_ticked_cart cloned the ticked cart into the list and back each tick,
and with it a chest cart's rolled loot: its whole Items list as NBT. The stored
cart already goes without the loot for the tick (tick_carts_now takes it), so
the copies do too and the loot stays with the ticked cart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:20:35 +01:00
Horizon agent 9f6aa5f2f6 Merge origin/main into PR #11682
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
#	crates/horizon-items/src/items.rs
2026-11-07 14:19:54 +01:00
Horizon agent a6b863754b Merge origin/main into PR #11672
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
#	crates/horizon-items/src/items.rs
2026-11-07 14:19:04 +01:00
Horizon agent e414a7a14b Merge origin/main into #11443's series branch
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	THIRD_PARTY_NOTICES.md
2026-11-07 14:17:59 +01:00
Horizon agent 04b8ed64ca Merge origin/main into PR #11625
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/mobs/ai.rs
2026-11-07 14:17:57 +01:00
Horizon agent 1c81831b12 Merge origin/main into PR #11533
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/commands/terrain_cmds.rs
2026-11-07 14:17:31 +01:00
Ludovic Marie 2b8d23af24 Merge pull request #11550 from socksprox/client/feature-gate-online
🤖 Agent: discord, friends and heartbeat become default-on Cargo features (compile gating only, no new network call), so the --no-default-features server build drops 14 crates; main's newer native_client_services invite code is gated too. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 14:16:40 +01:00
Horizon agent 547c7decbc Merge origin/main into PR #11508
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/persist.rs
2026-11-07 14:16:30 +01:00
e8a5564059 Cache decoded saved palette entries in the block registry
A saved area resolves the same dozen block states in every section of every column, but decode_column built a format!-ed String key and parsed each entry through BlockState::from_nbt into a per-column HashMap. The registry now memoizes palette entries by their NBT in an FxHashMap keyed by the tag itself, so a repeated entry costs one hash and one comparison and allocates nothing; the per-entry resolution rules move with it unchanged. Loading the 529-column saved area drops Persistent::load from 1,467.8M to 1,284.3M instructions.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
70a0a29eec Check packed palette indices one word at a time
Loading a generated area spends 16% of Persistent::load (the worker's
chunk load) in PalettedContainer::from_packed, and nearly all of it is
check_indices: a scalar bounds check of every entry of every section
(~10.6 instructions per entry, 22M entries per 529-column area).

check_indices now rejects a whole word at once. Adding 2^bits - palette
to a word lifts every field by that much, so a field carries out of its
own bits exactly when its index is past the palette; the carry out of
each bit is computed for the whole word with (a & b) | ((a | b) & !sum).
A field can only appear bad where a genuinely bad field precedes it in
the same word (a carry adds one to it), which never turns an accepted
input into a rejected one, and the offending word is rescanned entry by
entry on the error path so the reported index is the same first
offending entry as before. The last word's padding fields are biased by
zero, so they cannot look bad, as the entry-by-entry scan ignored them.
The same set of inputs is accepted: the early return when the palette
holds exactly 2^bits entries was already there and stays.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
5cffcf1370 Load saved light directly from its layers instead of expanding and repacking
The saved sky and block nibble layers are already the resident Light::Sky and Light::Block layout. Choose the resident variant from the layers and copy the stored layer when only one kind of light varies, so only sections where both vary still expand their light into a full array. Missing layers still inherit the bottom row above and dimensions without sky light still ignore saved sky layers.

Part of #9996.

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:15:22 +01:00
Ludovic Marie 6560641ba5 Merge pull request #11731 from horizon-rust/fix/wool-vibration-drops
🤖 Agent: harness-only. Kills items and corpses between cases (the drop leak between the cow and item cases), and lists server_wool_vibration as known_flaky for the real-time player timing (1 of 4 on a loaded playhorizon).
2026-11-07 14:12:19 +01:00
Ludovic Marie 295a73028d Merge pull request #11771 from horizon-rust/fix/stab-move-hold
🤖 Agent: exempts only STAB (action 8) from #11223's move hold, so a release still shoots from the position before the move (#8423). lunge_motion: parent 3/3 pass, #11223 3/3 fail, this branch 3/3 pass on playhorizon.
2026-11-07 14:12:15 +01:00
Horizon agentandClaude Opus 5.5 a52b9e48f3 Merge #11366 (main merged) into #11390
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:37 +01:00
Horizon agentandClaude Opus 5.5 21a27ca73e Merge origin/main into #11433 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:29 +01:00
Horizon agentandClaude Opus 5.5 17e7655cdf Merge origin/main into #11366 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:29 +01:00
Horizon agentandClaude Opus 5.5 f8ab91d422 Merge origin/main into #11445 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 6b72630759 Merge origin/main into #11116 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 7cf67fa99d Merge origin/main into #11159 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 c3f5770bb9 Merge origin/main into #11301 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
Horizon agentandClaude Opus 5.5 d4515b62ec Merge origin/main into #11425 (main builds again after #11767)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:10:28 +01:00
TiggerTakkyTTTandCodex ad89bb3123 Launcher: clarify latest nightly version
Show the newest build with a Latest badge, present Nightly dates in a player-friendly title, and separate older builds on the Versions page.

Co-Authored-By: Codex <[email protected]>
2026-11-07 20:10:13 +07:00
MSedgeMC 8c3f88a194 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:35 +00:00
MSedgeMC ac02477732 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:30 +00:00
MSedgeMC 4583362634 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:26 +00:00
MSedgeMC d00cf662b1 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-07 13:09:20 +00:00
MSedgeMC 6deb10741a port_map: client-007's prepareQuadFlat row cites the renamed helper 2026-11-07 13:07:59 +00:00
Horizon agentandClaude Opus 5.5 7770124ea0 Merge #11366's fix into #11390
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:06:02 +01:00
Horizon agentandClaude Opus 5.5 78ab0c3b39 living.rs landing test: MobWorld's level and dimension_type in place of nether/end (main's #10445 test)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:06:02 +01:00
Ludovic Marie b893b6ae31 Merge pull request #11767 from horizon-rust/fix/native-level-play-sound-clash
Fix main build: two native_level_play_sound methods break the mod API generator
2026-11-07 14:05:02 +01:00
Horizon agentandClaude Opus 5.5 7c7d13c458 Merge origin/main into #11390 (through #11366's resolved main merge)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:01:51 +01:00
Horizon agentandClaude Opus 5.5 ed7114b4a0 Merge origin/main into #11366 (spawn_rules_fixture: main's spawn_selection imports, without the Dimension this PR removes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:01:39 +01:00
Horizon agentandClaude Opus 5.5 acbb98aa5c Merge origin/main into #11433 (keep both data.rs test modules)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:00:23 +01:00
Horizon agentandClaude Opus 5.5 385877d1fc Merge origin/main into #11445 (keep both probe fixtures)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:00:13 +01:00
Horizon agentandClaude Opus 5.5 771ccd721f Merge origin/main into #11425 (keep both commands.rs test modules and command_exec imports)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:55 +01:00
Horizon agentandClaude Opus 5.5 5a07a7edfe Merge origin/main into #11301 (keep both data.rs test modules and command_exec groups)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:39 +01:00
Horizon agentandClaude Opus 5.5 b9e272b1ab Merge origin/main into #11116 (keep both native-mods.md paragraphs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:18 +01:00
Horizon agentandClaude Opus 5.5 37cee5b4fd Merge origin/main into #11159 (keep both command_exec corpus groups)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:59:09 +01:00
Ludovic Marie 6416c415a7 Merge pull request #11500 from iseaprojects/i18n/es-es-894
🤖 Agent: es_es now covers all 898 en_us keys (four recipe-transfer strings added after main grew), placeholders checked key by key; the old red verify/launcher was the horizon-checks-1 runner out of disk. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 13:57:23 +01:00
bulkandClaude Opus 5.5 29804afe17 mod API: rename the host-bridge sound and particle helpers that clash with level effects
#7676 and #9222 each added Game::native_level_play_sound and
native_level_send_particles; the mod API generator needs one of each,
so every build on main panics in build.rs. The host-bridge pair becomes
native_play_sound_in_place / native_send_particles_in_place; the names
mods call (play_sound, send_particles) are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:53:23 +00:00
Florian ERNSTandCodex 2688eb16d6 Decode Mob effect amplifiers as unsigned bytes
Co-Authored-By: Codex <[email protected]>
2026-11-07 13:52:45 +01:00
MSedgeMC 8fe6762146 test: compare enchantment durability effects
Add a direct Unbreaking and Mending durability comparison over the real
enchantment registry: item_damage remove_binomial (small and large paths)
and repair_with_xp, with --no-mending reproducing the Unbreaking corpus.
2026-11-07 12:50:41 +00:00
rdbt 0628aa5870 Merge pull request #11403 from TheMrZZ/fix/h065-shrieker-local-capture
Keep the client-level shrieker effect out of shared capture
2026-11-07 12:45:18 +00:00
rdbt fb03dbf6e3 Merge pull request #11402 from ilguerro/fix/w030
Reset the impulse context after an ender pearl teleport
2026-11-07 12:45:06 +00:00
rdbt 915f58a8c5 Merge pull request #11392 from rison1337/perf/cache-bone-rotations
perf(mobs): cache exact bone rotations when TAA is off
2026-11-07 12:44:55 +00:00
rdbt 2557cb185b Merge pull request #11371 from TheMrZZ/fix/saved-max-stack-prototype
Omit prototype max-stack patches in saved ItemEntity stacks
2026-11-07 12:44:44 +00:00
rdbt db2bc09336 Merge pull request #11370 from MSedgeMC/test/brewing
brewing: every recipe's mix, slot filters and the stand tick
2026-11-07 12:44:33 +00:00
rdbt 23af2bfc62 Merge pull request #11361 from MSedgeMC/test/firework-edges
firework_tooltip: every flight duration, long explosion runs and both components
2026-11-07 12:44:22 +00:00
TeiwazikandClaude Sonnet 5.5 ff28824d42 Riding: boat riders keep their own seat
position_riders re-seated every mob whose vehicle is not a mob through
passenger_riding_position, which puts every boat rider in seat 0 from
the boat's position. seat_boat_mobs had already placed them, so remote
players on a moving boat jumped between the two seats each pass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 13:44:21 +01:00
rdbt f6980a83e6 Merge pull request #11360 from AlfaRojo/fix/spectator-food-and-regen
A spectator in loaded chunks keeps its food and regeneration ticks
2026-11-07 12:44:11 +00:00
bulkandClaude Sonnet 5.5 a0c5f26cc2 Far ring brake: pull in over 50 ms, grow back under 45 ms
The ring got stuck at its smallest on servers whose normal tick sits
between 25 and 40 ms, since it only grew back under 25 ms.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 12:43:37 +00:00
rdbt dd64878f38 Merge pull request #11296 from TheMrZZ/fix/mob-command-stack-retention
Preserve standard mob equipment stacks in item commands
2026-11-07 12:43:23 +00:00
TeiwazikandClaude Sonnet 5.5 45978aaf67 Seat test: import Vec3 and play packets
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 13:43:19 +01:00
rdbt f0e0d96b61 Merge pull request #11258 from TheMrZZ/fix/loot-empty-result-positions
Preserve empty loot result positions for command replacement
2026-11-07 12:43:11 +00:00
rdbt 45cc5a91e0 Merge pull request #11253 from TheMrZZ/fix/h189-data-literal-snbt
Preserve modern SNBT literals during data execution
2026-11-07 12:43:00 +00:00
rdbt 132aa6b682 Merge pull request #11133 from Ragbie/fix/enderman-hurt-teleport-now
An enderman hurt by the game layer teleports at the hurt, as vanilla's does
2026-11-07 12:42:42 +00:00
rdbt fcc57182ae Merge pull request #11122 from Delete228/mod-data-load-in-place-upstream
Preserve live mod block entities when loading command data
2026-11-07 12:42:31 +00:00
rdbt fcc6416b34 Merge pull request #11121 from TheMrZZ/fix/h090-tnt-fuse
Decode primed TNT fuse through numeric short conversion
2026-11-07 12:42:17 +00:00
rdbt c85011b04d Merge pull request #10570 from Detimes/perf-nbt-ascii-strings
NBT: read and write one-byte strings whole
2026-11-07 12:41:58 +00:00
rdbt d93bf565f4 Merge pull request #10531 from PzmuV1517/shaders/perf-parallel
Shader packs: a pack's pipelines compile together on several threads
2026-11-07 12:41:47 +00:00
rdbt 9c5a3734ea Merge pull request #10506 from voidsipp/fix/round3-1-wet-fire-order
Apply burning damage before extinguishing wet mobs
2026-11-07 12:41:36 +00:00
rdbt 2ff274b96c Merge pull request #10493 from voidsipp/fix/mob-water-breathing
Respect breathing effects and bubble columns for mobs
2026-11-07 12:41:25 +00:00
rdbt 13d03ec87f Merge pull request #10384 from voidsipp/fix/10044-forced-respawn
Validate forced Overworld respawn points
2026-11-07 12:41:14 +00:00
rdbt 4ea806a2db Merge pull request #10383 from voidsipp/fix/10369-map-x-offscreen
Keep treasure map targets when carrying undecorated copies
2026-11-07 12:41:08 +00:00
martipopsandClaude Opus 5.5 303255f2d8 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:40:51 -05:00
martipopsandClaude Opus 5.5 a6339e67b5 Merge main into controller/guide
Brings in #11026 and #11534. docs/controllers.md keeps the guide's slider
hold beside main's server-list bumpers and Playing in the background.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:38:09 -05:00
rdbt 1cd80a0bcb Merge pull request #11757 from kacpherek/perf/render-api-tracking
an edit in game/ no longer rebuilds the renderer: dev rebuild 22.7 s -> 12.0 s
2026-11-07 12:37:35 +00:00
rdbt 4ebee0d01e Merge pull request #11218 from oxi-does-dev/feat/launcher-nord
Launcher: Nord theme and the theme picker in the Blocks look
2026-11-07 12:36:10 +00:00
rdbt 0ebe0d40de Merge pull request #11135 from SouceCalve/main
Adding support for installing from PrismLauncher-portable
2026-11-07 12:35:57 +00:00
rdbt 30ee97bbc7 Merge pull request #7690 from levzzz5154/modapi/entity-moves
Native mods: teleport, rotation, head yaw, facing, size and pushes through EntityRef (#7387)
2026-11-07 12:35:28 +00:00
rdbt 2a57a30ae2 Merge pull request #7685 from levzzz5154/modapi/entity-state
Native mods: an entity's state flags through EntityRef (#7387)
2026-11-07 12:35:16 +00:00
rdbt b3f9eddfd3 Merge pull request #7676 from levzzz5154/modapi/level-effects
Native mods: sounds and particles in a level through LevelRef (#7387)
2026-11-07 12:35:04 +00:00
rdbt f5a6eb9835 Merge pull request #7564 from levzzz5154/modapi/level-world
Native mods: a level's seed, difficulty, game rules and weather through LevelRef (#7387)
2026-11-07 12:34:52 +00:00
rdbt dd3f458ba1 Merge pull request #11303 from UMCEKO/dims/world-facts
Block behaviour reads the dimension type, not the dimension (#11282, 2/9)
2026-11-07 12:34:39 +00:00
rdbt d4006f0770 Merge pull request #11415 from UMCEKO/dims/dimension-facts
Dimension type facts read instead of matching the level (#11282, 1b/9)
2026-11-07 12:34:27 +00:00
rdbt bed953b207 Merge pull request #11291 from UMCEKO/dims/level-key
LevelKey: a level's key for any dimension, not three variants (#11282, 1a/9)
2026-11-07 12:34:11 +00:00
Horizon agentandClaude Opus 5.5 0e674b827c lan hub: a STAB keeps the move behind it in its tick
#11223 made every PlayerAction hold the move queued behind it for the next
tick. For a spear's STAB that moved the jump the move carries
(handlePlayerPositionChange's jumpFromGround) a tick after the Lunge's
impulse, and the server took two moves in the next tick: lunge_motion has
failed every jump phase on parity-main since. Releases, drops and digs keep
the hold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:34:01 +01:00
rdbt 2f0b066f38 Merge pull request #11026 from martipops/fix/pad-read
Controllers: fixes from reviewing the pad reader, typing and joining
2026-11-07 12:34:00 +00:00
rdbt 7d8bb9e69b Merge pull request #11645 from vegetableovosh/fix/11214-chunk-refresh
Fix/11214 chunk refresh
2026-11-07 12:33:47 +00:00
rdbt dd3c1d17a2 Merge pull request #11756 from TheMrZZ/fix/item-display-default-transformation
Save item-display transformations from modeled state
2026-11-07 12:33:35 +00:00
rdbt d5f43e0967 Merge pull request #11256 from MSedgeMC/test/transformer-use
use_item_on: roots on the clicked face, and the copper chest's other half
2026-11-07 12:33:23 +00:00
rdbt d372562a73 Merge pull request #11744 from yahya-nusseibeh/fix/launcher-account-key-salt
Seal the launcher's secrets file with a per-install secret
2026-11-07 12:33:11 +00:00
rdbt 99ddbc629e Merge pull request #11739 from yahya-nusseibeh/fix/relay-per-address-limits
Relay: a cap on open connections per address, and a per-site join budget
2026-11-07 12:32:59 +00:00
rdbt 37f69234e7 Merge pull request #11707 from doodlyyousuf/251106-fix-mob-equip-vibration
fix: mob saddle and gear post equip vibrations at full volume
2026-11-07 12:32:47 +00:00
rdbt 8fd21e02d4 Merge pull request #11746 from NongTham/fix/advancement-parent-normalization
Advancements: normalize parent and lookup IDs with full resource locations (#11653)
2026-11-07 12:32:36 +00:00
rdbt 32191d5b6a Merge pull request #11745 from yahya-nusseibeh/fix/deps-tokio-lru
Update tokio to 1.53.3 and mysql to 28 (lru 0.18) for advisories
2026-11-07 12:32:24 +00:00
rdbt 8331bedf2b Merge pull request #11725 from MSedgeMC/test/spawn-obstruction
test: compare mob spawn obstruction
2026-11-07 12:32:13 +00:00
rdbt 37656cc775 Merge pull request #11738 from yahya-nusseibeh/fix/shader-pack-bounded-reads
Shader packs: bound what one zipped file may unpack to
2026-11-07 12:32:01 +00:00
rdbt ae0ccc199c Merge pull request #11737 from yahya-nusseibeh/fix/hshader-running-total
Shader packages: count the 8 MiB total while reading, not after
2026-11-07 12:31:50 +00:00
rdbt 4c1d1990e1 Merge pull request #11736 from yahya-nusseibeh/fix/report-gh-refs
Website bug reports: GH-123 cross-references nothing either
2026-11-07 12:31:38 +00:00
rdbt bc0ddfbff0 Merge pull request #11529 from horizon-rust/fix/followup-10571
Text displays: animated text is never cached, and a culled one keeps its glyphs
2026-11-07 12:31:27 +00:00
rdbt 330d3db811 Merge pull request #11089 from CocofireHD/docs/perf-comparison-readme
README: a Performance section from the clean Horizon vs vanilla runs
2026-11-07 12:31:22 +00:00
rdbt a7a2cf9305 Merge pull request #11749 from M0-kii/fix/windows-launcher-bin-tests
Run launcher binary tests in the Windows PR checker
2026-11-07 12:30:56 +00:00
rdbt 1087b5a6ff Merge pull request #11724 from horizon-rust/ci/runner-gc-busy-versioned-bin
ci: runner-gc sees jobs after a runner self-update
2026-11-07 12:30:51 +00:00
24fa04d753 fix: drop mob XP after the guest who hurt it has left
Vanilla LivingEntity.dropExperience and Mob.dropCustomDeathLoot key on
lastHurtByPlayerMemoryTime, not whether that player is still in the
world. Horizon folded both into Death.player, so a guest who left
within the 100-tick memory made drops::die skip experience and
equipment.

Give die a player_killed flag from the timer. LAST_DAMAGE_PLAYER stays
None when killer_uuid cannot resolve them.

Fixes #11674

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 12:29:23 +00:00
martipopsandClaude Opus 5.5 fb3e73d3e2 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:26:09 -05:00
Ludovic Marie 951fc0f0cb Merge pull request #11534 from martipops/settings/shell-api
🤖 Agent: Extras moves onto the new settings_shell (one row per setting, search, description strip), same items and saves; design-system.md now lists Extras with Graphics/Controller as Blocks-frame pages; size (+1748) accepted because ~860 lines are the shell the next pages reuse (#11487 plan). (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 13:24:55 +01:00
Mayonesu0 1429b27098 Merge remote-tracking branch 'origin/main' into perf/launcher-write-data-file 2026-11-07 13:20:05 +01:00
Horizon agent 2f88719668 Merge remote-tracking branch 'origin/main' into fix/wool-vibration-drops 2026-11-07 13:19:20 +01:00
Ludovic Marie 25a1add0a2 Merge pull request #10208 from ilguerro/fix/entity-die-non-mobs
🤖 Agent: Requeued after merging main twice (lan_e2e.py keep-both: this PR's entity-die check beside main's new mob-thorns check; then #11735's clippy/lock fix); verify green. Breaking a painting, cushion, boat, minecart, frame or armor stand emits ENTITY_DIE like vanilla's kill path, and its sculk/vibration harnesses passed on the earlier head.
2026-11-07 13:16:57 +01:00
DetimesandClaude Opus 5.5 c78882c7da Older clients: attributes and effects in the client's numbering
1.21.2 dropped the generic./player./zombie. attribute prefixes, so the id tables of 1.21.1 and 1.20.6 (matched by
name) had no attribute runs and every update_attributes value was left out for those clients. They are regenerated
with the renames. Mob effect ids were never renumbered: update_mob_effect and remove_mob_effect now go through the
client's table, and an effect added after its release is not sent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:16:51 +00:00
Horizon agentandClaude Opus 5.5 4e1c0d68be Merge main into #10384 (main's clippy and launcher lockfile fixes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:16:21 +01:00
martipopsandClaude Opus 5.5 7753a08ca9 Merge main into controller/guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:15:00 -05:00
Florian ERNSTandCodex 34a9bfaa88 Save item-display transformations from modeled state
Co-Authored-By: Codex <[email protected]>
2026-11-07 13:11:42 +01:00
Horizon agentandClaude Opus 5.5 40dd7109bb Merge origin/main into random-source
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:09:37 +01:00
Horizon agentandClaude Opus 5.5 3cbe777d44 Merge origin/main into fix/set-custom-model-data
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:08:36 +01:00
Horizon agentandClaude Opus 5.5 1db1b34dcb Merge origin/main into perf/11126-structure-starts-jigsaw
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:08:04 +01:00
Horizon agentandClaude Opus 5.5 5fdf9fb0ac Merge origin/main into fix/11214-chunk-refresh
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:07:40 +01:00
Horizon agentandClaude Opus 5.5 6e19f95786 Merge origin/main into feat/realms-service-fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:07:15 +01:00
Horizon agentandClaude Opus 5.5 d30b514728 Merge origin/main into fix/old-stats-file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:06:46 +01:00
Kacpherek cf2ab15de3 Build the mod API generator and syn optimized: its build script parses all of game/ on every gameplay edit 2026-11-07 13:06:14 +01:00
M0-kii 68ec463175 Run launcher binary tests in the Windows PR checker
Keep --bins in the Windows launcher test command so binary-owned tests
are checked alongside the library, matching the planned validation.

Fixes #11747
2026-11-07 15:35:53 +03:30
DetimesandClaude Opus 5.5 6993f213b6 step_1_21_1: drop the import the explode rewrite no longer uses
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:05:41 +00:00
Horizon agentandClaude Opus 5.5 35d60b45b5 Merge origin/main into client/feature-gate-online
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:04:55 +01:00
Horizon agentandClaude Opus 5.5 a4df22049c Merge origin/main into settings/shell-api
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:04:01 +01:00
Horizon agentandClaude Opus 5.5 4bf0560d40 Merge origin/main into i18n/es-es-894
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:03:34 +01:00
Horizon agentandClaude Opus 5.5 511771b9f9 Merge origin/main into fix/11317-block-crack-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:03:03 +01:00
Horizon agentandClaude Opus 5.5 344382682e Merge origin/main into fix/11314-player-food-predicate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:02:36 +01:00
Horizon agentandClaude Opus 5.5 28299018c7 Merge origin/main into fix/vindicator-johnny
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 13:02:07 +01:00
Ludovic Marie 18840db914 Merge pull request #11451 from MSedgeMC/fix/armadillo-state-save
🤖 Agent: An armadillo's state is saved always and restored through switchToState on load, as Armadillo.add/readAdditionalSaveData do.
2026-11-07 12:59:39 +01:00
Detimes bb22bf6570 Merge remote-tracking branch 'upstream/main' into fix-11722-old-pick-item 2026-11-07 11:58:27 +00:00
Detimes edef044a68 Merge remote-tracking branch 'upstream/main' into fix-11721-explode-radius 2026-11-07 11:58:27 +00:00
DetimesandClaude Opus 5.5 c7f54b7461 Older clients: a 1.21.3 client's pick-item swaps the slot into its hand
1.21.3 and older clients send pick_item with the inventory slot that holds the picked block's item; 26.3 has no
such packet and the adapter dropped it. It now becomes a container_click swap of that slot with the held hotbar
slot (tracked from set_carried_item and set_held_slot), state id 0 so the server resends the inventory.

Fixes #11722

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:58:27 +00:00
DetimesandClaude Opus 5.5 47fd58c339 Older clients: real explosion radius for 1.21.1, explosions no longer fail for 1.21.8 and older
The 1.21.8 and 1.21.1 explode steps re-read a payload the renumbering had already put in the client's ids with
the pinned codec, so the particle's layout was chosen by the wrong id and sending any explosion failed. They now
read and write through the client's ids. 1.21.1's step read the knockback as three doubles where 1.21.4's step
leaves a double and two floats. 1.21.1's strength is 26.3's radius instead of a fixed 3.0.

Fixes #11721

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:54:53 +00:00
Horizon agent 88a75f983b Merge remote-tracking branch 'origin/main' into cg65v-10210 2026-11-07 12:54:24 +01:00
Horizon agent 4c4728ead5 Merge remote-tracking branch 'origin/main' into cg65v-10208 2026-11-07 12:54:16 +01:00
Ludovic Marie ea55e8156c Merge pull request #11735 from MSedgeMC/fix/clippy-is-multiple-of
🤖 Agent: Unbreaks main: clippy 1.99's manual_is_multiple_of in horizon-core (from #10553) and the socket2 0.5.10 entry missing from both lockfiles under --locked; both workspaces' cargo metadata --locked and clippy -p horizon-core -D warnings pass on this head.
2026-11-07 12:53:19 +01:00
Horizon agentandClaude Opus 5.5 320d55a88c launcher: the socket2 0.5.10 entry its lock was missing
Same break as the workspace lock: #11464 and #11569 left "socket2 0.5.10"
referenced with no package entry, so main's launcher build fails under
--locked. Regenerated with cargo metadata; nothing else moves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:51:04 +01:00
MSedgeMC 3b7dde0cd5 horizon-core: use is_multiple_of for the stride check
clippy::manual_is_multiple_of is in the stable toolchain now, and the workspace clippy step runs with -D warnings,
so main's own horizon-core fails verify before any PR's code is compiled.
2026-11-07 11:48:18 +00:00
NongThamandAntigravity <gemini> 4df3bd4db0 Advancements: normalize parent and lookup IDs with full resource locations (#11653)
Ensure advancement parent references and command lookups handle both bare
and namespaced identifiers, preventing orphaned advancement nodes and
matching vanilla's progression count on '/advancement grant @s everything'.

Co-Authored-By: Antigravity <gemini> <[email protected]>
2026-11-07 18:45:55 +07:00
Horizon agent dbd5775947 Merge remote-tracking branch 'origin/main' into cg65v-10210
# Conflicts:
#	crates/horizon-client/src/game/command_harness.rs
#	mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java
#	tools/test_command_exec_parity.py
2026-11-07 12:39:25 +01:00
TheMrZZandCodex gpt-6.1-sol 7e8c7a27c1 Use effective stack prototypes for component presence
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 12:38:41 +01:00
Mayonesu0andClaude Opus 5.5 199e8193fd Launcher: write a prepared version's data file after installing it
A version's first start read its thousands of small files one by one, about
100 s under Windows Defender here, because only the game wrote
horizon-data.bin, and only after that slow start. The launcher now writes it
in the background once an install or import has its resources, reading on up
to 8 threads (43 s against 281 s on one, on a fresh copy); the start then
loads in 1.2 s. The game stays the fallback, on its one idle thread.

A reused version no longer copies the data file, which names its own root.
complete_assets removes the file when it adds something, as the file answers
missing for what it lacks. A write removes what a stopped writer left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:38:08 +01:00
Horizon agent f16cc3d6d0 Merge remote-tracking branch 'origin/main' into cg65v-10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 12:37:32 +01:00
Ludovic Marie 0000cd3e7d Merge pull request #10538 from arslanarm/shaderpacks/end-fog-sky-fan
🤖 Agent: Requeued at the author's main merge 552f2c1b4: same net patch as d8676fff0 (identical patch-id), whose parity failed only command_exec's 26 /say lines that #11687 has since fixed on main. The change: shader packs in the End draw the sky fan under the dragon fight's fog like the rest of the pack's sky.
2026-11-07 12:37:21 +01:00
Ludovic Marie 2303a445fe Merge pull request #11482 from DUzzL/fix/11298-elytra-glide-refused
🤖 Agent: a refused START_FALL_FLYING now flips the glide bit of sent_shared so a flags update goes out, as LivingEntity.stopFallFlying's set(true)+set(false) dirties the flag in vanilla (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:34:18 +01:00
Ludovic Marie 6c53f16dbf Merge pull request #11591 from rdbtCVS/launcher-notes-in-versions
🤖 Agent: the Patch notes tab's three orphan strings are gone from the 86 lang files, so the launcher's i18n tests pass (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:31:28 +01:00
martipopsandClaude Opus 5.5 9495790d0b Merge main into controller/guide
main hides the HUD in Replay Studio; the guide's guide_from stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:31:23 -05:00
Ludovic Marie ee8a79e80e Merge pull request #11706 from Bioneisme/fix/damage-type-command-ids
🤖 Agent: /damage ids now come from the world's damage catalog and are replaced on each world open, so a pack's type doesn't leak into the next world (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:08 +01:00
Ludovic Marie f98c9b1f74 Merge pull request #11697 from mvanhorn/fix/6763-memory-first-view-chunk-batch
🤖 Agent: the single-player memory connection sends every ready view column in one batch before PlayerLoaded (about 0.5 s saved at render distance 12), then the 64-per-tick cap comes back; socket guests keep vanilla pacing. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:04 +01:00
Ludovic Marie 7524ec7664 Merge pull request #11667 from Cx188/fix/11212-chat-suggestion-callback
🤖 Agent: new client-only, read-only native hook chat_suggestion_applied fires after Tab or a click applies a suggestion, with the replaced range in UTF-16 units of the list's original input; fragment stamps cleanly on main. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:30:01 +01:00
Ludovic Marie 1bb63d1e2a Merge pull request #11647 from Hiuoy/feat/port-pack-events-data
🤖 Agent: port-pack events answers VillagerTradesEvent/WandererTradesEvent with the trade_set and villager_trade registries 26.x moved trades to. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:57 +01:00
Ludovic Marie b85e0f4f67 Merge pull request #11635 from MSedgeMC/port_map/dead-row-verdicts
🤖 Agent: Three ledger rows corrected (destroyAndAck's ack, RateKickingConnection via PacketRate::received, shouldRun as skip:other); checked PacketRate::received exists on main. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:53 +01:00
Ludovic Marie 35b963c3b8 Merge pull request #11547 from abit-foggy/codex/studio-b1-playback
🤖 Agent: Studio playback of .mcpr recordings through the normal client handlers (B1 slice of #9712, +2120), only active when a recording is opened; F1 now hides chat and tab list as vanilla Hud.extractRenderState does. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:50 +01:00
Ludovic Marie e43c075351 Merge pull request #11531 from vnsmoreira/packs/polytone-reader
🤖 Agent: new horizon-polytone crate reads Polytone targets and biome/dimension attribute layers from the public wiki only (Polytone is GPL, its source not read); no caller yet, first step of #11486. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:46 +01:00
Ludovic Marie da21244ed0 Merge pull request #11504 from voidsipp/fix/wind-burst-block-hook
🤖 Agent: the hook runs after the vanilla shuffle so RNG draws are unchanged and a no-mod dispatch is an empty loop; fragment wind_burst passes tests.test_stamp_native_hooks and --check --only on main merged in (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:42 +01:00
Ludovic Marie 2617902218 Merge pull request #11499 from martipops/controller/studio-reach
🤖 Agent: Studio rows and buttons now register as focus targets and the wheel moves one whole 40 px row per notch, which also stops part-scrolled rows drawing over the title and makes the last recording reachable. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:39 +01:00
Ludovic Marie bd5dc3ac4a Merge pull request #11496 from RINNECODER/up/port-chain-models
🤖 Agent: only block/ and item/ paths equal to the old id are renamed, so a mod's own pulley:block/chain and chain_command_block_* stay, and files are rewritten only when something changed (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:36 +01:00
Ludovic Marie 471cdcf2db Merge pull request #11495 from RINNECODER/up/recipe-suggest
🤖 Agent: recipe suggestions reuse json_ids_of_every_namespace like /loot, and the dead ids_under goes (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:32 +01:00
Ludovic Marie 6ffbbfc44c Merge pull request #11479 from k3yh/log-unrunnable-features
🤖 Agent: The unsupported placed features are now named in the log (capped at 20), which is what a pack author needs to fix them. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:29 +01:00
Ludovic Marie c0e74b2b53 Merge pull request #11475 from rison1337/perf/cull-offscreen-flames-taa
🤖 Agent: under TAA the flame gate now uses a frustum widened by one rendered pixel per side (jitter is at most half a pixel: 2× margin), one extra frustum per frame, shader packs still bypass. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:25 +01:00
Ludovic Marie d7cc28f952 Merge pull request #11468 from rison1337/perf/lazy-dropped-item-bounds
🤖 Agent: with entity culling off the decision is Bypass before any bounds are read, so the posed-envelope walk over every model point of every copy is now skipped (TAA condition dropped), RNG order of cluster_poses pinned by a test. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:22 +01:00
Ludovic Marie 16b511ed65 Merge pull request #11464 from TaquitoSuabe/perf/net-keepalive-pooling
🤖 Agent: TCP keepalive (10 s + 3×3 s) now ends dead friends tunnels and relay bridges in ~19 s instead of holding them until the 120 s idle; the stack buffers replace one Vec per connection, not per read. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
2026-11-07 12:29:18 +01:00
Horizon agentandClaude Opus 5.5 301765dc69 List server_wool_vibration as known flaky: its player cases are paced by the wall clock
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:28:35 +01:00
TheMrZZandCodex gpt-6.1-sol ac4859eab4 fix: match command damage-only item predicate ranges
Evaluate the supported damage-only subpredicate against modeled damage and
existing effective component presence. Preserve removal precedence and leave
unsupported conjunctions on their existing path.

Fixes #11715

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 12:23:52 +01:00
Florian ERNSTandCodex 087c763432 Normalize summoned leash knots and check their fence support
Co-Authored-By: Codex <[email protected]>
2026-11-07 12:23:27 +01:00
MSedgeMC 0748416e9e Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 11:22:23 +00:00
MSedgeMC f64b1b9d55 Add the who_lookup seam to living.rs's test fixture 2026-11-07 11:22:07 +00:00
Kacpherek 59dff021a1 Don't rebuild the renderer on every gameplay edit: its mod API reads only native_hooks.rs 2026-11-07 12:14:32 +01:00
MSedgeMC 1748b489eb Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 11:08:58 +00:00
Ludovic Marie 98b8827123 Merge pull request #10553 from ilguerro/perf/9996-sections
🤖 Agent: Chunk-section block writes now do one position lookup instead of two on the worldgen hot path; output unchanged, and CI parity on main+head (worldgen, structures, light and the save harnesses it selects) passes in full.
2026-11-07 12:07:04 +01:00
Horizon agentandClaude Opus 5.5 ba61774567 server_wool_vibration: let the killed cow's corpse go before the arena changes
A /kill-ed cow stays 20 ticks (LivingEntity.tickDeath) and still moves;
the fill that removes the carpet or slab under it drops it onto the floor
while the next case measures. item-slab read power 2 before its summon on
vanilla's side in a playhorizon run. Wait 25 server ticks after the kills.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 12:02:52 +01:00
vegetableandOpenAI Codex 5eeb4d5678 Keep interpolation compatible with the MSRV
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 16:01:42 +05:00
Horizon agentandClaude Opus 5.5 a5129be064 Studio playback: its Remote takes main's recipe_transfer (off)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:55:01 +01:00
Horizon agentandClaude Opus 5.5 7b03ce49c7 Merge origin/main into codex/studio-b1-playback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:54:51 +01:00
Horizon agentandClaude Opus 5.5 5a65831d4a Merge #11483's head (main with the clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:46 +01:00
Horizon agentandClaude Opus 5.5 4f2f4078d1 ci: see runner jobs after a runner self-update; keep the Mac's disk free
GitHub's runner moved its binaries to bin.<version>/ after a self-update, so
`pgrep -f "$runner/bin/Runner.Worker"` never matched: runner-gc took busy
runners for idle and deleted the probe builds and old prepared resources of
two running parity jobs (2026-11-07). Match bin*/ in runner-gc,
checks-runners.sh and linux-runner.sh.

runner-gc also counts shared-local/parity-target (19-24 GB per runner),
recreates it empty after removing it (a dangling .local link failed every
harness), and drops harness folders over 2 GB untouched for 6 hours.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:10 +01:00
Horizon agentandClaude Opus 5.5 64f9bb1a61 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:53:08 +01:00
Horizon agentandClaude Opus 5.5 991983679c Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:52:41 +01:00
Florian ERNSTandCodex c2b59b6f99 Dispatch ordinary arrow shoot events on their first tick
Co-Authored-By: Codex <[email protected]>
2026-11-07 11:52:23 +01:00
Horizon agentandClaude Opus 5.5 08f2880b2a Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:52:14 +01:00
Horizon agentandClaude Opus 5.5 b844b69826 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:51:48 +01:00
Horizon agentandClaude Opus 5.5 03d1f86f4a Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:50:56 +01:00
Horizon agentandClaude Opus 5.5 c2386a4616 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:50:06 +01:00
Horizon agentandClaude Opus 5.5 c524b40874 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:49:40 +01:00
Horizon agentandClaude Opus 5.5 d53411a24e Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:49:15 +01:00
Horizon agentandClaude Opus 5.5 db503217e3 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:48:22 +01:00
Horizon agentandClaude Opus 5.5 65d3e70f5d Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:47:54 +01:00
Horizon agentandClaude Opus 5.5 dddf61b000 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:47:28 +01:00
MSedgeMC 6ac83b92d7 test: compare mob spawn obstruction
Extend the spawn rule suite with Mob.checkSpawnObstruction: real entities
against water, lava, an obstructed level and Ocelot's special base, with
--no-obstruction reproducing the previous corpus.
2026-11-07 10:46:37 +00:00
Horizon agentandClaude Opus 5.5 48986d7fd8 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:46:34 +01:00
Horizon agentandClaude Opus 5.5 14428f3233 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:46:07 +01:00
exampleandClaude Opus 5.5 7746c0ef78 Merge main: picks up #11716's lint fix for verify
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:49 +01:00
Horizon agentandClaude Opus 5.5 fbbe20dc9c Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:40 +01:00
Horizon agentandClaude Opus 5.5 b9928751e4 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:45:12 +01:00
Horizon agentandClaude Opus 5.5 ed98ebc8a1 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:44:46 +01:00
TeiwazikandClaude Sonnet 5.5 55a54edebe Riding: follow plugin seat chains through invisible vehicles
Plugins seat players on text displays, interactions, armor stands or area
effect clouds that ride another player. Resolve the seat through the whole
chain of vehicles, keep the packet riding graph, move the local rider with a
moving seat, and use the Avatar attachment height for player vehicles.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 11:44:41 +01:00
Horizon agentandClaude Opus 5.5 4a697d3689 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:44:19 +01:00
Horizon agentandClaude Opus 5.5 c0ca0cce68 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:51 +01:00
Horizon agentandClaude Opus 5.5 984f2d9f06 Merge main (clippy as_chunks lint fix)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:23 +01:00
Horizon agentandClaude Opus 5.5 7fcc1c900a server_wool_vibration: clear the previous cow's drops between cases
The arena's /kill @e[type=!player] kills the previous case's cow, and the
cow drops beef and leather as it dies, after the selector was evaluated.
Those items fall onto the next arena and can hit the floor while the next
case measures, at a wall-clock moment: parity-main 42359055402 read power
2 before item-stone on vanilla's side, and playhorizon read 2 after
item-carpet in 1 of 3 runs at 67902962fd. Both cases follow a cow case.
A second /kill @e[type=item] removes the drops; setup lines aren't compared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:43:17 +01:00
Horizon agentandClaude Opus 5.5 3ed97c7339 Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:39:07 +01:00
Riccardo GuerrieroandClaude Opus 5.5 3f1b851c07 Merge origin/main into #11400 (main's 1.95 lint fix for interpolation.rs)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:37:51 +01:00
Horizon agentandClaude Opus 5.5 aa7516532a Merge main; drop run logs and the unrun diagnostic
tools/test_warden_memory_full_diagnostic.py is not a test_*_parity.py harness, so nothing runs it, and it is 816/839 by design. The canonical owned-memories harness stays. Drops the docs/parity run logs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:37:36 +01:00
Horizon agentandClaude Opus 5.5 baab15d42d Fail location_check on unloaded counterparts
Vanilla's LocationPredicate checks Level.isLoaded before the block predicate, so an offset into an unloaded chunk fails instead of holding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:35:59 +01:00
Horizon agentandClaude Opus 5.5 71c7dfaf97 Merge main; fail location_check on unloaded counterparts
Vanilla's LocationPredicate checks Level.isLoaded before the block predicate, so an offset into an unloaded chunk fails instead of holding. Drops the docs/parity run logs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:35:26 +01:00
Ludovic Marie fd06d1c265 Merge pull request #11716 from horizon-rust/agent/cg70-one-rust-version
🤖 Agent: fixes main's red clippy since #11561 (1.95 lints on interpolation.rs: is_multiple_of on non-zero unsigned divisors, as_chunks_mut::<4> quads); workspace clippy clean on main + this.
2026-11-07 11:34:53 +01:00
Riccardo GuerrieroandClaude Opus 5.5 c9f2efa210 A player's Thorns hurts the mob as the player's thorns hit
Thorns on the player answered a mob's hit with generic damage that had no
attacker, so the mob was not knocked back, did not turn on the player and
dropped no experience when it died of it. It now hurts the mob by
`minecraft:thorns` damage the player caused (`DamageEntity` with the
wearer as owner), rolled from the mob's own random, and the held weapon's
Breach applies as on any hit by the player.

The whole equipment counts, in `EquipmentSlot` order: the main hand, the
off hand, then feet up to head (Thorns' slots are `any`), so a Thorns
piece held in a hand answers too. Each piece rolls its chance, hurts the
attacker and wears in that order, also when the attacker is already dead.
A piece that takes no wear, or a creative player's, is left alone.

A mob hit by thorns damage plays `enchant.thorns.hit` where it stands
when the hit lands whole, silent or not (`playSecondaryHurtSound`).

Part of #9954.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:34:28 +01:00
Horizon agentandClaude Opus 5.5 43c987818b Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:52 +01:00
Horizon agentandClaude Opus 5.5 a94924cd72 Merge origin/main into fix/loot-kill-sheep-state
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:52 +01:00
Horizon agentandClaude Opus 5.5 484a9d0ae8 Apply the 1.95 lints to main's new interpolation code
is_multiple_of on a usize cell size, and as_chunks_mut::<4> for the four
columns' quads (each quad is now a [f32; 4]).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:32:14 +01:00
Horizon agentandClaude Opus 5.5 fa9500a04d Stop pinning camel temper to variant3 in the pose test
The pose test asserted that Temper lands in the legacy variant3 byte. #11483/#11659 move every equine's temper to the signed equine_temper field; the assertion was outside this test's subject (LastPoseTick), so drop it rather than pin the storage.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:49 +01:00
Horizon agentandClaude Opus 5.5 81a719f832 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:20 +01:00
Horizon agentandClaude Opus 5.5 029358df0d Merge #11483 (signed equine temper) into fix/camel-inherited-saved-fields
Both PRs added a signed AbstractHorse temper: #11483 for the seven horse and llama species, this one for camels. Keep one field, equine_temper, for every equine: the loader reads Temper unclamped for all of them, temper()/modify_temper() use it for camels too, and the camel saver writes it. The camel_temper field and the camel byte-clamp path are gone; tests follow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:30:16 +01:00
Horizon agentandClaude Opus 5.5 ec62d53993 Merge origin/main into fix/camel-inherited-saved-fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:28:36 +01:00
Horizon agentandClaude Opus 5.5 23b6a5da91 crack feedback tests: give the test guest main's new Joined.configured
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:26 +01:00
Horizon agentandClaude Opus 5.5 1e1e48831a Merge origin/main into fix/11317-block-crack-feedback
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:20 +01:00
Horizon agentandClaude Opus 5.5 d12d6262bb Drop the docs/parity run logs and their one-off capture script
Run logs are not docs; the PR body keeps the numbers. tools/inline_trim_packet_capture.py only produced the dropped packet log and nothing calls it; the protocol test keeps the captured payload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:02 +01:00
Horizon agentandClaude Opus 5.5 c36df3986f Merge origin/main into fix/inline-armor-trim-holders
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:25:01 +01:00
Horizon agentandClaude Opus 5.5 8a38d8831f Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:21:39 +01:00
Horizon agentandClaude Opus 5.5 4595cfca2f Merge origin/main into fix/loaded-sleeper-bed-position
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:21:38 +01:00
Horizon agentandClaude Opus 5.5 d7889ba66c Merge origin/main into fix/h290-pot-template-compounds
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:20:30 +01:00
Horizon agentandClaude Opus 5.5 78634140b3 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:19:26 +01:00
Horizon agentandClaude Opus 5.5 5ebfcbdf7f Merge origin/main into fix/world-pack-enchantment-definitions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:19:26 +01:00
Ludovic Marie d3319194cc Merge pull request #11118 from Delete228/native-model-material-stack-upstream
🤖 Agent: Native mods can draw terrain models with an explicit material (shade, UV shift); the shading follows Flywheel's (MIT, credited). Main merged in after #11117 landed; its test now builds faces with #11356's BakedFace::new (clippy -D warnings passes).
2026-11-07 11:18:17 +01:00
Horizon agentandClaude Opus 5.5 c3083e75cb Fog test: clone the start state, FogState is no longer Copy
FogState now holds the torch flicker's LegacyRandom, so main's rain fog
test (settled_rain_fog_is_its_target_and_easing_is_unchanged) moved
`start` twice; verify failed on E0382.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:18:11 +01:00
Horizon agent 67728a8568 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:17:34 +01:00
Horizon agentandClaude Opus 5.5 9a1da6539a Merge origin/main into fix/h145-required-function-tags
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:15:52 +01:00
Horizon agentandClaude Opus 5.5 6cbece0baa Chunk refresh: name the 26.3 class, LevelExtractor.allChanged
LevelExtractor.setupRender calls allChanged when the effective render
distance changes; LevelRenderer has no allChanged in 26.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:15:23 +01:00
Horizon agentandClaude Opus 5.5 38f887eab2 Merge #11655 (with #11643) into #11654
The three loot functions add arms and tests at the same spots in loot.rs; all kept. Land #11643, then #11655, then this.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:15:02 +01:00
Horizon agentandClaude Opus 5.5 71c1240617 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:14:40 +01:00
Horizon agentandClaude Opus 5.5 14100456d1 Merge origin/main into fix/named-predicate-context
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:14:40 +01:00
Horizon agentandClaude Opus 5.5 99901a6359 Merge origin/main into feat/realms-service-fixes; wire the invite codes footer
Main's backup screens (#9982) came in beside the invite codes screen.
The footer's Copy/Delete/Edit used ids 180-182, which no arm handled (the
100..=200 arm read them as rows 16-17) and which main's Backups arm
(170..=189) now takes first: they carry the selected code's row id
(130 + 3 * row + Copy 0 / Edit 1 / Delete 2) instead. Request.Post's
Content-Type is application/json; charset=utf-8.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:14:16 +01:00
Horizon agentandClaude Opus 5.5 32109b87b6 Merge #11643 (set_book_cover) into #11655
Both add a set_loot_component arm, a loot.rs function arm and a loot test at the same spots; both kept. Land #11643 first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:14:10 +01:00
Horizon agentandClaude Opus 5.5 7cb47be9fe The terrain model test builds its face with main's BakedFace::new
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:43 +01:00
Horizon agentandClaude Opus 5.5 c9af605b1f Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:33 +01:00
Ludovic Marie d14e14539b Merge pull request #11419 from voidsipp/fix/round5-4-11335
🤖 Agent: The brewing stand's fuel slot and quick-move both read the brewing_fuel component of the stack, as BrewingStandMenu.FuelSlot and quickMoveStack do.
2026-11-07 11:13:32 +01:00
Horizon agentandClaude Opus 5.5 b7772e4a84 Merge origin/main into fix/folder-datapack-metadata
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:32 +01:00
Horizon agentandClaude Opus 5.5 199eaad7da Merge #11631 into #11652: one RENAMES table
#11631 adds saved_piece_id (PiecesContainer.load's RENAMES) to the same file; read_jigsaw_start now asks it instead of its own copy of the four old ids. The upper-case id case is dropped from the test: Identifier.parse refuses upper case, so vanilla never reads 'MINECRAFT:JIGSAW' as a jigsaw piece. Land #11631 first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:13:04 +01:00
Horizon agentandClaude Opus 5.5 2d05c9517f Merge origin/main into #11480
command_exec_corpus.groups: keep main's mob_name_predicate_corpus import beside this branch's player_food_predicate_corpus one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:11:02 +01:00
Horizon agentandClaude Opus 5.5 b6ab7d72b5 Native client services: friend world ids behind the friends feature
Main added invite_public_part (horizon_friends::invite) after this branch
was cut; without the feature the server build's clippy
(--no-default-features --all-targets) could not resolve it. A friend's
world then shows no part of the invite, and the invite test needs the
feature.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:50 +01:00
Horizon agent 725f9bb17d Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:10:25 +01:00
Horizon agentandClaude Opus 5.5 e3e18fc523 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:09 +01:00
Horizon agentandClaude Opus 5.5 c082361b4d Merge origin/main into fix/function-command-admission
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:09 +01:00
Horizon agentandClaude Opus 5.5 2860d47ffd Merge origin/main into #11666
Main's #11661 added LevelAccess::container_open and its test for the open copper chest; this branch keeps that name and its test and drops its own container_has_viewers, folding the copper golems' chests into GameLevel::container_open. The recheck_container_openers doc comments #11661 left stranded above container_open are back on their function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:10:07 +01:00
Horizon agentandClaude Opus 5.5 a1ec90f7fc Stats: a joining player's old name-based file moves too
load_guest_records read stats/<uuid>.json directly, and on the server thread (the default single player) and on LAN and dedicated servers every player joins through it, so locateStatsFile only reached the in-frame path. The guest's own name goes to locate_player_json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:59 +01:00
Horizon agentandClaude Opus 5.5 b5d3de5ea4 Studio: log why a recording failed to open
Both entry points dropped open_studio_replay's error, so a bad archive did
nothing with no trace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:54 +01:00
Horizon agentandClaude Opus 5.5 66ecf65d4e Merge origin/main into fix/h157-player-save-projection
Keep main's vibration_tests module beside the new test module; the test's Joined literal gains main's new configured field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:46 +01:00
Horizon agentandClaude Opus 5.5 db673e9b76 Merge main into #10383: keep the craft-menu and map-file ops beside map_carried and treasure map copies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:30 +01:00
Horizon agentandClaude Opus 5.5 4d9d2cee62 Saved pieces: read an id without its namespace; move tag_json's doc back
Identifier.parse reads 'nvi' as minecraft:nvi, so RENAMES applies to a bare id too. The new saved_piece_id had been inserted between tag_json and its doc comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:08:17 +01:00
Ludovic Marie d8ae358b69 Merge pull request #11699 from Detimes/feat-10404-older-clients-3
🤖 Agent: older clients 26.1→1.20.5 behind accept_older_clients (off by default); a 26.3 client never reaches ClientSession or the components_before_1_21_6 rewrites; per-step unit tests + lan_bot joins one bot per release; doc of older_client fixed.
2026-11-07 11:07:55 +01:00
Horizon agentandClaude Opus 5.5 5564c36339 Merge main into #10384
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:07:24 +01:00
Horizon agentandClaude Opus 5.5 1315c0e4db Vindicator: write Johnny only while the flag is set
Vindicator.addAdditionalSaveData writes Johnny: 1b only when isJohnny is true; a vindicator that is not Johnny saves no key (and drops a stale Johnny: 0b from its raw compound).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:07:23 +01:00
Horizon agentandClaude Opus 5.5 ed53fe24ad Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:06:33 +01:00
Horizon agentandClaude Opus 5.5 9fc3a4f0fc Merge origin/main into fix/loot-mine-executor-presence
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:06:32 +01:00
Arslan ArmanulyandClaude Opus 5.5 d3e7858cda Shader packs: GPU example fixtures give their packs terrain programs
Since a pack with no program for terrain or translucent terrain gets the
notes "no program for terrain; nothing draws it" and "no program for
translucent terrain; nothing draws it", standard_pack_gpu_entities and
entity_shadow_ownership failed their no-notes assertions: their packs had
entity programs only. Both packs now also have gbuffers_terrain and
gbuffers_water. The fixtures draw no terrain, and every other assertion is
unchanged.

Fixes #11710

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 15:06:18 +05:00
Horizon agentandClaude Opus 5.5 0eba976485 Spanish: the four recipe transfer strings main added since
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:05:21 +01:00
MSedgeMC c19b0817a0 test: compare falling block landings
Add a direct parity suite for the landing branch of
FallingBlockEntity.tick: real falling entities into air, solid blocks,
water, plants, slabs, cobwebs and moving pistons, with concrete powder's
water contact and the anvil landing event behind --no-contact.
2026-11-07 10:05:19 +00:00
Horizon agent 28cb958bb2 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-07 11:05:09 +01:00
Horizon agentandClaude Opus 5.5 3cb902bb21 Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:04:50 +01:00
Horizon agentandClaude Opus 5.5 9f8ce3033d Merge origin/main into fix/camel-loaded-pose-timestamp
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:04:49 +01:00
Ludovic Marie 2535dd9715 Merge pull request #11584 from rdbtCVS/agent/rdbtcvs/dead-code-review
🤖 Agent: attributes only, no code removed; the 30 kept allow(dead_code) now carry a reason (19 native mod API). Windows/macOS-only code is the risk, CI's other platforms cover it.
2026-11-07 11:04:25 +01:00
Horizon agentandClaude Opus 5.5 9fa7986397 design-system: Extras and Controller pages draw the Blocks frame too
Extras moves onto settings_shell, which draws with graphics_style, so the
doc names the three settings screens that take the Blocks palette.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:49 +01:00
Ludovic Marie e9bd8be99b Merge pull request #11569 from rdbtCVS/agent/rdbtcvs/fewer-duplicate-deps
🤖 Agent: socket2 0.6 keeps every call direct.rs makes (set_reuse_address, the unix-only set_reuse_port, set_only_v6, connect_timeout), so the friends socket code needs no change; lock loses 11 packages.
2026-11-07 11:03:33 +01:00
Horizon agentandClaude Opus 5.5 7cad6a50ff Drop the docs/parity run logs
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:19 +01:00
Horizon agentandClaude Opus 5.5 9712ebdde7 Merge origin/main into fix/effective-item-component-validation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:03:18 +01:00
Florian ERNSTandCodex 70cedfb11a Route attribute commands through the selected guest body
Co-Authored-By: Codex <[email protected]>
2026-11-07 11:02:58 +01:00
Ludovic Marie 4972ede23a Merge pull request #11561 from rdbtCVS/agent/rdbtcvs/one-rust-version
🤖 Agent: one workspace rust-version (1.95, which horizon-mods already needed); the second commit is clippy's let-chain/as_chunks/is_multiple_of rewrites, all on unsigned values with non-zero divisors, so no behaviour moves.
2026-11-07 11:02:43 +01:00
Horizon agentandClaude Opus 5.5 4169af3fa6 Merge origin/main into fix/h181-noai-trader-llama-delay
Keep main's craft_map_take import beside the trader llama corpus import.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:01:55 +01:00
Horizon agentandClaude Opus 5.5 306f9425e8 Merge origin/main into xp-orbs-riding-merge-water
ordered_tick: main's obstacle grid for the orb move kept after the
merge scan and the followed-player pick. A kept followingPlayer now
stays at exactly 8 blocks: ExperienceOrb.followNearbyPlayer re-picks
only past distanceToSqr > 64. Orb merge fixture keeps merge_new_orb.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:01:54 +01:00
Ludovic Marie e86cb3276b Merge pull request #11680 from kacpherek/fix/near-chunks-no-xray
🤖 Agent: near sections no longer dither over far terrain (fade() is 0 or lod::HELD), and the replaced tile still stays behind for lod::FADE through previous, so no hole opens; the test bound lowered to 1,000 matches the half that now come in whole.
2026-11-07 11:01:47 +01:00
TheMrZZandCodex gpt-6.1-sol 6d5c7c51a6 Preserve modeled jukebox saved-song clock presence
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 11:01:17 +01:00
Ludovic Marie 3e80662fbb Merge pull request #11621 from horizon-rust/fix/harness-reuse-finished-world
🤖 Agent: respawn_world_point and disconnect_crafting reuse generate/world only after a generate/finished marker, written after server.stop(); an old half-made world regenerates once.
2026-11-07 11:01:04 +01:00
Ludovic Marie a9e99c3385 Merge pull request #11620 from horizon-rust/fix/lunge-jab-tick-diagnostic
🤖 Agent: LocatorBarClient.arc now flags a catch-up tick only before the jab (tick < stabTick); a catch-up right before it still changes the jab's motion, which the harness compares with vanilla, so lunge_motion keeps its signal.
2026-11-07 11:00:58 +01:00
Ludovic Marie 31908392d0 Merge pull request #11527 from rdbtCVS/agent/rdbtcvs/agents-md-no-counts
🤖 Agent: drops the harness/probe/field counts from AGENTS.md and CONTRIBUTING.md; the commands and paths around them are unchanged, so nothing a reader runs moves.
2026-11-07 11:00:53 +01:00
Ludovic Marie 35e72b8da9 Merge pull request #11239 from NikitaMGrimm/screenshot-sequence
🤖 Agent: horizon-ctl screenshot-sequence takes repeatable frames: each waits for the GPU (30 s at most) and for the pack's centre-depth copy, and F2 is refused while a sequence runs. Lands after #11238.
2026-11-07 11:00:45 +01:00
Horizon agentandClaude Opus 5.5 63d0b51776 Drop the docs/parity run log
Run logs are not docs; the PR body keeps the numbers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:00:08 +01:00
Horizon agentandClaude Opus 5.5 8917e8a9cb Merge origin/main into fix/llama-saved-temper
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 11:00:07 +01:00
Horizon agentandClaude Opus 5.5 3787a6b53c older_client: its doc names every release it now lets join
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:59:36 +01:00
Horizon agentandClaude Opus 5.5 ee026a522e Replace the damage type ids instead of extending them
by_registry outlives a world, so a pack's type stayed accepted by /damage in
the next world opened. Same as sync_enchantment_registries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:59:32 +01:00
Horizon agentandClaude Opus 5.5 3933ed160b Flame culling under TAA: keep the doc to one paragraph, drop the run samples
The flame culling doc now says TAA uses the widened frustum; the run
notes and samples stay in the PR body (run logs are not docs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:34 +01:00
Horizon agentandClaude Opus 5.5 8a83859133 Launcher: drop the Patch notes page's three strings from every language
The page is gone, so i18n's every_language_has_every_string_the_code_asks_for
failed on them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:02 +01:00
Horizon agentandClaude Opus 5.5 36d9983718 Merge origin/main into launcher-notes-in-versions
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:58:01 +01:00
Horizon agentandClaude Opus 5.5 354ba4a38a Merge origin/main into fix/h097-loaded-air-first-tick
Keep main's fall_step(entities) signature with the air guard first;
keep main's vibration_tests module beside the new test module.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:57:58 +01:00
Horizon agentandClaude Opus 5.5 d306a4c171 Dropped items: skip the posed envelope whenever entity culling is off
entity_cull_decision_in_bounds returns Bypass for EntityCullingMode::Off
before reading the bounds, so the TAA condition only kept an unused walk.
Drop the docs/performance run notes (run logs are not docs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:57:19 +01:00
Horizon agentandClaude Opus 5.5 b06f5f36b9 Merge origin/main into #11118
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:56:15 +01:00
Ludovic Marie 9319ad4bf1 Merge pull request #11356 from ilguerro/perf/mesh-padded-index
🤖 Agent: The mesher bakes face data once and reuses padded cell indices; run-parity on meshing passed, so the output is the same.
2026-11-07 10:55:19 +01:00
AkirabaneandClaude Opus 5.5 8e0b157254 dedicated: a stop waits for a proxy to move the players out first
With [transfer] on and players on, a stop (/stop, a stop signal, the
supervisor's stop or restart) now waits stop_wait_seconds (10 by default,
0 to 60, 0 stops at once as before) so a proxy told server/stopping can
move the players out first. The log says "Stopping in 10 s: a proxy may
move the players out first", which horizon-server takes as the start of
the stop. Every login is refused as server_shutdown, and the server ticks
on and answers the proxy's requests.

The wait ends as soon as no player is placed, at its time when no
hand-over is frozen, and at the latest a freeze's longest time later, so a
hand-over started in time finishes; the whole wait is cut at 90 s, within
the supervisor's 120 s for a stop, so stop_wait_seconds and
max_freeze_seconds may add up to 90 at most when the stop waits. No freeze
starts that this limit would cut: a player its source saved after the
target placed it would be on both servers. A freeze lasts ticks, so its
length is reckoned at the tick length of the moment (the rate's, or longer
on a server slower than its rate). A second stop ends the wait. Then the
stop goes on as vanilla's.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:51:02 +01:00
Ludovic Marie 411d1ca42c Merge pull request #11515 from horizon-rust/fix/mob-entity-collisions-10445
🤖 Agent: mobs path-stop at shulkers and a held-still happy ghast; earlier parity failures were env + main's /say regression (#11687 fixed it); CI parity reruns with run-parity before landing.
2026-11-07 10:50:43 +01:00
Alysia Zappavigna 4bb27d7c90 Merge remote-tracking branch 'upstream/main' into pr-11701 2026-11-07 20:48:42 +11:00
Arslan Armanuly 552f2c1b4b Merge main into #10538 (brings #11687's command_exec /say fix for parity) 2026-11-07 14:46:42 +05:00
Horizon agentandClaude Opus 5.5 765f388607 Merge main into #10493; take main's harness drivers
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:44:51 +01:00
Horizon agentandClaude Opus 5.5 0007d5c2a8 Merge main into #10506; take main's harness drivers
#11532 and main since make loaded_pig_tags, loot_mine_pot_components, mob_discard
and command_exec run under a plain parity.py call, which is what ccb85ce45 and
aae27797d did here. Main's versions replace them, so this PR keeps only its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:44:46 +01:00
Ludovic Marie 59aa0e53df Merge pull request #11690 from horizon-rust/fix/raid-wave-ticking-11374
🤖 Agent: raid wave positions (new and cached) now need an entity-ticking chunk in entity ticking range (ServerLevel.isPositionEntityTicking), not just a loaded one; raid, raid_village and raider_celebration harnesses passed.
2026-11-07 10:38:47 +01:00
Ludovic Marie 6920ab1e45 Merge pull request #11688 from horizon-rust/fix/hidden-effect-save-11384
🤖 Agent: the player save now writes and reads the recursive hidden_effect chain (Poison I under Poison II resumes after a reload); SavedEffect became a struct, guest data uses the same path; active_effects, effect_sync, player_effect_amplifier and status_effect harnesses passed.
2026-11-07 10:38:44 +01:00
Ludovic Marie 9bd77676c8 Merge branch 'main' into fix/mob-entity-collisions-10445 2026-11-07 10:38:40 +01:00
Ludovic Marie c3135f0196 Merge pull request #11274 from NikitaMGrimm/still-weather-at-level
🤖 Agent: HORIZON_STILL now settles a frozen world's rain, thunder and rain fog at the level instead of easing them; it only runs with that dev variable set.
2026-11-07 10:33:20 +01:00
Ludovic Marie 44abc057eb Merge pull request #11238 from NikitaMGrimm/pack-history-reset
🤖 Agent: The shader pack reset (both kinds) lands the in-flight frame before replacing targets, so the next frame draws as a fresh pack's first; dev tooling, no change without the reset command. #11239 follows it.
2026-11-07 10:33:15 +01:00
Ludovic Marie 111ba4fd45 Merge pull request #11197 from arslanarm/shaderpacks/shadow-settings-from-fragment-shaders
🤖 Agent: Shader pack shadow settings are now read from the stage that declares them (fragment const settings), with main's noiseTextureResolution kept on the formats stage; only shaders/ packs see a change.
2026-11-07 10:33:11 +01:00
Ludovic Marie 8a50b63daa Merge pull request #11117 from Delete228/native-still-fluid-render-upstream
🤖 Agent: Native mods can draw a still-fluid volume in a block; the fluid shading follows Ponder's FluidRenderHelper (MIT, credited in THIRD_PARTY_NOTICES) and nothing changes without a mod calling it.
2026-11-07 10:33:08 +01:00
Horizon agentandClaude Opus 5.5 073e47caed Merge origin/main into pvp-2/item-view
Picks up #11532 and 88a372f808 (four harnesses that failed under parity.py's defaults).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:32:55 +01:00
Horizon agentandClaude Opus 5.5 e36bbcfc63 Merge origin/main into pvp-2/gui-glint-lookup
Picks up #11532 and 88a372f808 (four harnesses that failed under parity.py's defaults).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:32:55 +01:00
Ludovic Marie afdeb72891 Merge pull request #11689 from horizon-rust/fix/raid-command-11372
🤖 Agent: /raid check now finds a raid still counting down (getRaidAt has no started filter), and /raid sound plays the horn only for "local" at volume 2 with the level random's seed; raid and raid_village harnesses passed.
2026-11-07 10:28:42 +01:00
Horizon agentandClaude Opus 5.5 f58b34cd26 Merge origin/main into #11443
#10523's settings gear now takes its width from the Recipes search box of
this layout; both Escape paths (mod_browser_back, close_transferred_recipe)
kept; one JEI notice for the sprites and the icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:27:14 +01:00
bioneismeandClaude Opus 5.5 6cc37bdcef /damage takes content mods' and world packs' damage types
The commands' minecraft:damage_type ids came from the vanilla export
alone, so /damage refused a type from a content mod or a world's data
pack with argument.resource.not_found. Vanilla's ResourceArgument looks
the id up in the server's registries, packs included.

build_registries now adds the damage catalog's types to the registry's
ids after the export is applied, keeping the export's tags.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 14:24:35 +05:00
Ludovic Marie e7bc612275 Merge pull request #11442 from GabrielNichols/codex/recipes-viewer-transfer
🤖 Agent: The viewer's transfer is a separate server-side path: it only moves ingredients the player already has, leaves grid and inventory untouched when one is missing or the inventory is full, and never teaches the recipe; the vanilla recipe book keeps its own path. Second of the Recipes series, after #10523.
2026-11-07 10:24:08 +01:00
Ludovic Marie 7ee4860304 Merge pull request #11111 from voidsipp/fix/round4-1-focused-text-hook
🤖 Agent: The focused text field is exposed to native mods before the character is inserted, so a hook can veto it; the hook fragments stamp cleanly on main. Main merged in (its ChatScreen is_draft argument added to the test).
2026-11-07 10:21:27 +01:00
536b3316bd fix: mob saddle and gear post equip vibrations at full volume
Vanilla LivingEntity.onEquipItem posts GameEvent.EQUIP (or UNEQUIP) at
the mob and plays the equip sound at volume 1.0. Horizon only did that
for dispensers, copper golems and the player's armor, so saddling a pig
did not activate a sculk sensor and pig/strider/horse saddles played at
0.5.

Interact, inventory, pickup and /item paths now post the event from the
mob, and saddle/harness/armor sounds play at 1.0.

Fixes #9956

Co-Authored-By: OpenCode <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
Co-authored-by: monkeycode-ai <[email protected]>
2026-11-07 09:19:20 +00:00
Matt Van Horn 5c7efbc427 Send the single-player first view faster than 64 chunks per tick
A memory connection that has not reported the view loaded now sends
every ready view column in one batch, and the recorded quota is raised
to cover that send so the following tick is capped again. A socket
guest, a player who has already loaded, and a respawn stay on the
64-column quota. Unlit columns stay withheld, the far ring still spends
only the leftover vanilla quota, and the gates that send nothing are
unchanged.

Fixes #6763
2026-11-07 01:18:24 -08:00
Ludovic MarieandClaude Opus 5.5 5e90d9f10b Merge origin/main into #11411; the credits test's Joined takes main's configured field
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:18:16 +01:00
Horizon agentandClaude Opus 5.5 faa8a4a52a Merge origin/main into #11413; the glide test's Joined and LanHost take main's new fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:17:40 +01:00
Ludovic Marie 6c0de3509d Merge remote-tracking branch 'origin/main' into w-11413 2026-11-07 10:17:26 +01:00
Horizon agentandClaude Opus 5.5 70ac0a01b8 Merge origin/main into #11457; the silent TNT test passes main's entity boxes (none)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:16:57 +01:00
Ludovic Marie 85a5c9f340 Merge remote-tracking branch 'origin/main' into w-11457 2026-11-07 10:16:57 +01:00
Ludovic Marie 67902962fd Merge pull request #11115 from Delete228/native-client-update-fields-upstream
🤖 Agent: Transient block entity fields from the update tag reach the native API without being saved; only mods that ask for them see anything new. Main merged in (its new Joined field added to the test).
2026-11-07 10:16:43 +01:00
Ludovic Marie 8aa99b4f64 Merge pull request #11692 from horizon-rust/perf/apple-pcore-workers
🤖 Agent: only the automatic count changes, and only where hw.perflevel0.physicalcpu answers (Apple silicon): M4 Pro 13 → 9 workers with no loading or pacing regression over 3 alternating rounds; off Apple the count is unchanged (tested on Linux), and a manual Worker threads setting still wins.
2026-11-07 10:16:39 +01:00
Ludovic Marie a93c0e1f11 Merge pull request #11687 from horizon-rust/fix/say-capture-broadcast
🤖 Agent: #10015's early return dropped /say from chat_capture, so command_exec lost 26 announcement lines; the broadcast now skips only the acting guest (lan_acting), whose copy stays in the capture in order. Console and command-block paths are unchanged. command_exec PASS fresh on the desktop; the control on main 004a39be0 FAILs 16320/16346.
2026-11-07 10:16:35 +01:00
Ludovic Marie 7615a429d3 Merge pull request #11684 from horizon-rust/fix/tnt-cart-impact-11380
🤖 Agent: a head-on stop on a straight rail no longer blows a TNT cart up (Entity.move zeroes the blocked axis and OldMinecartBehavior never re-applies it), and a glancing hit off its rails now does; tnt_saved_power, tnt_data_adoption and both vehicle harnesses passed.
2026-11-07 10:16:31 +01:00
Horizon agentandClaude Opus 5.5 e533502260 Merge origin/main into #11111; the test's ChatScreen takes main's is_draft
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:16:27 +01:00
Ludovic Marie b8206893b2 Merge remote-tracking branch 'origin/main' into w-11111 2026-11-07 10:16:27 +01:00
Detimes 81c1cd8b33 Merge remote-tracking branch 'upstream/main' into via-3 2026-11-07 09:15:38 +00:00
Ludovic Marie f4858e8a25 Merge pull request #11186 from flexeykinDev/install-ps1-signature
🤖 Agent: install.ps1 now verifies the release signature with the release-keys.txt key, the same check install.sh already does.
2026-11-07 10:12:13 +01:00
Ludovic Marie 261a343e75 Merge pull request #11428 from Akirabane/feat/transfer-refuse
🤖 Agent: The player-expect move now refuses a full server like a normal join would, and operators still bypass it as vanilla's PlayerList.canBypassPlayerLimit does.
2026-11-07 10:12:09 +01:00
Ludovic Marie 5798723345 Merge branch 'w-11238' into w-11239 2026-11-07 10:12:03 +01:00
Horizon agentandClaude Opus 5.5 37f3271472 Merge origin/main into #11238; the reset tests use main's NativeFrame api and NativePost tonemap arguments
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:11:37 +01:00
Ludovic Marie d887b1331d Merge pull request #11358 from NikitaMGrimm/hdr-step3-float-frame
🤖 Agent: The float HDR frame is only used with HORIZON_HDR_FRAME=float, so the default frame format and its cost are unchanged.
2026-11-07 10:11:24 +01:00
Ludovic Marie 2fcdb9f7af Merge remote-tracking branch 'origin/main' into w-11238 2026-11-07 10:11:21 +01:00
Horizon agentandClaude Opus 5.5 4aaf71dc39 Merge origin/main into #11411; main's spectator portal trips are not a credits respawn
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:10:54 +01:00
Ludovic Marie 6a3359d943 Merge remote-tracking branch 'origin/main' into w-11411 2026-11-07 10:10:44 +01:00
Horizon agentandClaude Opus 5.5 6397e50017 Merge origin/main into #11116; main's block_meets_player names its entity a Player
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:10:20 +01:00
Ludovic Marie fe36131256 Merge remote-tracking branch 'origin/main' into w-11116 2026-11-07 10:10:11 +01:00
Horizon agentandClaude Opus 5.5 99809b5a55 Merge origin/main into #11115; the new test's Joined takes main's configured field
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:09:44 +01:00
Ludovic Marie bc472639ed Merge remote-tracking branch 'origin/main' into w-11115 2026-11-07 10:09:36 +01:00
Ludovic Marie 7ebad255b0 Merge pull request #11107 from Detimes/feat-10404-older-clients-2
🤖 Agent: 26.2 packet layouts are only used inside a session that negotiated V26_2, and accept_older_clients stays off by default, so a 26.3 client's path is unchanged; the lan_bot tests cover both.
2026-11-07 10:08:01 +01:00
Ludovic Marie 1faa3281c2 Merge pull request #10523 from GabrielNichols/codex/recipes-inventory-settings
🤖 Agent: The Recipes settings button opens from the inventory browser; the JEI-derived icon is MIT and credited in THIRD_PARTY_NOTICES. First of the Recipes series (#11442 → #11453 follow).
2026-11-07 10:07:20 +01:00
TheMrZZandCodex gpt-6.1-sol 50467de40e fix: match typed custom data in mob equipment predicates
Project only typed custom-data snapshots from existing mob equipment
stacks and evaluate the admitted SNBT subset predicate. Preserve prior
view fields and unsupported unprojected-consumer behavior.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 10:05:55 +01:00
Horizon agentandClaude Opus 5.5 a7f5e08990 rustfmt the merge resolution
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:05:52 +01:00
Horizon agentandClaude Opus 5.5 9d1c979e91 Merge #11117 (with main) into #11118
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:00:02 +01:00
Florian ERNSTandCodex d4d2214cfb Bind loaded Warden memories to their runtime slots
Co-Authored-By: Codex <[email protected]>
2026-11-07 09:59:36 +01:00
Horizon agentandClaude Opus 5.5 b00d753c62 Merge origin/main into #11117
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:59:20 +01:00
Ludovic Marie 7ab92f9eb7 Merge pull request #11683 from horizon-rust/fix/command-oracle-light-wait
🤖 Agent: only reinforcement_room uses the oracle's light op, so the wait (ThreadedLevelLightEngine.waitForPendingTasks, ticking like settle in a frozen world) changes no other harness's records; command_reinforcements --fresh --repeat 3 PASS 3/3 at desktop load 40-48, where main failed 7 of 10.
2026-11-07 09:58:28 +01:00
Horizon agentandClaude Opus 5.5 3aaef65c5d Merge origin/main into #11121
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:58:25 +01:00
Horizon agentandClaude Opus 5.5 278ffc3b96 Merge #11238 (with main) into #11239
take_screenshot returns main's Option<PathBuf>; a running sequence still
refuses F2 (None).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:57:30 +01:00
Horizon agentandClaude Opus 5.5 7cfee48c36 Merge origin/main into #11238
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:56:45 +01:00
Horizon agentandClaude Opus 5.5 5c18aa614e Merge origin/main into #11253
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:56:12 +01:00
Horizon agentandClaude Opus 5.5 f7ba58871b Merge origin/main into #11256
Main's Clicker already posts the use-start vibration; take its version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:55:32 +01:00
Horizon agentandClaude Opus 5.5 017aee7bcf Drop the run notes from the PR's docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:50 +01:00
Horizon agentandClaude Opus 5.5 09ec3f2452 Merge origin/main into #11258
Keep the loot position test module beside main's mine component tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:49 +01:00
Horizon agentandClaude Opus 5.5 8667fab8c9 Merge origin/main into #11274
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:54:04 +01:00
Horizon agentandClaude Opus 5.5 d89ca171f3 Drop the run notes from the PR's docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:53:31 +01:00
Horizon agentandClaude Opus 5.5 59f6b76699 Merge origin/main into #11296
The saddle slot (main) is read before the PR's equipment stacks; both test
modules kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:53:30 +01:00
Horizon agentandClaude Opus 5.5 5fe63f13e8 Merge origin/main into #11403
Keep the shriek capture test module beside main's command damage tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:52:38 +01:00
Horizon agentandClaude Opus 5.5 bbe57b6b19 Merge origin/main into #11419
Keep the brewing fuel test beside main's new merchant test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:51:56 +01:00
Horizon agentandClaude Opus 5.5 b0047b5518 Merge origin/main into #11301
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:51:22 +01:00
Horizon agentandClaude Opus 5.5 8368732e4e Merge origin/main into #11425
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:50:55 +01:00
Horizon agentandClaude Opus 5.5 a26743d278 Merge origin/main into #11444
Main's thorns check counts events from before the hurt; the PR's helmet
path runs after that count like the plain one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:50:13 +01:00
Horizon agentandClaude Opus 5.5 e8edf93a48 Merge origin/main into #11457
splash takes both the PR's silent flag and main's hit motion (#11388).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:49:08 +01:00
Ludovic Marie 0876de55cc Merge pull request #9974 from daniliammo/fix/launcher-prune-nightlies
🤖 Agent: code-owner approval given; pruning only touches marker-carrying Nightlies, manual/legacy ones are outside the quota (tests cover all four cases).
2026-11-07 09:47:56 +01:00
Ludovic Marie 6f01443e55 Merge pull request #11551 from horizon-rust/fix/11519-checkerboard-negative-scale
🤖 Agent: vanilla's CheckerboardColumnBiomeSource sets bitShift = size + 2 with no clamp, and Java's >> masks the distance; wrapping_add(2) as u32 with wrapping_shr does the same. biome_source PASS with fresh Java on the desktop (main + this head).
2026-11-07 09:39:35 +01:00
Horizon agentandClaude Opus 5.5 dce6093276 Merge origin/main into #11414
Keep the mining experience cases alongside main's survival-only switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:36:43 +01:00
Horizon agentandClaude Opus 5.5 1990e4aecc Merge origin/main into #11301
Keep main's non_mob_tag base with the PR's class-owned container fields,
and both new test modules and corpus groups.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:35:57 +01:00
Horizon agentandClaude Opus 5.5 2e41b259c2 Merge main into #11197 (noiseTextureResolution kept beside the stage-aware reads)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:33:55 +01:00
Ludovic Marie e255c1b2fa Merge pull request #9967 from TheMrZZ/fix/h324-craft-map-swap
🤖 Agent: a map crafted from the grid gets Item.onCraftedBy on the taken stack and the scaling recipe reads the level's maps; merged with main's merchant on_take; craft_map_take and merchant_crafted pass.
2026-11-07 09:33:52 +01:00
Horizon agentandClaude Opus 5.5 b8a6663a3a Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:31:02 +01:00
Horizon agentandClaude Opus 5.5 05a81f37b5 Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:30:32 +01:00
Horizon agentandClaude Opus 5.5 e8d2cf4ee2 Drop the run report from docs/
The evidence stays in the PR description.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:29:54 +01:00
Horizon agentandClaude Opus 5.5 199ef4ad95 Restore the docs the previous commit removed by mistake
Only the PR's own run notes were meant to go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:29:19 +01:00
Horizon agentandClaude Opus 5.5 f2658dff92 Restore the docs the previous commit removed by mistake
Only the PR's own run notes were meant to go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:28:54 +01:00
Horizon agentandClaude Opus 5.5 07b7935bae Drop the run notes from the PR's docs
Validation and benchmark logs belong in the PR description, not in docs/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:28:15 +01:00
Horizon agentandClaude Opus 5.5 b9bc67509a Drop the run notes from the PR's docs
Validation and benchmark logs belong in the PR description, not in docs/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:27:42 +01:00
Horizon agentandClaude Opus 5.5 f834560860 Drop the unreachable second return in minecart_numeric_fields
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:26:59 +01:00
Horizon agentandClaude Opus 5.5 70f9c6e829 Keep eye_columns' doc comment on eye_columns
The new touching_unloaded_chunk was inserted between eye_columns and its
doc comment, which then documented the wrong function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:26:21 +01:00
Ludovic Marie 1e35443f6e Merge pull request #11532 from horizon-rust/fix/11520-harness-plain-run
🤖 Agent: dropping the --reference-only assertion makes a plain run go through runner.main()'s full Java/native comparison, and then the strict tags/pot report, so CI now compares instead of exiting in 0 s. mob_discard's default --resources is lab.RESOURCES (the prepared 26.3 folder) and it gets the missing import os. The two sound harnesses take main's #11555 version.
2026-11-07 09:23:03 +01:00
Ludovic Marie 5075231ffa Merge pull request #10031 from TheMrZZ/fix/h326-boat-live-leash
🤖 Agent: data get on a leashed boat now reads its leash from the same save path the world writes (write_boat_leash); merged main twice, boat_leash_view harness passes and the u/3 union builds with clippy -D warnings.
2026-11-07 09:22:59 +01:00
Ludovic Marie 004a39be07 Merge pull request #11670 from horizon-rust/fix/breeding-bottle-xp-11388
🤖 Agent: breeding now adds one fresh orb of the whole 1-7 roll (no award split, no merge), and XP bottle orbs start 0.25 along the hit face's normal with their motion turned away from it; orb_lava/orb_water/orb_saved_uuid/bed_explosion harnesses passed.
2026-11-07 09:17:36 +01:00
Ludovic Marie d4dc032645 Merge pull request #10024 from some-du6e/optifine/sky-draw
🤖 Agent: OptiFine custom skies draw their layers over the vanilla sky, only when a pack ships optifine/sky and the Horizon sky option is on (a frame without one only clears an empty list); merged main, clippy and the sky tests pass.
2026-11-07 09:17:32 +01:00
Florian ERNSTandCodex 16d77f41d4 Bind loaded block locations in loot-mine conditions
Co-Authored-By: Codex <[email protected]>
2026-11-07 09:17:11 +01:00
MSedgeMC 1bc23d7a17 test: compare nether portal frame search
The portal parity suite only sent entities through portals: the PortalShape
search that turns an obsidian frame into portal blocks was never exercised.
Add a shape family over real frames and a --no-shape switch that reproduces
the previous corpus byte for byte.
2026-11-07 08:16:39 +00:00
Horizon agentandClaude Opus 5.5 81261cc98d Apple silicon: size the chunk worker pool from the performance cores
The automatic pool size was every core but one. On Apple silicon the
efficiency cores count too, so a base M4 (4 performance + 6 efficiency
cores) ran 9 workers that kept the render and server threads waiting for a
performance core: fast flight's p99 frame went from 20 ms to 7 ms with 3
workers (#11294). The automatic count now leaves one of the performance
cores (hw.perflevel0.physicalcpu) instead; elsewhere it is unchanged, and
Graphics > Worker threads still overrides it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:14:33 +01:00
Ludovic Marie 36fc4fa187 Merge pull request #9991 from MSedgeMC/fix/can-be-a-baby
🤖 Agent: a negative Age makes a baby only where AgeableMob.canBeABaby allows (frog, parrot, zombie nautilus, camel husk, slime, magma cube stay adults: box, voice, loot); ageable_integer_load, entity_death_loot and baby_pig_attachment pass.
2026-11-07 09:13:51 +01:00
Ludovic Marie 9ab09b8ed4 Merge pull request #10032 from masa-masa-0314/feat/realms-reset-world
🤖 Agent: Realms reset-world, templates and upload, stacked on #9982 (land after it); upload goes only to the endpoint the service returns; realms tests pass.
2026-11-07 09:13:47 +01:00
Ludovic Marie 92b2dbfbc7 Merge pull request #9982 from masa-masa-0314/feat/realms-backups
🤖 Agent: Realms backup list and backup info screens on main's Realms code (only the backups commit kept; the carried settings/invite copies already landed); restore asks the service and re-reads the list; realms tests pass.
2026-11-07 09:13:43 +01:00
Ludovic Marie b3eee8adef Merge pull request #10130 from TheMrZZ/fix/guest-advancement-selectors
🤖 Agent: advancements= in a selector reads the selected player's own progress, a guest's included (EntitySelectorOptions on the ServerPlayer); guest_advancement_selector passes on the merge, now runnable by parity.py.
2026-11-07 09:10:49 +01:00
Horizon agentandClaude Opus 5.5 542f14f3bf Merge the rebuilt backups branch: reset, templates and upload on top
The two reset-world commits rebuilt on the backups branch (both PRs extend the
same configure screen): the scroll wheel covers the product page, template and
upload lists, and the worlds tab has Options and Backup side by side with
Reset below.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:10:28 +01:00
Horizon agentandClaude Opus 5.5 bdb927a7c4 Merge main: the backups commit rebuilt on main
This branch also carried early copies of the settings-tab and invite-code
commits, which landed on main as #9926 and #9968 and changed since. The tree
is main with only the backups commit on it, plus the configure screen's
tier fields that main added.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:10:27 +01:00
Alysia Zappavigna e297f7245b ambience tests: import SoundEngine 2026-11-07 19:09:01 +11:00
Horizon agentandClaude Opus 5.5 3788883346 /say in a captured command: the acting player gets it with the output
#10015 broadcast /say from cmd_say to every guest and dropped the line
from chat_capture. The acting guest still saw it, but the command
harness reads the capture, so command_exec lost every /say line
("[Player] relative dimension", the h154 function's "[Pig] H154
fallthrough") and failed parity-main at ebcfa2263a.

When a guest's command is captured, the broadcast now skips that guest
and the line stays in the capture, in order with the other output, which
is delivered to them as before. The console path is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:09:00 +01:00
Ludovic Marie 62a480890c Merge pull request #11679 from horizon-rust/docs/roadmap-drop-alt-accounts
🤖 Agent: roadmap.toml is the source; ROADMAP.md and roadmap.json regenerated (build --check passes). older-servers kept on purpose (1.8 PvP servers), only the sign-in item goes.
2026-11-07 09:08:00 +01:00
Alysia Zappavigna 640dd0bcf5 ambience tests: import SoundEngine 2026-11-07 19:05:56 +11:00
Horizon agentandClaude Opus 5.5 c4a240cb63 Roadmap: drop alt-accounts (other sign-in providers)
Mojang's approval covers Microsoft sign-in only; other account providers
(Ely.by, Yggdrasil servers) are against the rules Horizon plays by, so the
item can't ship. #1270 stays closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:05:10 +01:00
Horizon agentandClaude Opus 5.5 7ea951a07e Merge main: mob_local_sound and ominous_warning_sound already run plainly (#11555)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:04:53 +01:00
Horizon agentandClaude Opus 5.5 2f548e8b23 Run the raid command test with an operator's permission
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:04:27 +01:00
Horizon agentandClaude Opus 5.5 64d97f7db8 Command oracle: read light once the queued light updates ran
ThreadedLevelLightEngine runs the checkBlock tasks a /fill queues at most
1000 per update. command_reinforcements fills ~100k blocks around its room
and then requires block and sky light 0 there; on a loaded runner the
vanilla side still read the old light (7, 9, 10 at different positions),
which failed parity-main and 7 of its bisect steps back to 11-05 merges.
The light op now waits for waitForPendingTasks on that chunk, ticking as
settle does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 09:03:33 +01:00
Ludovic Marie 39e13b9f70 Merge pull request #11556 from horizon-rust/fix/remote-server-errors-i18n
🤖 Agent: follow-up to #10166: the remote server's SSH errors go through tr/trf with named placeholders, English stand-ins in all 86 lang files.
2026-11-07 09:02:23 +01:00
Ludovic Marie 90bdc031fe Merge pull request #11557 from horizon-rust/fix/macos-titlebar-without-glass
🤖 Agent: follow-up to #10129: with glass off, macOS still gets its 32 px title bar (plain side colour), so content no longer sits under the traffic lights.
2026-11-07 09:02:20 +01:00
Ludovic Marie b66a4683f1 Merge pull request #11617 from horizon-rust/fix/fullscreen-unmaximize-windows-only
🤖 Agent: follow-up to #10177: the unmaximize-before-fullscreen step runs on Windows only, and apply() no longer re-requests fullscreen each call while the window reports maximized.
2026-11-07 09:02:17 +01:00
Horizon agent cdd4332ae4 Merge remote-tracking branch 'origin/main' into n/11556
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-07 08:59:28 +01:00
Horizon agent 4835c74731 Merge remote-tracking branch 'origin/main' into n/9967
# Conflicts:
#	crates/horizon-client/src/menu.rs
#	tools/test_command_exec_parity.py
2026-11-07 08:58:08 +01:00
Horizon agent de976257da Merge remote-tracking branch 'origin/main' into n/10031
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 08:57:25 +01:00
Alysia Zappavigna 175fbe9e52 Merge commit '37d8dc0e5' into random-source
# Conflicts:
#	crates/horizon-client/src/game/lightning.rs
2026-11-07 18:56:19 +11:00
Horizon agentandClaude Opus 5.5 33f6584496 guest_advancement_selector harness: run both sides by default
parity.py runs a harness with no arguments; --side was required, so every
automatic selection failed it at once. The default now runs Java, then native
against Java's facts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:56:10 +01:00
Alysia Zappavigna fde182860f random_source: repeat under HORIZON_STILL; tidy imports 2026-11-07 18:56:10 +11:00
Kacpherek 7386e3968f Draw near sections whole over far terrain: their dither showed the caves inside 2026-11-07 08:50:34 +01:00
Horizon agentandClaude Opus 5.5 9478067990 Spawn raid waves only where entities tick
Raid.findRandomSpawnPos and the cached wave position both ask
ServerLevel.isPositionEntityTicking: an entity-ticking chunk in entity
ticking range. Horizon accepted any loaded chunk, so a wave could spawn
past the simulation distance, where its raiders would never tick.

Part of #11374 (the spawn positions; the raid/spawner order stays open).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:48:52 +01:00
Ludovic Marie 37d8dc0e56 Merge pull request #11661 from horizon-rust/fix/copper-chest-viewers-11378
🤖 Agent: a copper chest someone has open now skips its weathering step (WeatheringCopperChestBlock.randomTick checks getEntitiesWithContainerOpen); random_tick and random_tick_sections harnesses passed.
2026-11-07 08:48:46 +01:00
Horizon agentandClaude Opus 5.5 d12e6637f0 Let the raid command test reach the raids
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:47:14 +01:00
Alysia Zappavigna 3fc0eb473e random_source: one RandomSource.create(); entity and level randoms as vanilla 2026-11-07 18:46:43 +11:00
Florian ERNSTandCodex f9eb279790 Bind actual mob target state in loot-kill predicates
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:46:18 +01:00
Ludovic Marie 1f205c93df Merge pull request #11022 from ilguerro/fix/9776-block-vibrations
🤖 Agent: using items (fishing rod, ender eye…) now posts vanilla's item-use game events through causeUseVibration, so sculk sensors react as in Java; sculk, server_player_sculk, server_wool_vibration and use_item_on pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
2026-11-07 08:42:05 +01:00
Ludovic Marie 66b8b9e69c Merge pull request #11017 from TheMrZZ/fix/h101-merchant-crafted-stat
🤖 Agent: crafting with a merchant result slot now fires vanilla's merchant crafted trigger and updates the trade, checked by the new --merchant-crafted-fixture in command_exec; merchant_crafted, command_exec and trading pass against the 26.3 oracle in the gate's harness run (242 selected; failures were host setup only plus the lunge_motion timing flake).
2026-11-07 08:42:02 +01:00
Ludovic Marie 17995c544f Merge pull request #10837 from TheMrZZ/fix/non-mob-saved-mob-fields
🤖 Agent: item entities, armor stands and boats now save the shared Entity flags (Invulnerable, CustomNameVisible, Silent, NoGravity, Glowing, HasVisualFire, Tags) as vanilla's saveWithoutId does. Its 242 selected harnesses ran on main + this head + #11017 + #11022: save_roundtrip, world_reopen, entity_nbt_boolean, the armor stand and boat ones pass; the only failures are setup (no sound/lang assets on the host, harness args) and the known lunge_motion tick-timing flake under software rendering.
2026-11-07 08:41:57 +01:00
Horizon agentandClaude Opus 5.5 31338299d6 Format the orb tests; give the orb merge fixture merge_new_orb
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:36:45 +01:00
Horizon agentandClaude Opus 5.5 e2d24c60a4 Detonate a TNT minecart from the speed left after its move
MinecartTNT.tick reads the delta after AbstractMinecart.tick. The move
zeroes the blocked axes and no track step puts them back, so a cart
stopped head-on by a wall on a straight rail does not blow up, while one
glancing off a wall keeps its free axis and does, on or off its rails.
Horizon kept the speed the cart asked for before the move on the rail
paths and none off them.

Part of #11380 (the impact detonation; the team push points stay open).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:28:43 +01:00
Ludovic Marie 648f935466 Merge pull request #11657 from horizon-rust/fix/bee-hive-11377
🤖 Agent: overflow honey now leaves through Player.drop (eye height − 0.3, thrown along the view, pickup delay 40) instead of popping at the feet; held_drop harness passed, plus Game tests for Peaceful anger and event 2011.
2026-11-07 08:26:33 +01:00
exampleandClaude Opus 5.5 69ca4f94b7 Waxing a double copper chest reaches both halves; an open one doesn't weather
HoneycombItem.useOn gives a double chest's other half its game event and
particles and plays a second sound from the clicked block, on the server and
in the client's prediction. The wax sound leaves out the acting player and
goes out in the blocks source.

WeatheringCopperChestBlock.randomTick skips the attempt, and its draw, while
getEntitiesWithContainerOpen has anyone: a player's menu, or a copper golem
(CopperGolem.hasContainerOpen), a double chest counting for both halves.

Fixes #11378

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:24:54 +01:00
Horizon agentandClaude Opus 5.5 23d08068aa Keep a player's hidden effects in the save
MobEffectInstance.Details nests the instance an effect hides under
hidden_effect, and the player save writes the whole chain. Horizon wrote
only the top instance, so a weaker, longer effect (Poison I under
Poison II) was gone after a reload instead of resuming. SavedEffect is
now a struct carrying that chain; guest player data goes through the
same code.

Part of #11384 (the player side; mobs still drop weaker instances).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:24:54 +01:00
Cx188andCodex 4f0045d2d0 Notify native mods when chat suggestions are applied
Let native mods keep shared-item text aligned when a player accepts or
cycles chat suggestions. Report the original input, UTF-16 replacement
range, selected suggestion and resulting text. Cover keyboard and mouse
handling, server suggestions, emoji and length limits.

Part of #11212.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 10:24:13 +03:00
Ludovic Marie 6ab56ba9f1 Merge pull request #11600 from horizon-rust/fix/audio-probe-off-render-thread-11490
🤖 Agent: the device list is now read on a worker thread, so a slow Windows audio driver no longer stalls a frame; the render thread only swaps in the result. No vanilla behaviour changes, no harness covers it.
2026-11-07 08:23:53 +01:00
Horizon agentandClaude Opus 5.5 d193cb15ca Make /raid check find a raid counting down; horn only for "local"
RaidCommand.getRaid is getRaidAt, which returns a raid that has not
started its first wave yet. playSound plays the horn only for the text
"local", at volume 2 with the level random's seed. The status line
concatenates its floats the Java way (20.0, not 20).

Fixes #11372.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:21:25 +01:00
Ludovic Marie cc7f3b4c22 Merge pull request #11648 from horizon-rust/fix/effect-show-icon-11385
🤖 Agent: effects now carry vanilla's showIcon apart from the particles (5-arg constructor, update, save codec default, packet bit 4), so warden Darkness and /effect hideParticles no longer show a HUD icon; 8 effect/HUD/sculk harnesses pass.
2026-11-07 08:18:46 +01:00
Florian ERNSTandCodex 7f21b95989 Adapt new equipment wear test to named trim holders
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:15:48 +01:00
Florian ERNSTandCodex 76123f5638 Preserve inline armor-trim holders through item state and sync
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:13:34 +01:00
Florian ERNSTandCodex b322275555 Save Camel inherited horse fields from modeled state
Co-Authored-By: Codex <[email protected]>
2026-11-07 08:11:46 +01:00
Horizon agentandClaude Opus 5.5 b1eaf67925 One fresh orb from breeding; bottle orbs start away from the hit
- Animal.finalizeSpawnChildFromBreeding adds one new ExperienceOrb of the
  parent's 1-7 roll: breeding no longer splits it (4 into 3 + 1) or merges.
- ThrownExperienceBottle.onHit awards with direction: away from the block
  face it hit, or back along its motion after an entity hit; the new orb
  starts 0.25 that way and a random motion against it is turned round
  (ExperienceOrb(level, pos, roughly, value)) (#11388).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 08:06:24 +01:00
TheMrZZandCodex gpt-6.1-sol b8d3a3b132 fix: keep vanilla crafter drops unnamed
Exclude actual crafters from automatic dropped-item name capture while
preserving chest/shulker names, contents and the other copy policies.
Cover the real destruction route against a frozen fresh Java reference.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 07:53:50 +01:00
MSedgeMC 3b8ac89d2c port_map: SetBannerPatternFunction.run is ported
The row said layers from loot are not set and no vanilla table uses the
function; a datapack table does, and the row now names the arm and the item-side
parse that puts the layers on the banner.
2026-11-07 06:49:34 +00:00
MSedgeMC f8aa8e0e0d test: the loot arm appends or replaces, and the item wears the layers
The loot table test fills a chest with a white banner through set_components
(a red stripe at the bottom) and then set_banner_pattern with append: the
component carries both layers, in that order; with append false it carries only
the function's blue border. Without the arm the append case keeps the single
red layer.

The item test hands set_loot_component a banner_patterns list and reads
`banner_patterns` back as two BannerLayers (pattern and dye), and a null
component empties them again. Without the arm the list is empty - the banner
wears nothing.
2026-11-07 06:49:34 +00:00
MSedgeMC 6ec1865ac1 fix: a looted banner_patterns component dresses the banner
set_loot_component only knew the trim, so a rolled banner_patterns list landed
in `other` as a raw tag: the renderer and the tooltip read
`StackData::banner_patterns`, which stayed empty, and the banner from a
datapack table (or a set_components function) came out undyed. The arm parses
the component with the same `layers_from_nbt` the item's own component uses, so
the layers - pattern and dye - reach the fields the game reads.
2026-11-07 06:49:12 +00:00
MSedgeMC 18cb5eb6d0 fix: set_banner_pattern writes a banner's layers
SetBannerPatternFunction sets the stack's banner_patterns to the function's
layers, or with append adds them to the end of the component's own list
(BannerPatternLayers.Builder.addAll twice). A layer is a pattern id and a dye
name, so the list the table wrote is the component's list as it stands. The arm
was missing, so a datapack's banner pattern changed nothing: the banner came out
bare, or with only the layers an earlier function wrote.
2026-11-07 06:49:12 +00:00
MSedgeMC 66931c6958 port_map: the two set_custom_model_data rows are ported
apply names the ListOperation helper its four lists go through, and run names
the arm that draws the floats and colours and keeps the component the stack
already had. The rows said "no vanilla loot table uses it", which was true but
not a reason for a table that does to change nothing.
2026-11-07 06:46:00 +00:00
MSedgeMC 0ba45ef93e test: the four model-data lists each follow their own operation
A table writes custom_model_data through set_components (floats [1.0], strings
["old"]) and then set_custom_model_data: floats appended from a constant
provider, flags and strings replaced, a colour inserted first. The rolled stack
carries 1.0 and 2.0, [true], ["new"] and [16711680] - without the arm the
component keeps the first function's lists and has no colours at all.

The helper's own test covers what a table rarely reaches: a replace_section of
one element, one with no size (the new values' length), an insert past the end
that changes nothing, and a mode the codec would refuse reading as a
replacement.
2026-11-07 06:46:00 +00:00
MSedgeMC 68e1549ebd fix: set_custom_model_data writes the item's four model-data lists
SetCustomModelDataFunction reads the stack's custom_model_data (EMPTY when it
has none) and replaces each of its four lists the function names - floats and
colors from number providers drawn with the loot random, flags and strings as
written - by that list's ListOperation over what was there: replace_all,
replace_section (offset and an optional size), insert at an offset, or append.
The arm was missing, so a datapack's model data changed nothing at all.

ListOperation itself is a small helper beside the number readers; an offset
past the end leaves the list as it was, as vanilla logs and ignores it there.
The component then reaches the item through set_loot_component's generic arm,
which keeps it in `other` where the model selection, the CIT rules and the LAN
sync already read it - that half needed nothing new.
2026-11-07 06:46:00 +00:00
Horizon agentandClaude Opus 5.5 185187e7e2 Don't weather a copper chest someone has open
WeatheringCopperChestBlock.randomTick runs changeOverTime only when the
chest's getEntitiesWithContainerOpen is empty, so an open chest neither
oxidizes nor draws its odds (part of #11378).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 07:30:45 +01:00
HiuoyandClaude Opus 5.5 383f733f9e Port pack: villager and wandering trader trade events are data in 26.x
`horizon-port-pack events` gives a fifth answer, data: NeoForge's
`VillagerTradesEvent` and `WandererTradesEvent` are gone since vanilla 26.1
made trades the `trade_set` and `villager_trade` registries (NeoForge's 26.1
primer), so a port writes trades as data instead of waiting for a hook.
On ATM10 that answers 33 uses (20 and 13 mods).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 10:29:37 +04:00
MSedgeMC b2c9470eb7 port_map: SetBookCoverFunction.apply is ported
The row said no vanilla loot table uses set_book_cover, so nothing was missing;
a datapack's table does reach it, and it now names the arm that applies the
cover and the item-side parse that reads the component back.
2026-11-07 06:22:12 +00:00
MSedgeMC dc8f47b2e8 test: a loot table's cover titles the book, and the item reads it
The loot table test fills a chest with a written book through two modifiers: one
set_components with a title, author and page, then set_book_cover with another
title, an author and generation 2. The cover replaces the title, author and
generation, and the page the other function wrote is still the book's. Without
the arm the component keeps the first title.

The item test hands set_loot_component a written_book_content as the loot engine
builds it and reads the book back: title, author, generation and both pages, and
a null component leaves none. Without the arm the book has no content at all.
2026-11-07 06:22:12 +00:00
MSedgeMC 02dc69348d fix: a looted written_book_content reaches the book's fields
A component a loot function writes goes through StackData::set_loot_component
when the rolled stack becomes an item, and only the trim had an arm: a written
book's content landed in `other` as a raw tag nobody reads, so set_book_cover
(or set_components) titled a book whose tooltip and reading screen still showed
nothing. The content is now parsed the way the item's own component is - the
parse moves out of from_nbt into written_book_from_tag, shared by both - so the
title, author, generation and pages come out of a rolled book.
2026-11-07 06:22:11 +00:00
MSedgeMC 65e6b5ce95 fix: set_book_cover writes a written book's title, author and generation
SetBookCoverFunction updates the stack's written_book_content, over the
component it already carries: the title as a Filterable (the raw text, and the
filtered text when a server filtered it), the author, and the generation, while
the pages and the resolved flag stay as the earlier functions left them. A loot
table's written book got none of that: the modifier ran and changed nothing, so
a datapack book came out with no cover at all.
2026-11-07 06:22:11 +00:00
yahya-nusseibehandClaude Opus 5.5 b984398cbc Seal the launcher's secrets file with a per-install secret
Without a keychain, account-secrets.json was sealed under SHA-256 of a
constant and /etc/machine-id, which every local user can read; on a
system with none of the machine-id files the key was a constant built
into every launcher. Only the file's 0600 mode protected the Microsoft
refresh token inside, so a copy restored with wider permissions, or a
shared home, gave it away.

Format 2 (HZSECv2) adds a random 32-byte install secret to the key,
kept beside the file in account-secrets.key (0600, made at the first
write through a temporary file and a rename, under the file's lock).
The key is never the bare constant any more, and a v2 file whose
secret is missing or belongs to another install is refused like any
unreadable file, never written over.

A file sealed by an older launcher (HZSECv1) is still read with the old
key and sealed again under the new one at once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 08:20:48 +02:00
yahya-nusseibehandClaude Opus 5.5 d689881e68 Cap the packets a connection queues for the hub between ticks
A connection's network thread hands every play packet to the hub over
an unbounded channel. The hub's inbox caps (INBOX_LIMIT packets,
INBOX_BYTES bytes) apply only as it drains that channel at the start of
a tick, so while one tick runs long (an autosave, chunk generation) a
client can queue tiny packets without bound, each a boxed Serverbound
many times its size on the wire.

Each connection now counts what it handed the hub and the hub hasn't
taken yet (Undrained, shared between the network thread and the
guest's PlayerSender). Past twice the inbox caps the network thread
disconnects the client with the same message as the packet-rate kick
(disconnect.exceeded_packet_rate) and logs a warning; the hub
uncounts each packet as it takes it. A client within the inbox caps
never comes near.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 08:06:12 +02:00
vegetableandOpenAI Codex 3baa4f56ae Cover chunk reload invariants
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 11:03:01 +05:00
Horizon agentandClaude Opus 5.5 a03c91e630 guest_advancement_tests: Joined gained configured on main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:59:51 +01:00
masa-masa-0314andZCode 59bafaf8b6 Realms: the invite screen's selection reads as focused on main's draw
The invite screen's list is the screen's one focusable beside its
footer, so its extractSelection outline is white; main's draw has no
list_focused parameter to ask.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:59:09 +08:00
yahya-nusseibehandClaude Opus 5.5 c76534d9f1 Update tokio to 1.53.3 and mysql to 28 (lru 0.18) for advisories
cargo audit flagged two crates in the locks:

- tokio 1.53.2 (RUSTSEC-2026-0386), reached through quinn in
  horizon-mod-deps and in the launcher's lock. Patched in 1.53.3, a
  patch release, so both locks move there.
- lru 0.16.4 (RUSTSEC-2026-0253), reached only through mysql 27 in
  horizon-mod-deps. No 0.16 release carries the fix; mysql 28 depends
  on lru 0.18, which does, so the crate mods may use moves to mysql 28
  (mysql_common 0.37, pem 4, base64 0.23 and arc-swap come with it).

`cargo check -p horizon-mod-deps --features mysql` builds, and cargo
audit no longer reports either advisory. A native mod that asks for
`mysql = "27"` now has to ask for 28, since the lock is the allow list;
docs/native-mods.md names the new version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:59:08 +02:00
vegetableandOpenAI Codex 2b9cb6ba26 Fix chunk reload invalidation
Co-Authored-By: OpenAI Codex <[email protected]>
2026-11-07 10:56:42 +05:00
MSedgeMC a4951f7377 port_map: three rows the tree has moved past
Guest break refusal is the sequence ack every PlayerAction arm sends, the packet rate limit is PacketRate::received,
and MinecraftServer.shouldRun has no haveTime budget to defer against in Horizon's loop.
2026-11-07 05:54:17 +00:00
yahya-nusseibehandClaude Opus 5.5 bddd5ebf76 Website bug reports: GH-123 cross-references nothing either
neutralise breaks @mentions, #123 and github.com/ issue links in what
people write in the website's report form, so an anonymous report can't
put backlinks on the repository's issues. GitHub also links GH-123
(either case) to issue 123, which got through.

- At the start of a word, GH- followed by a digit gets a zero-width
  space after GH, the way @ and # already do: the text reads the same
  and links nothing. Other words (high-5, GH-pages, laugh-1) are left
  alone.
- Test: GH-12, gh-7 and (Gh-8) are neutralised; look-alikes aren't.

Found in yahya-nusseibeh's review of Horizon (L2, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:54:10 +02:00
yahya-nusseibehandClaude Opus 5.5 07ed5dac67 Relay: a cap on open connections per address, and a per-site join budget
Two ways one client could lock every user out of the Play with Friends
relay, with no account:

(a) The only connection cap was global (4,096), counted before anything
    is read, and each connection may stay silent for the 10 s first-line
    wait: about 410 idle connections a second from one address kept the
    relay full, so hosts couldn't register and guests couldn't join.
(b) Every join, known code or not, is refused while the global
    unknown-code budget (120 a minute) is empty. Each /64 may try 5
    unknown codes per 10 minutes, so ~240 /64s keep it empty, and one
    residential /56 has 256 of them.

- Limiter::open_connection / close_connection: connections open at
  once per address key (IPv6 /64; a /48 four times as many), 64 by
  default (RELAY_MAX_OPEN_PER_ADDRESS; a host keeps one connection per
  guest, hence the margin). Counted before anything is read, or right
  after the PROXY line behind the PROXY protocol; past the cap the
  connection is closed at once, not held for the first-line wait. An
  RAII OpenSlot gives the place back however the connection ends.
- Each site (IPv4 /24, IPv6 /48) has its own unknown-code budget, 24 a
  minute with a burst of 6, checked before the global one and spent on
  every unknown code, so emptying the global budget takes many sites,
  not one home.
- docs/play-with-friends.md: the new setting and the site budget.
- Tests: the per-address cap (IPv4, IPv6 /64 and /48, off without
  per-address limits), the per-site budget (shared within a /24 and a
  /48, refilling), and a running relay closing a fifth silent
  connection at once with a cap of 4, then letting new ones in once
  the held ones go.

Found in yahya-nusseibeh's review of Horizon (H2, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:51:55 +02:00
MSedgeMC 03c5e6b091 port_map: PlayerList.locateStatsFile is ported
The row said the name-based stats file is not moved to the uuid file. It names the function that
does it, and the segment rule that decides whether vanilla would look for it at all.
2026-11-07 05:49:55 +00:00
MSedgeMC f4cdc0873c test: pin the old stats file moving to the uuid name
The test works in a temporary world folder: nothing is written when there is nothing to move, the
`players/stats/<name>.json` file lands on the uuid name and is not left behind, a uuid file that
already exists wins and leaves the old file alone, and the segment check refuses capitals and
separators while accepting a lowercase name with dashes, dots and digits.
2026-11-07 05:49:55 +00:00
MSedgeMC f802d6cc2c fix: move an old name-based stats file to the player's uuid
PlayerList.locateStatsFile: the stats file is `stats/<uuid>.json`, but a version that wrote
`stats/<player name>.json` left its numbers under the old name. When the uuid file does not exist
yet and the name is one vanilla would write as a path segment
(FileUtil.isValidPathSegment: `[-._a-z0-9]`, and not `.` or `..` — the check is on
`<name>.json`, so a capital letter in the player's name refuses it), the old file is moved to the
uuid name and that is the file that is read; a failed move leaves the old one in place.

Horizon always resolved the uuid path, so a world opened after such a version started with empty
stats. The 26.3 advancements file has no name-based form (`PlayerList.getPlayerAdvancements`
resolves the uuid directory directly), so only stats move.
2026-11-07 05:49:55 +00:00
yahya-nusseibehandClaude Opus 5.5 bda6cb38ae Shader packs: bound what one zipped file may unpack to
Pack::read preallocated from a zip entry's declared size and inflated it
with no limit. Shader packs are zips players download, so a deflate bomb
(or a forged size field) in any file under shaders/ aborted the game
with an allocation failure as soon as shaders.properties was read: 512
MiB of spaces zip to 509 KiB and took peak RSS from 4 to 516 MiB; at
1.5 GiB the review measured 1,543 MiB.

- Nothing is allocated from the declared size; a file that declares or
  inflates to more than MAX_FILE (64 MiB, far above any .properties,
  .lang or GLSL file, with room for the largest textures) reads as
  missing, like a file the pack doesn't have. The same pack now stays
  at 4 MiB.
- Folder packs are unchanged (files on disk, not inflated).
- Test: a small zip whose shaders.properties unpacks past a 16 KiB cap
  reads as missing; the rest of the pack still reads.

Found in yahya-nusseibeh's review of Horizon (M1, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:48:23 +02:00
abit-foggyandCodex 3ec6d2426f fix(studio): restore title and hand state after replay
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 34c4cd3322 fix(studio): wait for world resources before replay entry
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 2c142a1d49 fix(studio): drop stale HUD height state
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex b83cbac5c4 chore(studio): merge latest main into playback
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:47:05 -10:00
abit-foggyandCodex 49471d6449 fix(studio): initialize replay save folder
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:53 -10:00
abit-foggyandCodex 2e57c30ab2 chore(studio): merge main into playback
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:53 -10:00
abit-foggyandCodex 745dc168bc fix(studio): rebuild replay state on seek
Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:21 -10:00
abit-foggyandCodex 698b836e2e fix(studio): route world exit through replay cleanup
Send pause-menu and death-screen exits through close_studio_replay when playback is active. Clear the replay timer and restore the prior camera so the title screen and the next world use normal player and hand rendering.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:21 -10:00
abit-foggyandCodex c8388a50f6 fix(studio): recover failed replay opens and return to title
Open and validate a replay archive before replacing the current screen. Track standalone replay sessions so closing one disconnects its synthetic remote and restores the title screen. Start playback in first person after applying timestamp-zero world packets.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:20 -10:00
abit-foggyandCodex c8d6221624 feat(studio): add MCPR streaming reader and archive options
Inflate recording streams to a bounded temporary file, verify the stream CRC during the copy, and index a sparse set of seek anchors instead of retaining every packet in memory. Preserve whole-frame recovery and timestamp-group seeking.

Co-Authored-By: Codex <[email protected]>
2026-11-06 19:46:20 -10:00
abit-foggy 44a7fa4dda fix(studio): smooth replay limb animations with virtual tick pacing 2026-11-06 19:46:20 -10:00
abit-foggy 53304d1bb6 fix(studio): advance walk limb animations during replay playback 2026-11-06 19:46:20 -10:00
abit-foggy 4dfa1dc31f feat(studio): record local player swing and pose animations into stream 2026-11-06 19:46:20 -10:00
abit-foggy 06e83b2f80 feat(studio): hide first-person hand and player hotbar in studio mode 2026-11-06 19:46:19 -10:00
abit-foggy 17c146a7be feat(studio): record local player movement into replay stream 2026-11-06 19:46:19 -10:00
abit-foggy 438b47c86b fix(studio): dump initial world snapshot on record start and return to main menu on close
- Dump resident chunk columns, player pose, center chunk, and dimension login into .mcpr stream at timestamp 0ms on recording start
- Allow all chunks in remote_chunk_in_range during studio replay and freecam so distant chunks are never discarded
- Reset title mode to Mode::Main when closing studio replay so the user returns to the main menu instead of multiplayer
2026-11-06 19:46:19 -10:00
abit-foggy bd9883050e fix(studio): bypass remote loading overlay and panorama in studio replay 2026-11-06 19:46:19 -10:00
abit-foggy 0faacc6a85 fix(studio): launch 3D replay editor from studio browser and title screen
- Add interactive glass button for '▶ Edit' on recording cards in StudioUi
- Allow single-clicking '▶ Edit' on any card to immediately load and open the editor
- Allow clicking the bottom 'Edit Replay' button to open the selected or first replay
- Handle Event::OpenStudioReplay in Loading::menu_option_events by queueing PendingWorldEntry::StudioReplay
- Handle PendingWorldEntry::StudioReplay in Game::poll_pending_world_entry to enter replay
- Add Session::replay and init_remote_for_replay to initialize client state when entering replay from title screen
- Dismiss title screen and uncapture cursor so timeline and studio HUD controls are interactive
- Release mouse cursor on Escape key in studio replay mode before opening pause menu
2026-11-06 19:46:19 -10:00
abit-foggy 6a9f30fd40 feat(studio): packet recording and replay playback (.mcpr container format)
- Implement standard .mcpr container reader/writer (metaData.json + VarInt timestamped recording.tmcpr stream)
- Add asynchronous StudioRecorder worker to record clientbound packets with zero impact on gameplay FPS
- Add ReplayDriver with timeline seeking, variable speed control, pausing, and looping
- Wire zero-alloc packet recording tap into Session for singleplayer, LAN, and remote multiplayer
- Add in-game /studio chat commands and control socket commands for recording and replay management
2026-11-06 19:46:18 -10:00
Horizon agentandClaude Opus 5.5 097dd3a63f Bees: Peaceful ends player anger, crop sparks, thrown overflow honey
- NeutralMob.isValidPlayerTarget also fails on Peaceful: a bee's anger at
  the player ends there.
- BeeGrowCropGoal sends level event 2011 (data 15) before the crop grows;
  the client draws it like 2012 (spawnParticleInBlock, happy villager).
- A honey bottle that no longer fits is player.drop(stack, false): from
  below the eyes, thrown along the view, no thrower (#11377).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:45:56 +01:00
masa-masa-0314andZCode 0787a9e7ff Realms: the invite codes screen is vanilla's selection list
RealmsInviteCodesScreen lays out a footer of two rows (Copy/Delete/Edit
97 wide over Create/Back 150 wide, FOOTER_HEIGHT 64), titles itself
Invite Codes (n/5) once fetched with the gray subtitle over an empty
list, and shows the loading dots without a list behind them. Its rows
are 28 high with only the code's text, selected like every
ObjectSelectionList (the port's extractSelection); Copy answers the
clipboard and Delete/Edit act on the selection (ids 180-182), not on
per-row buttons.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:44:56 +08:00
yahya-nusseibehandClaude Opus 5.5 22e1582558 Shader packages: count the 8 MiB total while reading, not after
Pack::read inflated each of up to 512 zip entries to 1 MiB + 1 and kept
them all, and only Pack::from_files then checked the pack's 8 MiB total.
511 entries of zeros take 561 KiB zipped, and the mod registry reads
every submitted .hshader (index::inspect, Kind::Shader) on its blocking
pool: one upload made it hold 1 GiB (measured: peak RSS 3 -> 1,026 MiB),
a few in parallel could get it OOM-killed.

- Each entry is read with room for no more than what is left of
  MAX_TOTAL (and its own 1 MiB cap), and refused as soon as it passes
  it: "the pack is over 8 MiB", or the file named when it alone is over.
  The same package now peaks at 6 MiB.
- A folder pack counts its total the same way.
- Tests: 64 MiB of zeros in a small zip, one file over its cap, and a
  folder over the total are all refused.

Found in yahya-nusseibeh's review of Horizon (H1, 7 Nov).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
2026-11-07 07:44:44 +02:00
k3yhandClaude Opus 5.5 29a511747d Selectors find cushions
A cushion is an ordinary entity in vanilla (a BlockAttachedEntity), but
Horizon had no command target for it: @e never matched one, so execute
if entity, tag, data get, tp and kill missed every cushion. Target::Cushion
joins the level's candidates like a painting; /kill removes it with no
drop and lets its rider go, /tp moves it with its block, data get writes
it through cushion_to_nbt.

Fixes #11626.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 00:44:33 -05:00
masa-masa-0314andZCode 9b524815b9 Realms: the invite-code list is an array or its result
InviteCodeList.parse reads a JSON array, or an object whose 'result' is
one, dropping entries without a code; the port waited for an
'inviteCodes' object the service never sends, so the codes screen
listed nothing to manage.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:43:42 +08:00
masa-masa-0314andZCode 4b1561050d Realms: the detail is the screen's server, and POSTs say json
- RealmsConfigureWorldScreen.fetchServerData replaces its serverData with
  getOwnRealm's answer; the port kept the server list's copy, whose
  regionSelectionPreference is stale, so a saved region read back as
  automatic. The detail answer now becomes the screen's server.
- Request.post sends Content-Type: application/json; the frontend
  answered 415 to the invite codes' text/plain POST.
- RealmsPreferredRegionSelectionScreen draws each region's ServiceQuality
  icon (ServiceQuality.getIcon, 10x8 at the row's right).

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:42:26 +08:00
Horizon agent ef3856a408 Merge remote-tracking branch 'origin/main' into n/10024
# Conflicts:
#	crates/horizon-client/src/client_lib.rs
#	crates/horizon-client/src/game.rs
2026-11-07 06:42:20 +01:00
masa-masa-0314andZCode 8d50119388 Realms: the region screen carries the settings tab's data back
Done used to rebuild the Configure screen with detail and tier cleared,
so the save that leaving the tab triggers found no slot to send and the
chosen region was dropped. The screen now carries both back, and the
save runs on the data it had.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:41:51 +08:00
masa-masa-0314andZCode 6f4a6a3865 Realms: the region rows answer a click
The click arm that handles the screens' drawn rows was guarded to
SubScreen::Configure, so the region preference's rows (and the create
screen's fields) never saw a press. The guard now covers the screens
whose rows it handles; the region selection draws extractSelection's
black entry with its white outline and the footer is vanilla's
150-wide Done/Cancel pair.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:41:51 +08:00
MSedgeMC 3afa8e234b port_map: the two saved-start rows say which piece ids are read
PiecesContainer.load now names saved_piece_id, where the rename table lives,
and StructureStart.loadStaticStart spells out that a jigsaw start (the renamed
old ids included) comes back from its saved pieces while another structure's
start comes back from the generator's rebuild of its chunk.
2026-11-07 05:41:18 +00:00
MSedgeMC efe0b28747 test: the four old jigsaw piece ids are read, other piece types are not
A piece tag named minecraft:nvi, pcp, bastionremnant or runtime comes back as
jigsaw, as does minecraft:jigsaw itself; a monument, stronghold or jungle
temple piece (OMB, SHCC, TeJP) comes back as nothing so the start keeps the
generator's rebuild, and so does a tag with no id or one that is not a
compound. With the table narrowed to minecraft:jigsaw alone the first four
fail.
2026-11-07 05:41:18 +00:00
MSedgeMC c7d9d1aedd fix: a saved start's pieces use the ids an old save wrote for jigsaw
PiecesContainer.load maps four piece ids - minecraft:nvi, minecraft:pcp,
minecraft:bastionremnant and minecraft:runtime - to minecraft:jigsaw before it
looks the type up: a save from before those pieces were jigsaw wrote them under
the older names with the same pool-element tag. The saved-start reader only
took the literal minecraft:jigsaw id, so such a start fell through to the
generator's rebuild of its chunk, and where that rebuild makes a different
layout (a world whose structure data or version moved on) the start came back
with no pieces at all: nothing placed, nothing bearding the terrain.

saved_piece_id now holds that rename table for the reader; a piece of another
type (a monument's or a stronghold's, whose NBT is not a pool piece's) still
sends the start to the rebuild.
2026-11-07 05:41:18 +00:00
masa-masa-0314andZCode ffa3a4d413 Realms: a slot setting's value reads as Gson's getAsString
The service sends settings like {"name":"hardcore","value":false},
a JSON bool where the DTO says string; Gson's JsonPrimitive.getAsString
accepts it, serde did not, and the detail answer the Worlds and Players
tabs need failed to parse.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:40:25 +08:00
masa-masa-0314andZCode aaccb9b51e Realms: the subscription is at subscriptions/{id}, not worlds/{id}/subscription
RealmsClient.subscriptionFor builds 'subscriptions/'; the port's
'worlds/{id}/subscription' answered 404, which the configure screen showed
as a fatal service error instead of its tabs.

Co-Authored-By: ZCode <[email protected]>
2026-11-07 13:40:25 +08:00
Horizon agent 06ca1059f8 Merge remote-tracking branch 'origin/main' into n/9991
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
#	crates/horizon-client/src/mobs/living.rs
#	tools/port_map/game-004.tsv
2026-11-07 06:40:22 +01:00
Horizon agentandClaude Opus 5.5 4a951a3199 Drop the harness run log from docs/parity
The harness and its tests hold what the log recorded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:38:27 +01:00
Horizon agent b09ef7084f Merge remote-tracking branch 'origin/main' into n/10130
# Conflicts:
#	crates/horizon-client/src/game/scoreboard.rs
2026-11-07 06:37:08 +01:00
Horizon agent b4c2f89660 Merge remote-tracking branch 'origin/main' into n/9967
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 06:36:08 +01:00
Horizon agent b0ae048383 Merge remote-tracking branch 'origin/main' into n/10031
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 06:34:52 +01:00
GabrielNichols 4b753f5956 Report Recipes warnings at their visible tooltip bounds 2026-11-07 02:33:12 -03:00
Florian ERNSTandCodex 6c6efb8d74 Refresh loaded sleeper position from bed geometry on first tick
Co-Authored-By: Codex <[email protected]>
2026-11-07 06:31:19 +01:00
Horizon agentandClaude Opus 5.5 880f8c6b56 Keep an effect's show_icon apart from its particles
MobEffectInstance.showIcon was not modelled: every effect showed its HUD
icon and every UpdateMobEffect carried the icon flag. Warden Darkness
(no particles, no icon) showed an icon. The flag now follows vanilla's
constructors, update, saves (show_icon defaults to show_particles),
consume effects and the packet flags (#11385).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 06:16:04 +01:00
TheMrZZandCodex gpt-6.1-sol fc711c3f6d fix: save pot sherd string templates as item compounds
Encode bare item templates using the primary record form while preserving
compound counts and components. Cover actual canonical load/save and the
real command route against a frozen fresh Java reference.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 06:14:22 +01:00
rdbtandClaude Opus 5.5 d2b41ed400 Parity harnesses: a generate step that died is not reused
respawn_world_point and disconnect_crafting reused generate/world whenever the
folder existed, so one crash left them failing until someone deleted it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:09:02 +00:00
rdbtandClaude Opus 5.5 7c5466e763 lunge_motion: the tick after a jab is not a catch-up before it
Since #11223 the server takes what follows a jab on the next tick, so the
harness's time query comes back a tick later and every jump failed as a
catch-up. The motions are identical to vanilla's and stay compared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:06:46 +00:00
rdbtandClaude Opus 5.5 f670d71c41 Parity harnesses: mob_discard, mob_local_sound and ominous_warning_sound run on a plain run
tools/parity.py runs a harness with no arguments, and these three required
--binary, --work-dir or --resources (mob_discard also used os without importing it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 04:52:13 +00:00
martipopsandClaude Opus 5.5 1edfd2f7ea Merge main into controller/guide
main removed the HUD's bar_height; the guide's guide_from stays.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:46:42 -05:00
k3yhandClaude Opus 5.5 945f40e8ff /ride takes armor stands, displays, interactions and markers as vehicles
RideCommand.mount forces the ride (startRiding(vehicle, true, true)), so
any entity takes a rider. Horizon only took boats, carts and mobs. A rider
now sits at the vehicle's default passenger attachment (its box height,
less the rider's own vehicle attachment) and gets off on sneak on top of
it, as Entity does for a vehicle that overrides neither.

Fixes #11603.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:40:02 -05:00
GabrielNichols 6fbbfdde1c Include the Recipes transfer translations 2026-11-07 01:38:12 -03:00
Florian ERNSTandCodex 4aeb14b3ba Load active world-pack enchantment definitions
Co-Authored-By: Codex <[email protected]>
2026-11-07 05:35:26 +01:00
levzzzandCodex 1301820f0d Run movement parity through the gameplay library tests
Select the library artifact that owns Game, retaining the exact one-test assertion so a thin executable with no tests cannot count as a pass.

Co-Authored-By: Codex <[email protected]>
2026-11-07 09:29:40 +05:00
voidsip b888cebdf6 Merge maintainer parity import correction 2026-11-06 22:24:07 -06:00
voidsip 337c5bbd4d Merge maintainer parity import correction 2026-11-06 22:24:05 -06:00
Horizon agentandClaude Opus 5.5 aef2dc6528 tests: load the parity drivers as tools.* in the CLI admission test
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:22:44 +01:00
Horizon agentandClaude Opus 5.5 65c57efffe tests: load the parity drivers as tools.* in the CLI admission test
Under `unittest discover -s tests`, the bare name test_rust_parity is
tests/test_rust_parity.py, so the drivers' fallback import took the wrong
module and four tests failed (no attribute verify_artifacts) in verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 05:22:20 +01:00
voidsip 446e36c1a6 Merge commit 'aae27797d94680928d6767419a51b1838b427e73' into fix/round3-1-wet-fire-order 2026-11-06 22:17:51 -06:00
voidsip 4efc3f6bb1 Merge commit 'aae27797d94680928d6767419a51b1838b427e73' into fix/mob-water-breathing 2026-11-06 22:17:50 -06:00
voidsip aae27797d9 Isolate parity helper imports from unittest discovery names 2026-11-06 22:17:17 -06:00
k3yhandClaude Opus 5.5 b9854b1d82 /ride seats and unseats any server player, whoever runs it
RideCommand.mount and dismount work on any ServerPlayer in vanilla. Horizon's
/ride reached only the player in place, so the console, a function, a command
block or another player could not seat a guest (every player of a server or a
LAN world, and in single player too): the guest's turn now runs through
for_player, as native_mount does. A guest as the vehicle answers
cant_ride_players, as any player does.

Fixes #11602.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:16:44 -05:00
voidsip c59cb034a2 Merge commit 'ccb85ce454b9ea064dc4c5a99f95bdbea836ee35' into fix/round3-1-wet-fire-order 2026-11-06 21:49:42 -06:00
voidsip fa733b3efa Merge commit 'ccb85ce454b9ea064dc4c5a99f95bdbea836ee35' into fix/mob-water-breathing 2026-11-06 21:49:41 -06:00
voidsip ccb85ce454 Keep specialized parity harnesses compatible with normal CI invocation 2026-11-06 21:49:02 -06:00
levzzzandCodex 3cb56882d9 Match vanilla movement bounds and facing at numeric limits
Reuse the native summon spawn-bounds predicate so negative endpoints stay valid, and use Java's integer width for the yaw direction lookup. Record 26 independent outcomes from both pinned vanilla jars and add a differential harness for the actual native calculations.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:44:37 +05:00
bulkandClaude Sonnet 5.5 596a11fb08 Launcher: show patch notes in a dropdown under each version, drop the Patch notes tab
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 03:42:56 +00:00
TheMrZZandCodex gpt-6.1-sol f3fcb0ca25 fix: reject function tags with unresolved required references
Propagate required dependency failures before expanding function tags so
invalid tags cannot run partial members. Skip whole invalid optional children
while preserving the existing ordered, deduplicated expansion.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 04:38:01 +01:00
Florian ERNSTandCodex 3db0da6dd2 fix: resolve named predicates through the Game context
Bind loaded predicate references without changing source identity or origin. Defensively reject invalid reference graphs before boolean evaluation while keeping registry admission separate.

Co-Authored-By: Codex <[email protected]>
2026-11-07 04:24:58 +01:00
levzzzandCodex eba31f178c Keep native rotation on the rotation-only command path
Reuse the current forceSetRotation path so turning an entity preserves its mount, position and velocity. Extend the existing resource-backed rider regression.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:23:11 +05:00
levzzzandCodex e539cd328f Merge current main into native entity movement
Preserve the current teleport passenger handling and supply its rotation policy from the native movement methods.

Co-Authored-By: Codex <[email protected]>
2026-11-07 08:18:25 +05:00
voidsip f71a05a062 Keep the HUD mode export separate from client snapshots 2026-11-06 21:11:23 -06:00
voidsip 48886daeb6 Merge commit '1647c1b4664f4221b224e81ec800df4331f81bcc' into fix/11123-native-hud-mode
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
2026-11-06 20:57:05 -06:00
voidsip f423aeea61 Keep HUD mode accessors in the native helper section 2026-11-06 20:56:39 -06:00
Horizon agentandClaude Opus 5.5 745b95ace9 Look at the audio devices off the render thread
Audio::should_change_device listed the output devices on the render
thread once a second. Windows has no cheap fingerprint to skip the list
(Linux reads /proc/asound/cards, macOS names the devices), so every
second the render thread waited ~90-120 ms on WASAPI's endpoints, even in
the main menu (#11490). The look now runs on a short-lived thread every 20
ticks; a later tick applies what it found with the same rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:54:49 +01:00
Vinícius MoreiraandClaude Opus 5.5 dd4528f4e4 Environment attributes: the layer stack, biome blending and weather
Ports EnvironmentAttributeSystem: every attribute is its default run
through layers in the order they are added, leading constant layers folded
into the base (bakeLayerSampler) and the result sanitized. The dimension
type's attributes are constant layers; the biome layer reads the biome at
the position, or for spatially interpolated attributes the
SpatialAttributeInterpolator mix that GaussianSampler accumulates; the
weather layers (WeatherAttributes.RAIN and THUNDER) blend by the rain and
thunder levels with each type's state-change lerp. Timelines plug in as
time layers and come next; nothing in the game uses the stack yet.

Entries of types not ported yet (append, overlay) are kept as no-ops so a
biome's other attributes still load.

The new harness environment_system compares 900 stacks with the pinned
client (36,000 lines), with the vanilla biome and dimension type data.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:38:15 -03:00
bulkandClaude Opus 5.5 b9e891bf3e Remove the dead_code allows nothing needs, give the rest a reason
Removing each one and asking the compiler (the workspace's targets and the
server build): most guarded nothing, since `pub` methods of `pub` types already
count as used. The ones native mods need stay, now saying so, as do the fixtures
that share another fixture's source and code whose caller has not landed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:37:35 +00:00
Florian ERNSTandCodex 84f6959921 fix: require folder data-pack metadata sections
Reject folders without parseable metadata, an object pack section and a non-null description before exposing their contents. Preserve existing parser and format behavior.

Co-Authored-By: Codex <[email protected]>
2026-11-07 03:35:13 +01:00
Vinícius MoreiraandClaude Opus 5.5 f2c9af4827 Environment attributes: the registry and modifiers, checked against vanilla
Ports vanilla 26.3's environment attribute registry (EnvironmentAttributes:
types, defaults, ranges) and one EnvironmentAttributeMap entry: a value or
a {modifier, argument} object, parsed as its codec does (hex, list and int
colors, FloatWithAlpha, BlendToGray) and applied as AttributeModifier.apply
with float math in Java's order (ARGB.alphaBlend, addRgb, subtractRgb,
multiply, greyscale, scaleRGB, srgbLerp; Math.min/max). The layer stack
that sums these comes next; nothing uses the module yet.

The new harness environment_attribute_modifier compares 8,835 cases with
the pinned client, the registry included.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:07:36 -03:00
Horizon agentandClaude Opus 5.5 e494a9d1f5 rustfmt the boat Ride condition
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 03:06:44 +01:00
bulkandClaude Opus 5.5 898d7a0b4a Drop the duplicate dependencies an upgrade can remove
arboard 3.7 moves to windows-sys 0.61 (removing 0.60, windows-targets 0.53 and
its eight platform crates). socket2 0.6 and if-addrs 0.15 in horizon-friends
remove socket2 0.5; the launcher's lock also loses windows-sys 0.59.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:55:58 +00:00
Florian ERNSTandCodex 0de6eeb024 fix: reject functions containing invalid ordinary commands
Validate candidate functions at actual compilation permission before exposing them, using refreshed world content and candidate registry IDs.

Co-Authored-By: Codex <[email protected]>
2026-11-07 02:55:05 +01:00
TheMrZZandCodex gpt-6.1-sol bca4a22e08 fix: project live player motion fields into saved data
Carry the active Player grace time and fall-particle flag through the existing
save context, restore owner snapshots after guest calls, and write the official
empty player Brain schema. Keep tracker projection outside this change.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 02:51:56 +01:00
Horizon agentandClaude Opus 5.5 3fc2ed796f Controllers: the guide's boat Ride drops the boarding cooldown main removed
#11158 took the player's boarding cooldown out (Player.removeVehicle zeroes it), and
may_board with it, so this branch no longer built once merged with main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:51:40 +01:00
DetimesandClaude Opus 5.5 fa9d06a48f Older clients and servers: 1.21.4, 1.21.3, 1.21.1 and 1.20.6 (#10404)
Id tables from the vanilla data reports (1.20.6's packet list rebuilt from
the archived wiki page, which names no resources, via 1.21.1's report), with
the renames game_profile, set_carried_item and horse_screen_open. Packet
layouts from minecraft.wiki's archived pages for protocols 766 to 769 and the
release notes: 1.21.5's chunk format and per-entity data, hashed container
clicks, 1.21.2's window ids, teleports, movement flags, input and sea level,
1.21's particles, explosion sound and trades, 1.20.5's strict error handling.
1.21.5 item components (attribute modifier display, equippable shearing)
behind a quirk. A 1.21.1 client gets no recipe book.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:46:59 +00:00
Kevin RamirezandClaude Fable 5.1 3aadba6aba CI: run verify again after a wall-clock test flaked on the slow runner
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 19:43:57 -06:00
Horizon agentandClaude Opus 5.5 0da0d2b260 Merge main into #10383: keep the macro-type and mob-name corpus groups beside treasure map copies
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:40:25 +01:00
voidsip 8c991c7472 Isolate trade parity runs and format modifiers 2026-11-06 19:39:51 -06:00
socksproxandClaude Opus 5.5 be8d3d59d9 horizon-client: discord, friends and heartbeat features
Discord Rich Presence, Play with Friends (horizon-friends) and the
anonymous "I'm playing" ping (horizon-telemetry/live) were built
unconditionally, though a server never runs them. Each is now a
default-on feature of horizon-client and horizon-client-lib, so the
normal build is unchanged and the server build (--no-default-features,
horizon-engine) leaves them out: horizon-friends and 13 crypto and
network crates drop from its dependency tree.

Without `friends` the game keeps working: game/friends_off.rs stubs the
host side (no Friends button in the pause menu, as on another server),
no code reads as an invite, and a friend: address is refused as an
invalid address.

Fixes #11539

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:34:02 +01:00
bulkandClaude Opus 5.5 a2727a069e Apply the lints the raised rust-version turns on
Clippy follows rust-version, so crates that move from 1.85 now get its
collapsible let chains, as_chunks and is_multiple_of suggestions. Almost all
from `cargo clippy --fix`, none changes what the code does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:29:01 +00:00
rdbtandClaude Opus 5.5 ac3436923a CheckerboardBiomeSource: a negative scale reaches the shift unclamped, as in vanilla
#11226 clamped scale to 0 before the + 2. Vanilla has no clamp and Java masks the shift
distance, so tools/test_biome_source_parity.py failed at scale -1 and below.

Fixes #11519

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:25:45 +00:00
Florian ERNSTandCodex 593e9e05cc Carry actual loot-mine executor presence into block drops
Co-Authored-By: Codex <[email protected]>
2026-11-07 02:20:33 +01:00
voidsip e03f69d607 fix: spill contents of items destroyed by explosions 2026-11-06 19:16:27 -06:00
rdbtandClaude Opus 5.5 a3c17924ca Parity harnesses: loaded_pig_tags and loot_mine_pot_components run on a plain run
Both asserted --reference-only when no --reference-dir was given, so tools/parity.py,
which passes no arguments, failed them before they ran anything.

Fixes #11520

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:15:56 +00:00
bulkandClaude Opus 5.5 fab81be9ac Build every workspace crate against one rust-version
Members declared 1.85 to 1.95, or nothing. horizon-mods needs 1.95 and sits
under horizon-client-lib, cargo-horizon-mod and the launcher, so 1.95 is the
version the game really needs; the lower numbers promised builds that could
not happen. Set it once in [workspace.package] and inherit it everywhere.

The separately built workspaces (launcher, telemetry server, mod registry,
third_party) cannot inherit it and keep their own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:14:44 +00:00
Horizon agentandClaude Opus 5.5 9ffa81a748 Items: read a stack's components in place instead of copying its item's defaults
ItemModels::components cloned the item's whole default component map (an
enchanted sword's: a few dozen JSON values and strings) for every stack
with a patch, and the model resolver did it on every draw: the hotbar,
each held item of every player in view, the first-person hand twice. A
PvP fight with enchanted gear paid it a dozen times a frame.

ComponentsView reads the patch, then the defaults, with no copy; the
resolver, the swap animation scale, attack range, use duration and the
use animation read through it. components() stays for what needs the
merged map (bundle fullness, still built only for bundles).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:09:17 +01:00
Horizon agentandClaude Opus 5.5 7ce65988b2 Items: look an enchanted GUI item's glint sprite up once per sprite, not per quad
submit_quads hashed the quad's sprite name into the glint atlas for every
quad of an enchanted item drawn in the GUI (a generated sword: hundreds of
side quads, one sprite). It now keeps the last sprite's glint rect, as it
already does for the item atlas rect: the same uvs, one lookup a sprite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:08:35 +01:00
bulkandClaude Opus 5.5 71d809e40c AGENTS.md: drop the counts that go stale
The harness, probe, Game field, impl file and module counts had drifted
(324 harnesses against 502 on disk). Nothing regenerates prose, so name the
things and let the tree give the numbers. CONTRIBUTING.md's "about 290"
harnesses goes with them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:05:27 +00:00
Horizon agentandClaude Opus 5.5 818c598db9 Text displays: animated text is never cached, and a culled one keeps its glyphs
Follow-up to #10571 (Skr1zzzz). The cache checked obfuscation on the raw
spans, so a plugin hologram sending §k in literal JSON stopped
animating, and an inline head whose skin arrives later stayed stale: the
animated test now runs once on the resolved text, as drawing reads it.
The key also holds lang::bidirectional() as the name tag cache does, and
a display out of view keeps its cached quads instead of dropping them
before the view check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:01:34 +01:00
Horizon agent eadca0ce30 Merge main into #10242
# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-07 02:01:07 +01:00
Horizon agentandClaude Opus 5.5 213cfce453 Launcher: the macOS title bar keeps its height without glass
#10129 made the native title bar transparent on macOS but drew the bar only with
glass on, so with glass off the page started under the traffic lights. The bar
is always there: glass, or the side colour as on Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:00:38 +01:00
Horizon agentandClaude Opus 5.5 8b7f726609 Fullscreen: unmaximize first on Windows only, in one helper
#10177 unmaximized before every fullscreen request on every system, and apply()
asked for fullscreen again on each call while the window still reported
maximized. enter_fullscreen keeps the #9984 fix where it applies (winit keeps a
maximized window inside the work area on Windows); the resumed() checks share
one helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 02:00:24 +01:00
Horizon agent 3e88f5909b Merge main into #10210
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 02:00:23 +01:00
Horizon agentandClaude Opus 5.5 d59814a196 Shader packs: a pack build no longer wanted stops (follow-up to #10532)
#10532 builds a pack on a thread of its own, but dropping PackLoading (a
pack switch, packs off, another dimension) only detached that thread,
which went on for 5 to 50 s; switching back and forth stacked builds.
PackLoading now holds a cancel flag set when it is dropped: PackPost::new
checks it per program and compile() stops handing out jobs, and the
refusal it returns (is_cancelled) is not reported as the pack failing.
A thread that cannot be spawned now turns the pack off for the
dimension instead of retranslating it every frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:32 +01:00
Horizon agentandClaude Opus 5.5 18309e4967 Remote servers: translate the SSH errors
#10166 landed with its messages as raw strings. Every message a player reads
goes through tr/trf with named placeholders, and every language file gets the
English stand-ins.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:25 +01:00
Horizon agentandClaude Opus 5.5 955c1ba290 Merge main into #10531; the sky parts are pipelines of the compile too
Main gave a sky program two more pipelines (its vanilla blends for the
disc and void, and for the stars). Here they are RenderJobs made with the
others, in their own slots, and the draw reads them with get().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:59:15 +01:00
Horizon agent d8676fff04 Merge main into #10538
# Conflicts:
#	crates/horizon-client-render/src/gfx/pack_post.rs
2026-11-07 01:57:46 +01:00
Horizon agent ab6900b4c9 Merge main into #10283
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 01:57:18 +01:00
Horizon agent 34d078e57b Merge main into #10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-07 01:56:19 +01:00
Horizon agentandClaude Opus 5.5 1ddc0b8a4d Flame crossbow test: a Flame arrow burns for 2000 ticks
a_flame_crossbow_shoots_burning_arrows (#10488) expected 100 fire ticks and failed with prepared resources. Enchantments.FLAME is Ignite(LevelBasedValue.constant(100.0F)), and Ignite calls igniteForSeconds: 100 seconds, 2000 ticks, as the bow path already gives. The code was right; only the expectation in the test was off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:54:10 +01:00
Horizon agent 868f88a2ce Merge main into #10570 2026-11-07 01:54:00 +01:00
Horizon agent d64be662fe Merge main into #10559 2026-11-07 01:53:52 +01:00
Horizon agent 1d63e80fc1 Merge main into #10553 2026-11-07 01:53:49 +01:00
Horizon agent 3b3f6aa00f Merge main into #10552 2026-11-07 01:53:46 +01:00
Kevin RamirezandClaude Fable 5.1 69ceb53897 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:53:43 -06:00
Horizon agent 58e32ea813 Merge main into #10280 2026-11-07 01:53:40 +01:00
Horizon agent e2268eb854 Merge main into #10201 2026-11-07 01:53:35 +01:00
Horizon agent 5f2f0e689a Merge remote-tracking branch 'origin/main' into fix/mob-entity-collisions-10445 2026-11-07 01:53:25 +01:00
Horizon agentandClaude Opus 5.5 1c42629d64 mob_ai probe: fill MobWorld's others_solid
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 01:52:08 +01:00
Horizon agent a892d2d1eb Merge remote-tracking branch 'origin/main' into fix-11022
# Conflicts:
#	crates/horizon-client/src/game/sculk.rs
2026-11-07 01:50:41 +01:00
Horizon agent 74643df024 Merge remote-tracking branch 'origin/main' into fix-11017
# Conflicts:
#	tools/test_command_exec_parity.py
2026-11-07 01:50:40 +01:00
Horizon agent 07c0d4e8fb Merge remote-tracking branch 'origin/main' into fix-10837
# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-07 01:50:40 +01:00
martipopsandClaude Opus 5.5 ecd3764d93 Settings shell: leaving search gives the keys back, and fixes from reviewing it
Once the search field had the focus it kept the keyboard, so Enter did nothing and letters filtered
the list with the ring on a row. Moving the focus away now ends typing.

A key that moves nothing shows no ring, a row greyed out while focused does nothing, the wheel stops
behind a resource error as `scroll_at` does, and a description too long for the strip ends in an
ellipsis, with room for one more line on Extras.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:50:14 -05:00
Florian ERNSTandCodex 8b9e8417af Preserve loaded Camel pose timestamp boundaries
Co-Authored-By: Codex <[email protected]>
2026-11-07 01:42:37 +01:00
TaquitoSuabe 8bfcf86e6d net: configure TCP keepalive and zero-alloc stream pumps 2026-11-06 17:34:49 -07:00
Kevin RamirezandClaude Fable 5.1 1426129516 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:33:12 -06:00
voidsip a932bc6c86 Create recipe directory for isolated smithing menu fixture 2026-11-06 18:28:12 -06:00
Kevin RamirezandClaude Fable 5.1 747b958bd9 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:23:06 -06:00
martipopsandClaude Opus 5.5 7f1eef46b2 Settings shell: a screen describes its rows, and Horizon Extras moves onto it
A screen now implements settings_shell::Screen, describing its pages, groups,
rows and values, and Shell does the drawing, clicks, scrolling, the controller's
focus and declared buttons, keys and each row's description. OptionsUi reaches
it through one accessor, so the wheel and a stick's scrolling can't part ways.

Horizon Extras is the first screen on it. Its settings do what they did and are
saved the same way. It gains the shell's look and search, and its descriptions
move from tooltips to the strip, which grows for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:21:17 -05:00
voidsip 001f3d008d Clarify the wind hook preserves shuffle random draws 2026-11-06 18:19:07 -06:00
Vinícius MoreiraandClaude Opus 5.5 c80eac9506 Polytone: read targets and biome/dimension attribute modifiers
A new crate, horizon-polytone, reads the Polytone resource pack format
clean-room from the public wiki: modifier targets (implicit path id, ids,
tags, regex, required) and the attributes_modifiers layers of biome and
dimension modifiers (groups, REMOVE, values, modifier objects, colors,
colormap references, expressions kept as source). Nothing uses it yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:17:25 -03:00
DetimesandClaude Opus 5.5 edeee4d656 Older clients and servers: 1.21.8, 1.21.6 and 1.21.5 (#10404)
Packet and registry id tables generated from the vanilla data reports of the
three releases; packet layouts from the public protocol documentation
(minecraft.wiki's archived packet pages for protocols 770, 771 and 772).
Each step has a reader (older server) and a writer (older client) with round
trips; entity data serializers without 1.21.9's additions. The end-to-end
bot test now joins as every release down to 1.21.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 00:15:18 +00:00
iseaprojectsandOpenCode a0d291e2bb i18n(es_es): complete the Spanish strings after en_us grew to 894 keys
es_es.json held 271 of the 894 keys en_us.json now ships, so 623 strings
(70% of the file) fell back to English on Spanish screens: the whole PvP
and HUD page, splitscreen setup and profiles, Horizon extras, the mod
browser, the resource pack browser, friends, the on-screen keyboard,
Replay Studio, the music page, cosmetics and the shader and graphics
descriptions.

This fills the table: 894/894 keys, no missing or extra ones, every
placeholder kept. The four values that were still English (Particle
Culling and its three descriptions) are translated, and the shader page
name now says "shaders" like the rest of the file.

lang.rs also serves es_ar, es_cl, es_ec, es_mx, es_uy and es_ve from this
file.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 20:08:07 -04:00
Florian ERNSTandCodex 2aa9308e47 Reject stackable damageable item command inputs
Co-Authored-By: Codex <[email protected]>
2026-11-07 01:04:42 +01:00
Kevin RamirezandClaude Fable 5.1 23c572b374 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 18:03:09 -06:00
RINNECODERandClaude Opus 5.5 4ed6650b88 Port pack: carried models name 26.3's iron chain
A 1.21.1 model's minecraft:block/chain or item/chain (parent or texture) became
iron_chain in 26.3, so a mod block drawing vanilla's chain baked with the missing
texture. Carried models and block states get the RENAMED_IDS rename data files
already had.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:02:48 -06:00
RINNECODERandClaude Opus 5.5 a124aba31a /recipe suggests content mods' recipes
The recipe index reads every content namespace since 31756ebfe, but /recipe's
suggestions still listed only minecraft: ids. List data/<ns>/recipe of every
namespace, as /loot's loot tables already are.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:02:48 -06:00
voidsip 8158bcae73 Use the item codec crate in the mining parity probe 2026-11-06 17:45:50 -06:00
voidsip 7397241fb6 Give debug wind fixtures an explicit stack budget 2026-11-06 17:43:29 -06:00
Kevin RamirezandClaude Fable 5.1 733d3abd7e CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:43:05 -06:00
TheMrZZandCodex gpt-6.1-sol 8bb5a572ec Advance eligible TraderLlama despawn delay with NoAI
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-07 00:42:42 +01:00
voidsip 62724ffccf Record scoped mining parity source and artifact hashes 2026-11-06 17:41:16 -06:00
martipopsandClaude Opus 5.5 f8a7a59085 Controllers: Replay Studio's recordings and buttons take the focus
Its rows and buttons only painted, so a pad reached them through the crosshair alone. The D-pad now
walks the recordings to the buttons, the right stick scrolls them, and Done declares its button.

A notch is now one whole row, since nothing clips a row that's partly scrolled off, and the scroll
limit counts the rows that fit, so the last recording can be reached with the mouse too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:39:17 -05:00
DUzzL 52105d2c9b Adventure mode: gate placed entities and firework on can_place_on 2026-11-07 00:38:11 +01:00
voidsip 9782763e82 Cover mining component admission and explicit break slots 2026-11-06 17:35:52 -06:00
voidsip 2eb6044dfc Keep remote mining fixture compatible with speed cache 2026-11-06 17:34:56 -06:00
Florian ERNSTandCodex dd47001112 Record signed equine temper proof and scope
Co-Authored-By: Codex <[email protected]>
2026-11-07 00:32:23 +01:00
DUzzL 330c59d954 Resend the glide flag when the server refuses a glide start 2026-11-07 00:30:33 +01:00
Evrim Cicek 02e7620924 Merge remote-tracking branch 'upstream/main' into fix/tnt-fuse-any-number
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 02:30:18 +03:00
Evrim Cicek 71b527b7c2 Merge remote-tracking branch 'upstream/main' into fix/enderman-hurt-teleport-now
# Conflicts:
#	tools/command_exec_corpus.py
2026-11-07 02:30:09 +03:00
DetimesandClaude Opus 5.5 9d4cecee14 Older clients: serve 26.1, 1.21.11 and 1.21.10 clients too (#10404)
Mirror translate's per-release readers as writers (client_session/step_*),
keep sent entities for entity data and 26.1's spectate by UUID, drop the
world_clock registry and split 1.21.11's interact into interact and attack.
End-to-end test joins a bot of each release through the adapter's Session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:29:51 +00:00
Kevin RamirezandClaude Fable 5.1 6564896c45 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:23:11 -06:00
k3yhandClaude Opus 5.5 a456a1476c Worldgen: name the placed features that cannot run
The warning only gave their count, so a data pack or content mod author
could not tell which features were skipped or why. Log each one with its
reason, up to 20.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:22:02 -05:00
MSedgeMC 85a97ed37c Re-run CI: the previous run failed in setup with the self-hosted runner out of disk 2026-11-06 23:13:26 +00:00
baytyoneandZCode 91b3a7410e Experience orbs: pickup over the vehicle, a kept following player, merges before the move, eye-level water
Vanilla's Player.aiStep widens the pickup area to the union with the vehicle's box while riding; ExperienceOrb.tick keeps its followingPlayer until they leave 8 blocks, runs scanForMerges in its own turn before the move, and floats by the eye's fluid rather than the feet's block. Fixes #11389.

Co-Authored-By: ZCode (GLM) <[email protected]>
2026-11-07 04:12:29 +05:00
Florian ERNSTandCodex b775c66123 Preserve signed equine temper independently of gameplay limits
Co-Authored-By: Codex <[email protected]>
2026-11-07 00:10:40 +01:00
voidsip e3295a9974 Trim gateway fixture trailing blank line 2026-11-06 17:10:23 -06:00
rison1337andCodex d15d01fbc2 perf(render): keep offscreen flame culling active with TAA
Use a conservative input-pixel guard for the billboard camera frustum
without changing the ordinary model frustum, flame envelope or shader
pack fallback. Keep this PR independent of experience-orb culling.

Retain the separate flame measurements and image controls, with their
original binary provenance and an explicit note about the PR split.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 04:04:56 +05:00
MSedgeMC b918309242 port_map: a vindicator's Johnny flag is latched, saved and read
`tools/port_map/game2-004.tsv` recorded
`Vindicator.addAdditionalSaveData` as `missing` and `Vindicator.setCustomName`'s
gap as "the Johnny goal reads the current custom name each tick; vanilla latches
the flag once a name is Johnny". Both rows now name what the port does, with a
row for `readAdditionalSaveData` beside them.
2026-11-06 23:04:15 +00:00
MSedgeMC 68f5909ef6 test: a vindicator's Johnny latch through a rename and a save
`vindicator_server_ai_step` is the AI step, and `mob_to_nbt`/`mob_from_nbt` are
the pair `Game::mob_tree_nbt` and `Game::load_mob` go through. A vindicator
named Bob is not Johnny; named Johnny it is; renamed to Bob it stays Johnny.
The save writes `Johnny: 1b`, the reloaded vindicator is still Johnny with the
name Bob, and a compound with no `Johnny` (its `CustomName` reading Johnny)
loads false and saves `Johnny: 0b`.

Without the latch the test stops at the first name: `left: false`, `right:
true`.
2026-11-06 23:04:15 +00:00
MSedgeMC 995a99b984 fix: a vindicator that was named Johnny stays Johnny
`Vindicator.setCustomName` latches a flag the first time the name it is given is
`Johnny`, and nothing ever clears it:

    public void setCustomName(@Nullable Component name) {
       super.setCustomName(name);
       if (!this.isJohnny && name != null && name.getString().equals("Johnny")) {
          this.isJohnny = true;
       }
    }

`VindicatorJohnnyAttackGoal` starts for `isJohnny` (`addAdditionalSaveData`
writes it, `readAdditionalSaveData` reads it back with `getBooleanOr("Johnny",
false)`), so a vindicator renamed from Johnny to Bob keeps attacking every
living entity, and one whose save says `Johnny: 1b` does so after a reload.

Horizon read the live custom name in the goal's `when` each tick and saved no
flag, so a renamed vindicator stopped attacking everything, and a reloaded
vindicator that had been named Johnny but is now Bob never started.

`extra.johnny` is the latch, taken from the live name in
`vindicator_server_ai_step` (the AI step is where a name set since the last step
is seen, and it never clears), read by the goal, written as `Johnny` and read
back from it. A save with no `Johnny` loads false, whatever the name in the
compound, as `getBooleanOr` does.
2026-11-06 23:04:14 +00:00
Kevin RamirezandClaude Fable 5.1 c14c063507 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 17:03:06 -06:00
voidsip 8554155932 Remove obsolete saved gateway arrival helper 2026-11-06 17:00:00 -06:00
rison1337andCodex 539c847e65 perf(items): avoid unused dropped-item bounds work
Reuse resolved local model depth when placing dropped-item stack copies.
Skip the unused posed occlusion envelope with entity culling and TAA off,
and retain the original preparation under TAA after measured tail regressions.

Keep the existing cluster API and random sequence, with a regression test,
paired production measurements and explicit rendering-validation limits.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 03:58:08 +05:00
MSedgeMC d114d2ef89 test: a stub with inline data or no .mcc still reads the .mcc
Without the change the first reads `Err(InvalidData, "external chunk stub has
inline data")` and the second `Err(Os { code: 2, kind: NotFound })` instead of
the 4000 bytes in `c.-32.0.mcc` and an absent chunk.
2026-11-06 22:56:48 +00:00
MSedgeMC 30a3b019ff fix: read a chunk's external stream the way RegionFile reads it
`RegionFile.getChunkDataInputStream` never trusts the stub's length once the
external flag is set: `streamLength != 0` only logs "Chunk has both internal and
external streams" and the stream comes from the `.mcc`. When the `.mcc` is not a
regular file, `createExternalChunkInputStream` answers `null`, so the chunk is
absent. Horizon refused both: a stub that also declared inline data failed with
"external chunk stub has inline data", and a stub whose `.mcc` was gone failed
with a `NotFound` io error, which aborts a reader where vanilla skips the chunk.
2026-11-06 22:56:47 +00:00
voidsip 4807f13288 Format world border regression assertions 2026-11-06 16:56:09 -06:00
GabrielNichols fe28bf9738 Route Recipes shortcuts in Creative inventory 2026-11-06 19:54:08 -03:00
TheMrZZandCodex gpt-6.1-sol 1d72cf2ed8 Discard loaded falling air before age and physics
Use the existing air predicate and Gone result at the start of the actual falling-block tick. Cover admitted command actors with a real Game tick regression and frozen Java parity controls.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 23:53:32 +01:00
brasgaandClaude Opus 5.5 5b9fe567ec Player predicates test food level and saturation (#11314)
PlayerPredicate.matches calls FoodPredicate.matches on the player's
FoodData: the food level in `level`, then the saturation, a float
compared as a double, in `saturation`. The player facts carried no food
and the matcher never read `food`, so any food bounds passed.

PlayerFacts now holds the food, always filled (two numbers), for the
player in place and for a server's other players, and player_facts tests
it after the level, as vanilla does.

The command_exec harness gets a group that fills and drains the player's
food in player loops and tests named food predicates, a non-player, and
a tick advancement whose player condition asks only for food.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:53:29 -03:00
MSedgeMC 6a5fc04295 test: a re-saved section writes only the states a cell holds
Both cases read the emitted tag, so they fail on the live palette: 342 longs
of `data` where vanilla's `SimpleBitStorage(4, 4096)` is 256, a palette of
`[0, 1, … 16]` where first use is `[1, … 15, 0]`, and `(2, Some(256))` where a
section edited back to one state is `(1, None)`.
2026-11-06 22:49:30 +00:00
DUzzL 9558ecf848 Keep the copper golem's name on its statue 2026-11-06 23:49:15 +01:00
MSedgeMC 5dc59396d6 fix: save a chunk section's palette as vanilla packs it
`PalettedContainer.pack` re-encodes the cells into a fresh palette before a
section is written: a state no cell holds any more is dropped, the entries are
in first-use order, and the bit count is `getConfigurationForPaletteSize` of
the states that remain. `PalettedContainer::to_nbt` wrote the live palette
instead, which keeps the slot of every state a block edit replaced (the `free`
list needs it) and packs at that wider count: a section whose 17th state was
edited away saved 342 longs where vanilla writes 256, and a section edited back
to a single state saved a 2-entry palette with a 256-long `data` where vanilla
writes one entry and no `data`.
2026-11-06 22:47:00 +00:00
Kevin RamirezandClaude Fable 5.1 d6802414fa CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:43:06 -06:00
DUzzL a7d2edbfb9 Beacon: select players by bounding-box overlap with the effect box 2026-11-06 23:37:57 +01:00
GabrielNichols 99a593a6bf Match the item browser header and Creative transfer affordance 2026-11-06 19:37:54 -03:00
martipopsandClaude Opus 5.5 c752fee870 Controllers: Play in Background, rumble in splitscreen, the keyboard after a new renderer
Play in Background, off by default and per player, lets the pad keep driving the game while its
window has no focus, and a game the pad is playing doesn't pause on losing focus.

While several play, the router now plays each player's rumble on that player's pad. Rumble no
longer stops when the window loses focus: nothing asked for that, and it's useful to an AFK player.

Starting or leaving splitscreen builds a new renderer, whose atlas has none of what was drawn into
it at run time. Those two paths only forgot the skins, so the on-screen keyboard lost its letters
and rounded corners, and maps were exposed the same way. All renderer swaps now forget the same
things.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:37:00 -05:00
Nikita Maximilian GrimmandClaude Opus 5.5 5d44519d5b Frame identity: keep the player on a block while the scenes are built
Left in the air at y 163, the player fell into the sea below while prepare() ran, which unlocked the boat
recipes: their toast showed over the first shots at a different moment each run, so main against itself
failed in the top right corner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:27:38 +01:00
MSedgeMC 3e484d6adf port_map: Armadillo's state is saved and read
`tools/port_map/animal-1.tsv` had no answer for
`Armadillo.addAdditionalSaveData` or `Armadillo.readAdditionalSaveData`. Both
now name `entity_nbt::mob_to_nbt` / `mob_from_nbt`: the state written as its
`ArmadilloState` name beside `scute_time`, and read back through
`switchToState`, idle when the save names none.
2026-11-06 22:27:24 +00:00
MSedgeMC 3ba815a17a test: an armadillo's rolled-up state through the chunk save
`mob_from_nbt` and `mob_to_nbt` are the pair `Game::load_mob` and
`Game::mob_tree_nbt` go through. An armadillo whose save names
`state: rolling` loads rolling and takes 4.5 of a hit for 10; six ticks into
the roll its shell is out; a save that names no state, or a name that is no
`ArmadilloState`, loads idle and takes the whole 10. Saving a rolling armadillo
writes `rolling` even when the compound it came in with said `idle`, and the
reloaded one has its time in the state restarted, so the shell is not out until
it has been rolling for six ticks again.

Without the state in the pair the loaded armadillo is idle (left: 0, right: 1)
and takes 10 for a 10 hit where it took 4.5 rolled up.
2026-11-06 22:27:24 +00:00
MSedgeMC d2d4e99101 fix: keep an armadillo's rolled-up state in its chunk save
`Armadillo.addAdditionalSaveData` writes the state the armadillo is in beside
its scute timer, and `readAdditionalSaveData` switches to it:

    output.store("state", Armadillo.ArmadilloState.CODEC, this.getState());
    output.putInt("scute_time", this.scuteTime);
    ...
    this.switchToState(input.<Armadillo.ArmadilloState>read("state", Armadillo.ArmadilloState.CODEC)
       .orElse(Armadillo.ArmadilloState.IDLE));

`ArmadilloState.CODEC` is a `StringRepresentable.fromEnum`, so the key holds
`idle`, `rolling`, `scared` or `unrolling`; `switchToState` sets the synched
state, and `onSyncedDataUpdated` restarts `inStateTicks` when it changes.

Horizon models the same state (`extra.armadillo.state`, the value
`Armadillo.getState` is synched with) and wrote only `scute_time`, so an
armadillo that unloaded while rolled up came back idle: a hit for 10 went from
4.5 back to 10.0 (`Armadillo.hurtServer`: a ball takes half of the damage past
the first point), `hiding_in_shell` went false so the model lost its shell, and a
brush or a food click was answered by an idle armadillo instead of a ball.

`state` is now written from the live state - `idle` for one that is not rolled
up, as vanilla writes it unconditionally - and read back through `set_state`,
which is `switchToState` plus `onSyncedDataUpdated`: a save that names no state
(or names one that is not an `ArmadilloState`) loads as idle, and the time in
the state starts again at 0.
2026-11-06 22:27:24 +00:00
GabrielNichols a92071933a Bring Recipes search and its skin to Creative inventory 2026-11-06 19:27:01 -03:00
voidsip 7914cfe741 Keep saved-creeper radius test compatible with silence metadata 2026-11-06 16:23:43 -06:00
Kevin RamirezandClaude Fable 5.1 786bc6d8ca CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:23:11 -06:00
MSedgeMC c3326aed5e test: direct furnace block-entity tick parity
The command-execution furnace family could not give a checkable gap control
on this host, so this is a direct AbstractFurnaceBlockEntity.serverTick
harness instead: a real RecipeManager and a furnace, blast furnace or smoker
in a block-update level, compared with FurnaceCooking::tick. It pins the
cooking_fuel burn table for every fuel item, the 200/100/100 cook times and
speed multiplier, fuel consumption and LIT state, RecipesUsed experience, a
full output slot, timer decay, and an input changed mid-cook. --no-change
reproduces the corpus before that family.

757/757 cases match the pinned client.
2026-11-06 22:20:04 +00:00
voidsip eab98fdd88 Allow slow debug fixtures and retain wind hook evidence 2026-11-06 16:18:35 -06:00
voidsip 3cdf31772f Fix falling damage helmet durability order 2026-11-06 16:14:24 -06:00
GabrielNichols 254d2c8e10 Keep the Recipes footer through empty search transitions 2026-11-06 19:13:52 -03:00
voidsip 088b9e37e5 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11330-explosion-item-health 2026-11-06 16:13:47 -06:00
voidsip 98cfa7ef55 Merge current upstream main into stack tool mining fix 2026-11-06 16:13:44 -06:00
voidsip a79aa4f977 test: add headless dropped-item explosion regression 2026-11-06 16:13:38 -06:00
voidsip 55d7ad61f1 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11328-trade-modifiers 2026-11-06 16:13:33 -06:00
voidsip 2b839bce3d Keep smithing regression compatible with baseline APIs 2026-11-06 16:13:32 -06:00
voidsip 929ad64e2a Match trade enchantment modifiers and provider costs 2026-11-06 16:13:26 -06:00
voidsip 5345f12b9d Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/smithing-transform-11307 2026-11-06 16:12:56 -06:00
voidsip 5a6a6e4e77 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11123-native-hud-mode
# Conflicts:
#	crates/horizon-client/src/game/native_hooks.rs
2026-11-06 16:12:55 -06:00
voidsip b1e79509ac Fix smithing transform optional inputs and result templates 2026-11-06 16:12:45 -06:00
voidsip bf40027865 Fix stack tool mining rules and immersion checks 2026-11-06 16:12:44 -06:00
voidsip 07b8984a5b Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/wind-burst-block-hook 2026-11-06 16:12:38 -06:00
voidsip 476bb33916 Expose native HUD mode with independent hand visibility 2026-11-06 16:12:32 -06:00
voidsip cf69ad16ef Add a native wind burst block filter hook 2026-11-06 16:12:23 -06:00
voidsip 0289686b53 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11317-block-crack-feedback 2026-11-06 16:12:21 -06:00
voidsip eeb3c04215 Match server crack stages and observer range 2026-11-06 16:12:13 -06:00
voidsip 9af63f81d3 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/11319-gateway-item-motion 2026-11-06 16:12:06 -06:00
voidsip 4fd7c5c271 Keep dropped items live through End gateways 2026-11-06 16:11:57 -06:00
GabrielNichols c7d27a84a1 Fill Recipes grids without teaching the recipe 2026-11-06 19:10:19 -03:00
voidsip c0aef4ac41 Merge commit '9d11d0c710705ee0901c2186d78641048a6660b0' into fix/round5-5-11321 2026-11-06 16:09:25 -06:00
voidsip 9e11182d53 Respect world borders for pistons and natural spawning 2026-11-06 16:09:24 -06:00
voidsip 854e12bf62 Merge upstream main for source-silence validation 2026-11-06 16:08:31 -06:00
MrCloudy2andClaude Opus 5.5 858582b279 Droppers insert into chest boats and container minecarts
DropperBlock.dispenseFrom finds its target with HopperBlockEntity.getContainerAt:
the block's container, else a container entity (chest boat or raft, chest or
hopper minecart) whose box meets the space in front, picked at random. Horizon's
dropper only asked for a block container, so items fell out next to a chest boat
instead of going in. It now falls back to the entity lookup hoppers already use.

The dispenser harness gains 48 dropper cases with chest boats, rafts and chest
and hopper minecarts centred in front, reaching in from beyond, just past the
space, two at once, behind a chest block and non-container vehicles. The oracle
and the fixture print the container entity's items (Tinv lines).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 23:06:16 +01:00
voidsip c06d1d81c4 Respect Silent on explosions and potion impacts 2026-11-06 16:06:09 -06:00
Florian ERNSTandCodex a44cdec48e Adopt live falling-block data into the resident entity
Co-Authored-By: Codex <[email protected]>
2026-11-06 23:03:23 +01:00
Kevin RamirezandClaude Fable 5.1 03e72bc0b0 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 16:03:08 -06:00
MSedgeMC 197ad9fcc5 port_map: Phantom's anchor_pos is read and written
`tools/port_map/game2-002.tsv` recorded `gap: minor: anchor_pos is not read` and
`not written` for `Phantom.readAdditionalSaveData` and
`Phantom.addAdditionalSaveData`. Both now carry the anchor and say how: the key
only for a phantom that has an anchor, absent or unreadable as no anchor.
2026-11-06 22:00:10 +00:00
MSedgeMC 58feda2a09 test: a phantom's circling anchor through the chunk save
`mob_from_nbt` and `mob_to_nbt` are the pair `Game::load_mob` and
`Game::mob_tree_nbt` go through, so the test loads and saves a phantom by its
compound. A phantom at (0.5, 75, 0.5) whose save names `anchor_pos: [100, 64,
-40]` loads with that anchor, circles to (109.66, 62, -37.41) - the anchor,
`distance*cos(angle)` across and `-4 + height` up - and a phantom that moved its
anchor to (9, 70, -3) saves that position, not the one its compound came in
with. A phantom with no anchor loads with none and saves no `anchor_pos`.

Without reading the anchor the load gives the phantom (0, 80, 0), its own
position five blocks up, and the circle point (9.66, 78, 2.59); without writing
it the save keeps the stale (100, 64, -40).
2026-11-06 22:00:10 +00:00
MSedgeMC c02c215c62 fix: save a phantom's circling anchor with its chunk
`Phantom.addAdditionalSaveData` writes the anchor the phantom circles around
beside its size, and `readAdditionalSaveData` takes it back:

    protected void readAdditionalSaveData(final ValueInput input) {
       super.readAdditionalSaveData(input);
       this.anchorPoint = input.<BlockPos>read("anchor_pos", BlockPos.CODEC).orElse(null);
       this.setPhantomSize(input.getIntOr("size", 0));
    }

    protected void addAdditionalSaveData(final ValueOutput output) {
       super.addAdditionalSaveData(output);
       output.storeNullable("anchor_pos", BlockPos.CODEC, this.anchorPoint);
       output.putInt("size", this.getPhantomSize());
    }

Horizon models that anchor in `extra.home`, which
`PhantomCircleAroundAnchorGoal.selectNext` centres the circle on
(`anchor + (distance*cos(angle), -4 + height, distance*sin(angle))`), but
`mob_to_nbt` wrote only `size` and `mob_from_nbt` read only `size`. A phantom
saved and reloaded therefore kept the anchor `register` gives every new phantom
- its own position five blocks up, vanilla's `finalizeSpawn` value - instead of
the one in its save: a phantom at (0.5, 75, 0.5) loaded with
`anchor_pos: [100, 64, -40]` came back anchored at (0, 80, 0), and its first
circle point was (9.66, 78, 2.59) rather than (109.66, 62, -37.41).

`TagValueOutput.storeNullable` writes the key only for a non-null value, and
`read(...).orElse(null)` gives null for a save without one, so a phantom with no
anchor now writes no `anchor_pos` and loads back with none - the circle then
fills it with its own `blockPosition()`, as vanilla's does.
2026-11-06 22:00:10 +00:00
MSedgeMC 73737f9e6c test: brewing stand tick, brew time and fuel uses
The brewing suite stopped at recipe matching, assemble and the slot filters;
nothing advanced the block entity. The tick family now runs the real
BrewingStandBlockEntity.serverTick and BrewingStand::tick for 0..8001 ticks
and compares slots, brew time, total brew time, fuel, total fuel, speed,
bottle bits, ingredient and the brew-event count. It covers the 400-tick brew
divided by speed_multiplier and its ceiling (2, 3, 0.3 and 0), blaze powder's
twenty uses and speed 1, fuel loaded with nothing to brew, splash and
lingering outputs, ingredient shrink and mismatch, finishing without fuel and
twenty brews from one blaze powder. --no-tick leaves the tick corpus out.

2860/2860 cases match the pinned client.
2026-11-06 21:54:20 +00:00
GabrielNichols 7351b11d1a Sync Recipes settings with main 2026-11-06 18:52:17 -03:00
Delete228andCodex 8c13c07e40 Merge the fluid renderer fixture update
Co-authored-by: Codex <[email protected]>
2026-11-07 00:49:10 +03:00
Delete228andCodex a661a2262d Pass empty motion data in the fluid renderer fixture
Co-authored-by: Codex <[email protected]>
2026-11-07 00:48:50 +03:00
Delete228andCodex 00a77c1e1f Merge the updated fluid renderer into the model renderer
Co-authored-by: Codex <[email protected]>
2026-11-07 00:46:25 +03:00
martipops 37b174ada3 Merge remote-tracking branch 'upstream/main' into fix/pad-read
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-06 16:46:14 -05:00
TheMrZZandCodex gpt-6.1-sol 008aba983e Spill explicit vehicle container contents on kill removal
Write back live menu slots before spilling chest boats, chest rafts and
container carts. Reuse the container split/motion algorithm through its
actual double-origin path, preserving integer callers and empty-slot draws.
Keep ordinary hurt, hull, lead and pending-loot policies separate.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 22:45:56 +01:00
Delete228andCodex 0c66644118 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 129e00b6b1 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 39f7f1bf42 Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Delete228andCodex 63ac8a4adf Merge Horizon main into the PR branch
Co-authored-by: Codex <[email protected]>
2026-11-07 00:45:54 +03:00
Kevin RamirezandClaude Fable 5.1 75a078b0ce CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:43:56 -06:00
Kevin RamirezandClaude Fable 5.1 fdc26c707e CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:43:30 -06:00
Riccardo Guerriero 01b3be1502 Merge remote-tracking branch 'origin/main' into perf/11126-structure-starts-jigsaw 2026-11-06 22:42:17 +01:00
voidsip 9c292718aa Persist comparator output and hopper transfer cooldown 2026-11-06 15:40:57 -06:00
Nikita Maximilian Grimm 3b64b39c5b Merge branch 'shader-api-02-serve' into hdr-step3-float-frame
# Conflicts:
#	crates/horizon-client/src/game/shader_packs.rs
2026-11-06 22:38:21 +01:00
Kevin RamirezandClaude Fable 5.1 58859564a8 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:36:34 -06:00
Florian ERNSTandCodex 7b53924423 Use modern falling-block state decoding and sand fallback
Co-Authored-By: Codex <[email protected]>
2026-11-06 22:36:12 +01:00
Kevin RamirezandClaude Fable 5.1 d4984f2ca0 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:36:08 -06:00
voidsip 113cd40efb Respect per-stack brewing fuel components 2026-11-06 15:33:46 -06:00
DUzzL dd2d21f0f9 Fix natural spawn height bound off by one
NaturalSpawner.getRandomPosWithin samples up to chunk.getHeight(WORLD_SURFACE)+1,
and ChunkAccess.getHeight is firstAvailable-1, so the bound is firstAvailable.
Horizon already stored firstAvailable and added one more. Also correct the
oracle's fake LevelChunk.getHeight.
2026-11-06 22:30:06 +01:00
umcekoandClaude Sonnet 5.5 5fa72fe13a Gameplay reads the dimension type for lava, water, beds and anchors (#11282, 3/9)
Fast lava, water evaporation, bed and anchor rules, the raid-bright check, the
day timeline, ambient light and the default clock of /time read the level's
dimension type facts instead of matching on the level, as vanilla reads
DimensionType and its attributes. No behaviour change for the three built-in
levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 0b23212f49 The dragon fight follows has_ender_dragon_fight (#11282, 5/9)
The fight tick, end crystal fire and respawn, the boss bar, the fight dragon
check, the exit portal and the DRAGON chunk ticket read the dimension type's
has_ender_dragon_fight, and the End flashes read its End skybox. No behaviour
change for the three built-in levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 5db025666c Mobs and spawn rules read the dimension type (#11282, 4/9)
MobWorld carries the level key and its dimension type facts instead of nether
and end flags, and the spawn light test samples the type's
monster_spawn_light_level as vanilla's DimensionType.MonsterSettings does.
Fast lava, snow golem melting, piglin zombification, fixed-time and day
timeline checks read the type's facts. No behaviour change for the three
built-in levels; random draws are unchanged.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 b37e409c9c Block behaviour reads the dimension type, not the dimension (#11282, 2/9)
Vanilla reads each level's DimensionType and its attributes (fast lava, water
evaporates, the day timeline, infiniburn, sky light) rather than naming the
Overworld, Nether and End, so a datapack dimension using the Nether's type
behaves as the Nether. horizon-world block behaviour now does the same through
LevelAccess::dimension_type(); sites vanilla keys on the level compare LevelKey.
No behaviour change for the three built-in levels.

Part of #11282

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:27:09 +03:00
umcekoandClaude Sonnet 5.5 3874099e52 Dimension type facts read instead of matching the level (#11282, 1b/9)
Code that matches on the Overworld, the Nether or the End should read the
level's dimension type, as vanilla does. Add DimensionFacts: the
DimensionType fields and gameplay attributes Horizon reads, the four
vanilla types built in code, and from_json for packs. A test checks the
built-in table against the prepared 26.3 dimension_type and timeline tag
files.

No callers yet; nothing changes for players, saves or packets.

Part of #11282.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:26:56 +03:00
umcekoandClaude Sonnet 5.5 dfbfc81aa9 LevelKey: a level's key for any dimension, not three variants (#11282, 1a/9)
Vanilla makes one level per entry of the dimension registry and keys each
by its ResourceKey<Level>. Add LevelKey: the three built-in levels as
constants, any other id validated as Identifier.parse does and interned,
plus builtin_or_overworld for today's lossy lookups from saves, guests
and packets.

No callers yet; nothing changes for players, saves or packets.

Part of #11282.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 00:26:56 +03:00
Kevin RamirezandClaude Fable 5.1 df010b6204 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:24:27 -06:00
AkirabaneandClaude Opus 5.5 8e033a0480 dedicated: no move onto a server that is full or stopping
player-expect is refused as "the server is full" once the places the
player limit counts and those of the other expected players reach
max-players: each expectation holds a place, so two proxies sending
players at once cannot fill a server past its limit. As at login, an op
who bypasses the limit is never refused, and a renewal holds no second
place.

Once a stop has begun (/stop from the console, a player or the control
endpoint, or a stop signal), player-expect, player-import and
player-confirm are refused as "the server is stopping": a player must not
arrive on a server that is about to close. A retry after the commit is
still answered, and export, release and drop stay open, since they are how
players leave. transfer-status says whether the server is stopping.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 22:23:35 +01:00
Nikita Maximilian Grimm 89bfc9ab79 Merge remote-tracking branch 'fork/shader-api-02-serve' into hdr-step3-float-frame 2026-11-06 22:21:44 +01:00
voidsip c558b29549 Honor block drops and RNG order for mining XP 2026-11-06 15:19:27 -06:00
DUzzL 37f8ebb618 Read scaffolding as its stable shape in fluid wall tests 2026-11-06 22:15:14 +01:00
Kevin RamirezandClaude Fable 5.1 83d3c8afa5 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:14:48 -06:00
Kevin RamirezandClaude Fable 5.1 ab98869192 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 15:14:22 -06:00
DUzzL bedf076c56 Read stack components in anvil and grindstone results 2026-11-06 22:10:39 +01:00
TheMrZZandCodex gpt-6.1-sol a3d9d8f2c3 Keep client shrieker playback outside shared capture
Use the existing unshared audio path for level event 3007 and cover actual event capture, local playback, and random continuation.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 22:08:41 +01:00
rison1337andCodex 332f312b38 perf(mobs): cache exact bone rotations when TAA is off
Reuse bounded exact local geometry and normal rotations for GPU mob posing.
Keep the original walk and transform helpers when TAA is enabled, with the
policy refreshed every frame from the existing anti-aliasing setting.

Add byte-exact churn and bypass-switching coverage and measured crowd results.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-07 02:07:56 +05:00
Kevin RamirezandClaude Fable 5.1 4ee0e8dd33 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:57:41 -06:00
Kevin RamirezandClaude Fable 5.1 7416b4a5f6 CI: run the checks again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:57:15 -06:00
Florian ERNSTandCodex 9634732468 Omit prototype max-stack patches in saved ItemEntity stacks
Co-Authored-By: Codex <[email protected]>
2026-11-06 21:54:00 +01:00
MSedgeMC bb24c87d5d test: every brewing recipe's mix and the three slot filters
No parity suite reached the brewing recipes: brewing.rs's Brewing::find,
PotionIngredient and the BrewingStandMenu slot filters had no differential
corpus. A new suite loads the vanilla data pack's real RecipeManager and
compares every bundled BrewingRecipe's assemble with Horizon's find output,
the brewing_input and brewing_reagent property sets against is_bottle and
is_reagent, and the fuel slot's BrewingFuel against brewing_fuel. Cases cover
every recipe, every potion item and potion id against every reagent, the whole
item registry as a slot candidate, and non-matching pairs. --baseline keeps
only the exact recipe pairs and reproduces the corpus before the cross-product.

2811/2811 cases match the pinned client. The brewing-stand tick, brew time,
fuel use and remainders remain pending.
2026-11-06 20:51:50 +00:00
26210e2da4 Read saved jigsaw structure starts before regenerating them
Part of #11126.

Load complete jigsaw starts from the owning chunk's structures.starts into
its shared generator cache, retaining regeneration for unsupported or
undecodable starts. Read pending saves in every dimension and preserve
reference counts alongside the original piece layout.

Cover resource-free round trips, generated layouts across two seeds and
pending-record loading from a different referencing chunk.

Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 21:50:30 +01:00
martipopsandClaude Opus 5.5 197eb01adf Controllers: the guide says Open only for what opens, and nothing for a click a block keeps
Portals and an empty lectern showed Open, and a block that keeps the click and does nothing
(an empty flower pot or bookshelf slot, a pressed button) showed Use, or the held item's own
row. An ender chest said Use and now says Open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:40:39 -05:00
rdbtCVSandClaude Opus 5.5 f17d86aa79 Keep #[cfg(test)] on the test module the merge of main took it from
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:31:36 +00:00
rdbtCVSandClaude Opus 5.5 59ef682c9c Keep #[cfg(test)] on the test module the merge of main took it from
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:31:10 +00:00
MSedgeMC 176caeb5fd test: every firework flight duration and long explosion runs
The firework tooltip corpus sampled flight durations 0-3, 7 and 255 and runs
of at most six equal explosions, so Fireworks.addToTooltip's run count above
six and the rest of the unsigned-byte flight range were never compared. A
deterministic edge corpus now adds all 256 flight durations, runs of 0..256
equal explosions in every shape, colour and fade lists up to 32 entries
(custom, negative and duplicate colours), equality-sensitive runs (same shape
with different colours), both components on one stack, firework components on
non-rocket/non-star items, and every combination of hidden components.
--no-edges writes the random corpus exactly as before.

3331/3331 cases match the pinned client.
2026-11-06 20:30:47 +00:00
rdbtCVSandClaude Opus 5.5 1e7af3a2c5 Restore the #[test] the merge of main took from this test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:29:27 +00:00
Kevin RamirezandClaude Fable 5.1 b97af5d3da CI: run verify again after the runner ran out of disk
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:28:11 -06:00
Kevin RamirezandClaude Fable 5.1 9dac406ddc A spectator in loaded chunks keeps its food and regeneration ticks (#11312)
ServerPlayer.doTick runs super.tick() (Player.aiStep's tickRegeneration)
and foodData.tick for every player but a spectator touching an unloaded
chunk. Horizon's guest tick returned for a spectator before the damage
clock, peaceful_regeneration and food_tick, so health, saturation and
exhaustion froze while watching and were still there on the way back to
survival.

A spectator whose box, inflated by one block, lies in loaded chunks
(Entity.touchingUnloadedChunk) now runs those three; hazards, travel
and movement statistics stay skipped. food_tick already counts a
spectator as invulnerable, so starvation hurts none.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:23:40 -06:00
rdbtCVSandClaude Opus 5.5 40ffd36b11 TNT fuse test: skip without prepared resources, as its neighbours do
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:17:33 +00:00
rdbtCVSandClaude Opus 5.5 5c05eacc6d Let the remote motion handler reset a trident's stuck timer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:17:07 +00:00
rdbtCVSandClaude Opus 5.5 0fe1d0e15d Port map: keep PackSelectionScreen.Watcher on folder_stamp, which exists
Review feedback on #10288.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:42 +00:00
rdbtCVSandClaude Opus 5.5 3f38b2d0bd Merge origin/main into PR #10288
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:42 +00:00
rdbtCVSandClaude Opus 5.5 1088c588e4 Merge origin/main into PR #11121
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:11 +00:00
rdbtCVSandClaude Opus 5.5 ed8eaed845 Merge origin/main into PR #10201
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:11 +00:00
rdbtCVSandClaude Opus 5.5 fe0a3ac905 Merge origin/main into PR #10538
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:11:09 +00:00
rdbtCVSandClaude Opus 5.5 439c074655 rustfmt the test modules after merging main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:10:38 +00:00
rdbtCVSandClaude Opus 5.5 0a6d5eb0a7 Merge origin/main into PR #10511
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:09:33 +00:00
rdbtCVSandClaude Opus 5.5 03340f679c Merge origin/main into PR #10130
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:08:28 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 005530f4de style: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:08:18 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 295d815ec2 fix(render): give unmarked light no HDR-lighting knee in the HDR pass
The knee eases HDR lighting's gain into the peak; light written as light (alpha 1) has no
gain, and on a float frame the knee would compress it before the clamp to the peak. On a
unorm frame such pixels never reached the knee, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:04:18 +01:00
rdbtCVS 1b1f240ab1 Merge origin/main into PR #9967
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:54 +00:00
rdbtCVS c6735e2d8e Merge origin/main into PR #10290
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/game/command_harness.rs
#	mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCommandExecOracle.java
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:43 +00:00
rdbtCVS 98afd549d4 Merge origin/main into PR #10344
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 20:00:39 +00:00
Nikita Maximilian GrimmandClaude Opus 5.5 44753cefe0 fix(render): keep today's HDR look on the float frame, and draw world text in the frame's format
Marked pixels (light sources, the sun, HDR lighting) clip at white before their lift, as on
a unorm frame; only unmarked ones show light over 1, which is how shaders will write light.
Without that, the float frame tinted the sun and its halo instead of lifting them white.

The native world-text pipeline targeted the swapchain's 8-bit format, which a 16-bit or
float frame's pass refuses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 21:00:16 +01:00
Kevin RamirezandClaude Fable 5.1 ef6f7a2170 Sky darkening truncates the light level, as updateSkyBrightness does (#11323)
Level.updateSkyBrightness is `(int)(15.0F - skyLightLevel)`: the float
truncated. Horizon's environment::sky_darken (and the End copy in
Game::sky_darken) rounded, stepping the darkening up half a level early
at dusk and late at dawn, which moves bed eligibility and the light-
dependent spawns with it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 14:00:13 -06:00
rdbtCVS b9526c300c Merge origin/main into PR #10291
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 19:59:42 +00:00
6e09e5035b mesher: bake face data and reuse padded cell indices
Cache each baked face's shape for both collision-fullness states and pack
its own UVs once, preserving the original floating-point operation order.
Address neighboring blocks, AO samples and fluid samples from padded cell
indices instead of recomputing coordinate indices for each lookup.

Add asset-independent checks against coordinate lookups for padded steps
and all AO sample tables, plus literal shape bounds and packed UV values.

Co-Authored-By: pi coding agent (DeepSeek-V4.1-Flash) <[email protected]>
Co-Authored-By: Codex (gpt-6.1-sol) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8KX3vDMZy53y1vXqeT
2026-11-06 20:49:08 +01:00
Riccardo Guerriero 425b65a99f Merge remote-tracking branch 'origin/main' into fix/w149 2026-11-06 20:45:56 +01:00
a222a7e47a End a server player's glide on a climbable like vanilla
LivingEntity.travelFallFlying stops the glide on a climbable, and a
server simulates its players' travel (AUTHORITATIVE_SIDE_AND_SERVER).
Horizon's server travel cleared the flag only on the guest's temporary
body; as_guest now copies it back, so the glide ends on the server and
the client, which takes the flag from the server, stops gliding too.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:45:15 +01:00
c4dcf2d2c8 Wait for the client's load after the End credits respawn
handleClientCommand(PERFORM_RESPAWN) for a player who won the game
calls restartClientLoadTimerAfterRespawn, as the death respawn does: the
player's movement and actions wait, and isInvulnerableTo spares it, until
its client reports the level loaded or 60 ticks pass. Horizon restarted
the timer only after a death.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:41:08 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 b376a835f6 feat(render): draw the frame in float with HDR on, under HORIZON_HDR_FRAME=float
HDR design step 3, the frame and the display pass (docs/design/hdr-linear-light.md):
with the switch, HDR on and no shader pack, the renderer draws into Rgba16Float where
the surface offers HDR output and the device draws it, whatever Smooth Gradients is.
0.1 and shaders/ packs keep today's frame; HDR off is untouched.

The HDR pass keeps light over 1 where the world shows, up to the peak, and clips it at
white under the HUD and menus and with HDR Lighting off, as a unorm frame does. On
today's frames, which hold nothing over 1, the pass computes the same values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:40:17 +01:00
effe6ea423 Forget a mob's unresolved saved lead holder when a player leads it
Leashable.setLeashedTo goes through LeashData.setLeashHolder, which
clears the delayed holder a save named. Horizon kept it, so 100 ticks
after loading the forgotten holder still dropped a lead item next to a
mob the player was leading, or a holder that loaded late took the mob
from the player. Mobs and boats both.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:33:37 +01:00
TheMrZZandCodex gpt-6.1-sol fec7a5f9e6 Respect class-owned container fields in vehicle saves
Keep plain vehicles free of container fields and save pending boat tables
instead of Items. Normalize recognized pending keys while preserving the
existing cart loot restoration path.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 20:28:32 +01:00
Nikita Maximilian Grimm 1f31d3d23a Merge remote-tracking branch 'origin/main' into hdr-step3-float-frame 2026-11-06 20:18:33 +01:00
be7591c70b Drop a converting piglin's inventory like vanilla
Piglin.finishConversion cancels the admire, then drops every non-empty
stack of its inventory with spawnAtLocation (whole stacks, components
kept), then converts; the converted sound comes before all of it. Horizon
dropped only the admired off-hand item, before the sound, and the
inventory was lost with the piglin.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 20:16:57 +01:00
Florian ERNSTandCodex 399171f0e0 Preserve standard mob equipment stacks in item commands
Co-Authored-By: Codex <[email protected]>
2026-11-06 20:16:54 +01:00
m_uvexandPluto 41d699a458 packaging: include native mod build dependencies in launcher FHS
Adds gcc, glibc.dev, pkg-config, alsa-lib.dev, and udev.dev to the FHS
environment so native mod compilation works on NixOS without external shell.

Co-Authored-By: Pluto <[email protected]>
2026-11-06 22:10:15 +03:00
1a43ca3439 Reset the impulse context after an ender pearl teleport
ThrownEnderpearl.onHit calls resetCurrentImpulseContext after the fall
distance reset, so a wind charge's or mace smash's impact height no
longer limits the next fall.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:26:03 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 1e95d86f56 HORIZON_STILL: a frozen world's rain fog sits at its level too
Without a shader pack the rain also pulls the fog in
(AtmosphericFogEnvironment's rain fog), which eases toward the rain only in
tick_particles, and a frozen client skips that. With the rain at its level,
a frozen scene showed full rain but no rain fog. Under HORIZON_STILL a frozen
client now puts the rain fog at its target, from the same inputs the eased
path reads; the easing itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:39:22 +01:00
Florian ERNSTandCodex 070a8019e0 Preserve empty loot result positions for command replacement
Co-Authored-By: Codex <[email protected]>
2026-11-06 18:12:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 c50b3ef699 HORIZON_STILL: a frozen world's rain and thunder sit at their levels
The rain and thunder levels fade 0.01 a tick and stood still in a frozen
world, so a scene that wanted rain had to step the world about a hundred
ticks, and its mobs walked differently in each launch. Under HORIZON_STILL a
frozen world now holds both levels at their targets, as STILL already does
for a shader pack's smoothed values: /weather rain shows full rain with no
step. A running or stepping world, and normal play, fade them as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:05:51 +01:00
TheMrZZandCodex gpt-6.1-sol daefbd84e7 Preserve modern SNBT literals during data execution
Reuse the admitted modern SNBT decode contract for merge and literal modify.
Add actual Game storage and command-oracle regressions for typed values and
no-change copy callbacks.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 17:51:01 +01:00
MSedgeMC 33ec06ab12 fix: roots on the clicked face, and the chest beside a copper one
tools/test_use_item_on_parity.py never held an axe, hoe or shovel against a
block the minecraft:block_transformer registry names, so game.rs's
use_block_transformer was never exercised. It now compares log stripping,
copper scraping/waxing, copper doors and trapdoors, double copper chests and
soil tilling, which found two differences from vanilla:

- rooted dirt's hanging roots must use Block.popResourceFromFace, on the
  clicked face with the face's own motion, and drop before the block changes;
- scraping or waxing a double copper chest must send a second block_change
  game event and the same particle at the chest's other half.

The VanillaDispenserOracle stand-in player also swallowed the item use start
vibration LivingEntity.startUsingItem posts; it now posts it too.

5200/5200 cases match on the new corpus.
2026-11-06 16:49:46 +00:00
oxi-does-dev 147c6bceef Launcher: keep the partner card's corners in the glass look 2026-11-06 18:39:41 +03:00
Nikita Maximilian GrimmandClaude Opus 5.5 94a66c81d4 screenshot-sequence: STEP_MS in decimal milliseconds
16.666667 steps a 60 fps clock. STEP_MS is a finite number more than 0,
and the reply gives it back as given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8650e51ddb screenshot-sequence: no pack reload or loading screen during a sequence
A Horizon pack's hot reload could rebuild the pack under the same name
mid-sequence, unseen by the scene check: it now waits for the reply. A
sequence doesn't start, or ends, while the level-loading screen shows,
which a new world keeps a moment after it is ready. The 30 s limit is on
the frames before the last: the last frame's PNG waits as screenshot does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 0be2751890 horizon-ctl screenshot-sequence: a shader pack's first frames on a set clock
screenshot-sequence FRAMES STEP_MS PATH, under HORIZON_STILL with the world
frozen, makes the running pack start over and draws FRAMES frames on a
scripted clock: frame n is n frames and (n - 1) x STEP_MS milliseconds in,
STEP_MS after the frame before (0 on frame 1). The last is written as
screenshot PATH writes it, its -hdr.png too under HDR output, and the reply
names the pack and the last frame's clock. The same build, GPU and driver
write the same bytes every run, for shaders/ packs, Horizon's own and none.

Sequence frames are submitted inline and waited for on the GPU, so each
reads the centre depth the one before left, or the sequence fails; a frame
the surface refused is drawn again. It refuses to start with far terrain,
TAA, frame generation, chunk fade or the shadow cache on, refuses commands
that would change the scene while it runs, and ends with an error when the
camera, world, settings, pack or window size change. cancel stops it, quit
still quits. Documented in agent-control.md's STILL section, shaders.md and
api/shaders.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:15 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 bb8be7d516 Shader packs: the reset forgets the world's frames for smooth() too
Since smooth() keeps going through teleports, a pack remembers whether it
has drawn a frame of the world. A reset pack now forgets that too, as a
pack just loaded has seen none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:33:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 4d025d5e93 Shader packs: the reset also blanks a Horizon pack's own post targets
A pass that reads one of the pack's targets ([[post]] output) before the
pass that writes it got last frame's picture after a reset; a pack just
made reads it blank. The reset now drops the targets and their bind
groups, made again blank on the next frame; the pipelines stay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:14 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 8744511d74 Shader packs: a reset that makes the running pack start over
Renderer::reset_shader_pack_history makes the next frame draw as a freshly
loaded pack's first, for either kind. A shaders/ pack's buffers, images,
storage buffers and shadow maps are made again when next drawn, its passes
get their declared uniform values back, and the frame uniforms, custom
uniforms and centre depth forget earlier frames; its programs stay. A
Horizon pack's post history goes black and its eased values, wetness and
frame count restart. Without a pack it does nothing. Nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:28:14 +01:00
m_uvexandPluto 160e997e4a packaging: add flake.nix for Horizon launcher and devShell
Provides a Nix flake packaging the Horizon launcher in an FHS environment
along with a development shell containing dependencies for building the project.

Co-Authored-By: Pluto <[email protected]>
2026-11-06 18:21:05 +03:00
oxi-does-dev e35010eb7d Merge remote-tracking branch 'origin/main' into feat/launcher-nord 2026-11-06 18:16:00 +03:00
oxi-does-dev 014f25363a Launcher: Nord theme, square solid look, sky without resize bakes
Adds the Nord palette (polar night, snow storm, frost) as a launcher theme with its own dark and light tokens and sky art. Palette themes draw the square Blocks look: no glass, no rounded corners. The night tokens are square everywhere, and the Blocks status dots and controls follow. Blocks and the light glass look draw the baked sky, so resizing no longer rebakes the scene per frame and stays responsive. Settings gains the launcher-theme picker in the Blocks look. Nord now shows the sky instead of the scene's pale halo.
2026-11-06 18:11:28 +03:00
martipopsandClaude Opus 5.5 b0286fb598 Controllers: the bumpers move the selected server
Its arrows are hit-tested inside the icon, where a pad's click on the row never
lands.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:10:52 -05:00
martipopsandClaude Opus 5.5 cf73e1d25f Merge main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:05:53 -05:00
flexeykinDevandClaude Opus 5.5 4ac75c281a install.ps1: verify the release signature when OpenSSL 3 is found
The first install on Windows now checks SHA256SUMS.sig (a Nightly's
SHA256SUMS.v2.sig, for the version its title names) against the release
key, then the archive against SHA256SUMS, as install.sh does. OpenSSL is
looked for on the PATH and in Git for Windows. Without OpenSSL 3 it says
so and keeps the GitHub SHA-256 check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:53:25 +01:00
Arslan ArmanulyandClaude Opus 5.5 401693ba65 Shader packs: shadow settings come from the fragment shaders, as Iris reads them
Horizon took shadowDistance, sunPathRotation, shadowIntervalSize,
shadowNearPlane and shadowFarPlane from a program's vertex shader too, and
read nothing of shadow.fsh but its formats when the pack has no shadow
pass. Iris 1.11.7, with probe packs of ours, ignored those five declared
only in final.vsh, gbuffers_textured.vsh or shadow.vsh (its shadow
projection, shadow camera and sun stayed at the defaults), and applied
shadowDistance and sunPathRotation declared only in shadow.fsh of a pack
whose shadow buffers nothing read.

Part of #10490

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 19:20:46 +05:00
Delete228andCodex be3fbc2d2b Supply entity kinds in collision probe fixtures
Co-authored-by: Codex <[email protected]>
2026-11-06 16:58:15 +03:00
Delete228andCodex 7af3e1ec44 Test stateful mod block entity lookup in the tooling fixture
Co-authored-by: Codex <[email protected]>
2026-11-06 16:58:15 +03:00
Delete228andCodex 79fe067a7e Declare the state factory with the GameLevel hook points
Co-authored-by: Codex <[email protected]>
2026-11-06 16:41:39 +03:00
Evrim CicekandClaude Opus 5.5 c40c92e0bc Minecarts read their numeric NBT from any numeric tag, as vanilla's ValueInput does
AbstractMinecart, MinecartTNT, MinecartFurnace and MinecartHopper read
their fields through ValueInput's numeric getters, which take any
NumericTag: getIntOr (fuse, DisplayOffset), getShortOr (Fuel, the low 16
bits), getDoubleOr (PushX, PushZ) and getBooleanOr (FlippedRotation,
HasTicked, Enabled: byteValue() != 0). Cart::load took only byte, short
and int, and only double for the pushes, so {fuse:20L} left a TNT
minecart unprimed and {PushX:1} gave a furnace minecart no push.

Command harness group "minecart numeric fields": 3/18 -> 18/18.

Fixes #11146

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 16:40:54 +03:00
MSedgeMC 4304cc44b5 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-06 13:39:30 +00:00
cb62d074db Search neighboring columns for dolphin breathing like vanilla
Match vanilla's column order and fallback when choosing a breathing destination.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 14:35:37 +01:00
martipopsandClaude Opus 5.5 62298d7c36 Controllers: the guide's rows promise what the button does, from reviewing it
- The pad's own click hid the guide, and so did Alt or Super on their own. The
  inventory rows now hide for the keyboard and mouse too.
- Rows that promised something the game refuses: Equip on what can't be swapped,
  Throw on a Riptide trident out of water or about to break, Use on an empty or
  fixed item frame, Trade on a baby villager.
- Slots: Put rows on a slot that refuses the carried stack or has no room, Put Down
  for stacks with different components, Take over a result slot with another stack
  in hand, and any rows on a crafter's output or Put rows on a furnace's.
- Rows drew over a panel that fills a small view.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:28:21 -05:00
Delete228andCodex a3b1e9dd08 Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
Delete228andCodex d705f96e06 Keep native compatibility checks runnable in the upstream test harness
Co-authored-by: Codex <[email protected]>
2026-11-06 16:11:31 +03:00
Delete228andCodex ea750ddf7b Format the native compatibility changes
Co-authored-by: Codex <[email protected]>
2026-11-06 16:05:49 +03:00
Delete228andCodex 5bea1f4226 Format the native compatibility changes
Co-authored-by: Codex <[email protected]>
2026-11-06 16:05:48 +03:00
homayk228 266040ff8e Merge branch 'main' of https://github.com/SouceCalve/horizon 2026-11-06 19:52:48 +07:00
homayk228 a123c03b16 Added support for installing from PrismLauncher-portable
Now installer supports native to PrismLauncher json and correctly finds json, client.jar and other sources from minecraft, allowing it to install from portable installations without additional downloading from internet
2026-11-06 19:52:43 +07:00
Delete228andCodex 90b9df2899 Load same-type mod block entity command data in place
Replace saved NBT and item slots through the existing entity while preserving its live identity and runtime menu data. Keep normal decoding, dirty marking and update logging; verify real data commands, typed opaque fields, inventory removals and fresh different-type/chunk-loaded identities.

Co-Authored-By: Codex <[email protected]>
2026-11-06 15:47:54 +03:00
Delete228 3ba55cc9fa Supply the standard LAN join expectation in the fanout test 2026-11-06 15:46:01 +03:00
Evrim CicekandClaude Opus 5.5 0b0a58af3e Settle an enderman's teleport inside explosions, falling blocks and conduits; its hurt first
Review: hurts dealt after the creature tick (ordered TNT, beds and anchors,
falling blocks, conduits) still waited for the next tick. Explosions and
falling blocks now settle right after each hurt, before the push, and the
conduit pass after it runs. And the hurt's own events (its sound,
InfestedMobEffect.onMobHurt) are handled before the teleport, where the
enderman was hurt and from its random's next draws, as
LivingEntity.hurtServer runs them before Enderman.hurtServer teleports.

Tests: an_explosion_teleports_endermen_before_it_returns (11/12 left
pending without the explosion settle), an_infested_endermans_silverfish_
come_out_where_it_was_hurt (no silverfish when the outbox only joined the
teleport's events).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:42:29 +03:00
TheMrZZandCodex gpt-6.1-sol afd49ac9c2 Fix primed TNT numeric fuse loading
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 13:40:09 +01:00
Delete228 c8cdfd117a Draw native terrain models with explicit materials and lighting 2026-11-06 15:39:28 +03:00
Delete228 218449af0c Render native still fluid volumes with an explicit vanilla material 2026-11-06 15:39:28 +03:00
voidsip 87386f72d9 Expose focused text fields to native character hooks 2026-11-06 06:35:13 -06:00
Delete228andCodex 796db06bf8 Resolve mod block shapes from entity context
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:32:57 +03:00
Delete228andCodex e1604cc9c0 Support transient fields consumed at mod client update construction
Mod block entities can replace, clear and atomically take an owner-controlled batch of client update fields without saving it or changing retained contents and revisions. The lazy queue is empty for new, decoded and cloned entities; normal loaded-chunk and block broadcast paths remain unchanged.

Validated with 3 real entity lifecycle/concurrency/poison tests and 3 reusable save/chunk/two-viewer Hub tests. Strict all-target Clippy passes for horizon-client-render and horizon-client-lib. No new dependency, third-party implementation or resource files.

Co-authored-by: Codex <[email protected]>
2026-11-06 15:32:56 +03:00
CocofireHDandOpenCode Agent c81dc55a1c README: a Performance section from the 2026-10-29 Horizon vs vanilla runs
The comparison #1073 asks for, in the repository: FPS and 1% lows at render
distances 8, 16 and 32 for Horizon, vanilla 26.3 and vanilla with the common
performance mods, from the clean, all-windowed, idle-machine run already saved
in benchmarks/perf/, with the conditions, the startup/load/memory figures and
links to the full reports, the method and playhorizon.dev/benchmarks.

Co-Authored-By: OpenCode Agent <[email protected]>

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 13:25:46 +01:00
Evrim CicekandClaude Opus 5.5 335326296c An enderman hurt by the game layer teleports at the hurt, not on its next AI step
Enderman.hurtServer teleports inside the hurt (teleport(), or
repeatedlyTryToTeleport() for a projectile or a thrown potion). Mob::damage
has no world, so it only leaves the tries in counter2 and the enderman's
next AI step ran them. Game::settle_hurt_teleports runs them right after
/damage, and in the creature tick before the mobs tick and after the
systems' outbox: a command after /damage no longer finds it in place, and
under /tick freeze it teleports at all.

Command harness group "enderman command damage": 3/5 -> 5/5.

Fixes #11069

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 15:24:29 +03:00
martipopsandClaude Opus 5.5 6e0b26dbc2 Controllers: fixes from reviewing this branch
- Take back the server list's move arrows as targets: they split Tab's stop on
  the list, and the D-pad still couldn't land on them.
- A right stick on Z takes over in every direction, not only its pulled end.
- A pad's toggle-sneak doesn't count as holding, or a rebound key's release could
  leave the player sneaking.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:22:43 -05:00
Delete228andCodex 177548d416 Create native block entities from their actual block state
Co-Authored-By: Codex <[email protected]>
2026-11-06 15:21:47 +03:00
Horizon agentandClaude Opus 5.5 7ba6892f93 wet_fire harness: default to the prepared resources when HORIZON_TEST_RESOURCES is unset
parity.py does not set it, so the harness failed at once under parity.py and CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:18:19 +01:00
Horizon agentandClaude Opus 5.5 0443141598 Mobs stop at shulkers and at a happy ghast held still
Entity.collide adds getEntityCollisions: the boxes of the entities whose
canBeCollidedWith(mover) holds. Mob movement only had block collisions,
so a zombie or a dropped-on mob fell through a living shulker and
through a happy ghast a player holds still by standing on it (#10445).

Seen carries the happy ghast's still timeout and whether it carries
someone; the mob tick lists the slots that may block (shulkers, happy
ghasts) so other mobs pay nothing when there are none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:16:47 +01:00
Horizon agentandClaude Opus 5.5 96c7d68d0d Unloading a chunk takes its lingering clouds with it
entity_chunks(remove = true) is the unload path: every other kind it
writes is also taken out of the level, and the potion clouds were not,
so a chunk loaded again got a second cloud on the same spot. The new
test unloads and reloads a cloud and expects one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:04 +01:00
Horizon agentandClaude Opus 5.5 5988321440 Jukebox: post jukebox_stop_play only while a song plays
JukeboxSongPlayer.stop posts the event inside `if (this.song != null)`:
taking a disc out (by hand or by a hopper) after its song ended is silent.
The test takes the finished disc out and expects only the end's event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:11:02 +01:00
Horizon agentandClaude Opus 5.5 b02930a2f2 Compile every pack pipeline even after one is refused
compile() stopped handing out jobs once one pipeline was refused, so the
serial rebuild that follows compiled all the remaining programs cold.
Now every job runs and only the first refusal is recorded, so the
rebuild finds the driver cache warm. The test checks the jobs after a
refused one are still made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:06:55 +01:00
Detimes 8dad7f0ac0 Merge remote-tracking branch 'upstream/main' into via-2 2026-11-06 12:04:54 +00:00
Horizon agentandClaude Opus 5.5 5ca811b47a Merge main into #10210; drop the packet trace and receipts
The merchant fixture compares strict command and menu records, so the raw
packet trace (net/command_trace.rs in the client receive loop, the Java
trace reader, the oracle's serialized channel) and the per-operation
receipts were diagnostics only. The production client no longer writes or
joins anything for them; tools/test_merchant_gate_parity.py is a thin
wrapper like the other command_exec fixtures.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:04:15 +01:00
Horizon agent 7c32d1c4d6 Merge main into #10242
# Conflicts:
#	crates/horizon-items/src/item_data.rs
#	tools/command_exec_corpus.py
#	tools/test_command_exec_parity.py
2026-11-06 13:01:20 +01:00
Horizon agent 35e7942094 Merge main into #10208
# Conflicts:
#	tools/lan_e2e.py
2026-11-06 13:00:21 +01:00
Horizon agent e9b071e19e Merge main into #10538
# Conflicts:
#	crates/horizon-client-render/src/gfx/pack_post.rs
2026-11-06 13:00:01 +01:00
martipopsandClaude Opus 5.5 84efc873cf Input: one device letting go of sneak or sprint leaves the other's hold
Releasing Shift while R3 held sneak in hold mode dropped it for both, and so did
the other way round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:59:30 -05:00
Horizon agent 2abb7c86a1 Merge remote-tracking branch 'origin/main' into pr-10531 2026-11-06 12:55:29 +01:00
Horizon agent 47dc19fa20 Merge main into #10283 2026-11-06 12:54:39 +01:00
Horizon agent 9141f1d538 Merge main into #10280 2026-11-06 12:54:33 +01:00
Horizon agentandClaude Opus 5.5 d4a985e599 Drop the run-log note and the non-standard parity script
tools/test_non_mob_saved_flags_parity.py needs a fresh --work-dir that
parity.py never passes, so every selected run would fail; the unit test
covers the four writers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:48:38 +01:00
Horizon agentandClaude Opus 5.5 2ce4e8da11 Reset a remote trident's life on motion; cover lerpMotion in the remote motion test
ThrownTrident inherits AbstractArrow.lerpMotion, which restarts life even
when the motion is zero. The remote motion test compiles remote_motion out
of remote.rs, so its fixture needs the arrow fields; it now also checks
that zero motion keeps a stuck arrow in place and non-zero motion frees it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 12:38:30 +01:00
Horizon agent 3f1af338cb Merge main into #10201 2026-11-06 12:37:53 +01:00
martipopsandClaude Opus 5.5 00879f19d7 Move the keyboard-took-over mark out of the mouse button handler
tests/test_mouse_button_events.py compiles that handler alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:16:01 -05:00
DetimesandClaude Opus 5.5 a3abc60adf lan_bot: skip 26.2-layout packets in the 26.2 bots, walk in one move per tick (#10404)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:14:30 +00:00
martipopsandClaude Opus 5.5 a4ffae3402 Controllers: reach the recipe search, suggestions, server arrows, Realms tabs and HUD editor
Each was painted by hand or hit-tested with no target, so a pad couldn't use it:
the recipe book's search box had no keyboard, a command's suggestions couldn't be
picked, a server couldn't be moved, Realms Configure's tabs had no bumpers, and
the HUD editor's footer ended its crosshair.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:14:06 -05:00
martipopsandClaude Opus 5.5 701af0a693 Controllers: the guide's Ride is what the boat would do, and the keyboard hides it
The boat row said Ride while sneaking, in a boat or on a full one, which refuses
or opens a chest boat's chest. In play only moving the mouse took the pad out
of use, so the guide stayed up through keyboard and mouse-button play.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:07:12 -05:00
Detimes a8dc253fe9 Merge via-2c 2026-11-06 11:06:44 +00:00
Detimes ac7f3dbcf9 Merge via-2b 2026-11-06 11:06:20 +00:00
Detimes 4ae9769ab7 Merge via-2a 2026-11-06 11:06:19 +00:00
martipopsandClaude Opus 5.5 e41c28a92b Controllers: typing on signs and books, the death screen, and joining
- A book's page and signing title weren't fields, so a pad couldn't write one.
- A sign's keyboard closed on Enter after line 1, and nothing reopened it. Enter
  now keeps it open, and A opens it again.
- A keyboard open over chat stayed over the death screen and took every button.
- B answered the death confirm during the second it's meant to wait.
- A guest who joined with a pad got P1's Controllers off in a new profile, so
  that pad did nothing, on this join and every later one.
- Pad presses in another window could join a player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:37:01 -05:00
TheMrZZandCodex gpt-6.1-sol 39c12c2542 Award crafted-item statistics for merchant result takes
Track MerchantResultSlot removal and quick-move counts, reset them at the achievement callback, and award through the actual trading player's stat API without a recipe ID.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 11:34:26 +01:00
rison1337andCodex GPT-6 786bd54096 Merge main to pick up mob_to_nbt build fixes
Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 15:33:17 +05:00
DetimesandClaude Opus 5.5 bafc55bbe3 older clients: say which particle stand-ins keep their options
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:33:15 +00:00
DetimesandClaude Opus 5.5 a8c6fb9d35 lan_bot: a 26.2 client's world packets come in 26.2's layout
The join test's bot reads packets with the pinned readers; the world
packets the adapter now writes in 26.2's layout are counted (chunks) or
skipped instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:32:42 +00:00
DetimesandClaude Opus 5.5 bf60148c4f older clients: write 26.2's world packet layouts for 26.2 clients (#10404)
Chunk columns (states down with stand-ins, repacked at 26.2's 15-bit
global palette, block entity types down, light masks as longs),
light_update, level_particles (26.2 field order, one speed, Int count),
explode (no trailing sound flag, silent blasts name intentionally_empty),
move_entity_pos/_pos_rot/_rot, entity_position_sync and animate in
26.2's encodings, swing_animation as 26.2's animate.

Tests write each in 26.2's layout and read it back with translate's
26.2 readers, made pub(super) for that.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:31:25 +00:00
DetimesandClaude Opus 5.5 ec0c273cf2 item, recipes: write pot decorations, item-holding component values and tag slot displays in an older version's layout and ids
While ids are renumbered down, the values kept as pinned bytes that hold items
(pot_decorations, container, use_remainder, sulfur_cube_content) are read again
and written with that version's ids, pot sides as 26.2's four item ids, and a
tag slot display as 26.2's bare tag id (a composite of item displays for a list
of items). Quirks::{pot_decorations_are_items, tag_slot_display_is_id} written
the other way; ids::quirks_written gives the quirks of the version written for.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:30:52 +00:00
DetimesandClaude Opus 5.5 3571849861 lan_bot: a 26.2 client's walk, dig, swing and teleport confirmations reach the game as 26.3's
The test server keeps every packet the hub hands to the game; a 26.2 bot
confirms its teleports by id alone, walks, starts and stops destroying the
block below in 26.2's action numbers, and swings with each hand.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:30:18 +00:00
DetimesandClaude Opus 5.5 cf358cdd4e adapter: write login, respawn, advancements, commands, registry data, maps and stats in 26.2's layout for 26.2 clients (off by default)
layout_26_2 mirrors translate's 26.2 readers for a ClientSession: spawn info game
types back to bytes, the advancement tab position back inside its display,
argument types renumbered down with a string stand-in for 26.3-only parsers,
26.3-only synchronized registries not sent, map decorations and stats in 26.2's
numbering. Enabled with layout_26_2::set_enabled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:29:30 +00:00
DetimesandClaude Opus 5.5 4745f91af6 older-clients: read 26.2's accept_teleportation, player_action and swing as 26.3's
A 26.2 client confirms a teleport by its id alone: the session follows the
player_position packets it is sent and the moves it reports, and fills in
26.3's position and rotation from the teleport it accepts. Its digging
actions past 0 are one up (26.3 inserted CHANGE_DESTROY_DIRECTION at 1), and
its main-hand swing becomes 26.3's punch (an off-hand swing has no packet).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:29:07 +00:00
voidsip edc6751798 Keep new lava and fire ignition silent in rain 2026-11-06 04:28:43 -06:00
voidsip f214ffcc8a Merge remote-tracking branch 'origin/feedback-main-20261106' into fix/round3-1-wet-fire-order 2026-11-06 04:15:07 -06:00
voidsip a82ad5eed6 Merge remote-tracking branch 'origin/feedback-main-20261106' into fix/mob-water-breathing 2026-11-06 04:14:24 -06:00
Florian ERNSTandCodex 3122ae98c0 Omit Mob-only flags from modeled non-Mob saves
Co-Authored-By: Codex <[email protected]>
2026-11-06 11:13:36 +01:00
martipopsandClaude Opus 5.5 1a6fba23c7 Controllers: what reading a real pad got wrong
- An idle pad's trigger rests at -1, which counted as moved past 0.5, so touching
  it took over from the pad in use.
- An analog trigger jumped to full at gilrs' 0.75, so a higher threshold did
  nothing.
- gilrs drops `~` from SDL rows, so sticks on the nine Linux rows with one ran
  backwards, the Switch 2 Pro among them.
- The 8BitDo Ultimate 2 without a row looked up when pushed down, with X and Y
  crossed.
- Rumble reached the pad from an unfocused window, adaptive triggers stayed on
  over books and signs, a stale lightbar stayed after splitscreen, and in_use
  outlived an unplugged pad.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:11:50 -05:00
fac11b3d6a Cauldrons, beehives, snow golems, farming villagers and lead knots make the vibrations vanilla's do
- LayeredCauldronBlock.lowerFillLevel posts a block change.
- BeehiveBlockEntity.addOccupant posts a block change.
- SnowGolem.aiStep posts snow placement.
- HarvestFarmland.tick posts crop placement.
- LeadItem.bindPlayerMobs and LeashFenceKnotEntity.interact post attachment.

Part of #9776.

Co-Authored-By: Codex (gpt-6.1-terra) <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 11:07:40 +01:00
martipopsandClaude Opus 5.5 81bcb3950f Controllers: a slider's hold belongs to one slider on one screen
The hold shared the keyboard's slider_edit, so Start, a scroll, the mouse or a
Tab could leave some other slider held. The pad's own position also survived
Undo, so the next step jumped back near the undone value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:06:34 -05:00
Riccardo Guerriero 16cdc3afd9 Merge remote-tracking branch 'origin/main' into perf/9996-surface 2026-11-06 10:27:43 +01:00
Horizon agentandClaude Opus 5.5 58f31cdf25 Merge #10330 (main merged) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:27:33 +01:00
Horizon agentandClaude Opus 5.5 2e779eed59 Merge main into #10330 (#10304 and #10544 landed)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:27:30 +01:00
Riccardo Guerriero 82ac14c4ad Merge remote-tracking branch 'origin/main' into perf/9996-sections 2026-11-06 10:27:23 +01:00
martipopsandClaude Opus 5.5 8578927d70 Merge main into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:27:19 -05:00
Horizon agentandClaude Opus 5.5 d9b7730a6f Warden anger test: a re-angered suspect keeps its saved anger, as increaseAnger does
min(150, 35) + 20 = 55, then one anger tick: 54. The test pinned the dropped
anger the earlier port left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:05:08 +01:00
rison1337andCodex GPT-6 69d388deb0 Reuse completed armor-trim preparation between frames
Keep the completed ordered request list for the current atlas so an
unchanged overflow does not regenerate and upload every trim each frame.
Preserve final LRU residents, retry failures, and invalidate the cache
when equipment, atlas contents or the graphics device changes.

Fixes #10539

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-06 14:01:59 +05:00
Riccardo GuerrieroandClaude Opus 5.5 0855321d41 A surface run above the minimum surface level stops there
Below the level the band takes its other variant, which may give another
block.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd3xkgubE8VfXrALh2td
2026-11-06 09:50:34 +01:00
Riccardo GuerrieroandClaude Opus 5.5 db6a8b7f44 Write surface rule runs of one block at once
`build_surface_shared` applies the compiled rule at every solid block of a
column. In the Overworld most bands specialise to "the ore veins, then one
block" (deepslate), so 1.7M rule applications per 64 chunks produce ~1M block
writes, each walking the rule tree from the top and going through
`set_block`/`set_block_worldgen_column`.

A band variant that is a block rule, or ore veins followed by one, gives that
block at every position where no vein places one. The walk now writes the whole
run of solid blocks at once: the top y through the single-block path, the rest
through `TerrainChunk::fill_solid_run`, stopping at the first y where a vein
can place a block (each vein's first test, its density), at the band's bottom
and at the write bounds. Only blocks that are opaque for the stage's heightmaps
and carry no fluid take this path, so one update at the run's top leaves the
maps as a write per block does.

`CompiledRule::apply` on a sequence also tests its conditional children in the
loop instead of through a recursive call.

Instructions (callgrind, proto phase, 64 chunks, seed 12345): Overworld
6.49 G -> 6.03 G (-7.0%), surface stage 1.98 G -> 1.53 G (-23%), rule
applications 599 M -> 329 M; Nether 2.97 G -> 2.73 G (-8.1%).

A first draft by DeepSeek V4.1 Flash, via pi, reviewed and reworked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:50:34 +01:00
Horizon agentandClaude Opus 5.5 eb649daff5 Merge main into #10506: keep the Enderman hurt teleport
Main added enderman_hurt_teleports after the block contact damage; this
branch drops that call's world argument. Both kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:34:58 +01:00
Horizon agentandClaude Opus 5.5 1716888f1c Merge main into #10493: keep both ai.rs test modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:34:32 +01:00
Horizon agentandClaude Opus 5.5 9267013546 rustfmt the bullet Dir/Steps test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:57 +01:00
Horizon agentandClaude Opus 5.5 e95b6a26c5 Merge #10330 (main call site, rustfmt) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:57 +01:00
Horizon agentandClaude Opus 5.5 cfab7464ed Merge #10304 (main call site fixed) into #10330
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:11 +01:00
Horizon agentandClaude Opus 5.5 597adf33bd Pass no UUID map at the mob_to_nbt call main added since
c843c1aa0 (the loot-pickup save test) calls mob_to_nbt with the old four
arguments; the bridge's fifth, uuid_of, is |_| None there like the other tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:23:08 +01:00
martipopsandClaude Opus 5.5 9d141f2e48 Merge main into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:22:32 -05:00
Horizon agent 790d68e500 Merge remote-tracking branch 'origin/main' into fix-10383
# Conflicts:
#	crates/horizon-client/src/game/maps.rs
#	tools/command_exec_corpus.py
2026-11-06 09:15:51 +01:00
Horizon agentandClaude Opus 5.5 c72e75d8cf Merge main into #10384: keep the exported liquid flag
Main checked bubble columns by name (6bd507f89); this branch reads the
block's exported liquid flag, which covers the same three blocks and any
mod block marked liquid, so the name list and its test go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:13:58 +01:00
Detimes 3d47396e9b NBT: read and write one-byte strings whole
Most NBT strings in a saved chunk (names, ids, compound keys) are ASCII.
The reader decoded modified UTF-8 one byte at a time and the disk writer
put every unit through its own limit check. Now an all-ASCII string is
widened in one pass when read, and a string whose units are all one byte
(1 to 127) is written in one extend when it fits under the byte limit;
anything else takes the old per-unit loop, so the bytes and the error
offset are unchanged. A new test checks ASCII, zero, mixed and non-ASCII
strings against a per-unit reference and the limit's error offset.
2026-11-06 08:10:31 +00:00
Arslan ArmanulyandClaude Opus 5.5 e1a9161f7e Shader packs: the End under the dragon fight's fog has gbuffers_skybasic's fan
While the dragon fight's world fog leaves out the End's sky, Iris 1.11.7 still
runs a pack's gbuffers_skybasic (render stage SKY, vertex colour fogColor), and
the gl_Vertex values our probe packs read there (render distances 12 and 8) fit
a fan around the horizon: from (0, -16, 0) under the eye to eight rim points 16
above it, every 45 degrees, at the render distance. Horizon ran no sky program
in the End, so packs that paint their sky there left that band empty.

Frame carries the boss bar's world fog; PackSky draws only the fan then, in
the fog colour.

Part of #10491

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 13:06:31 +05:00
PzmuV1517 155bca35fd Shader packs: a pack's pipelines compile together on several threads
The driver compiles each program of a shaders/ pack as its pipeline is made,
and a pack's pipelines were made one at a time while the pack was laid out:
Metal's compiler service sat on one core for 5 to 50 s on a big pack (BSL,
Complementary, Photon) with four others idle.

The pack is now laid out first, its pipelines recorded as jobs whose pipeline
goes in a slot of its pass, then made all at once on up to 8 threads (wgpu
keeps error scopes per thread, so each is still checked). When wgpu or the
driver refuses one, the pack is built again a program at a time, as before,
so that program is skipped with its note exactly as it was.

Cold loads on an M5 (each pack's first load, nothing in the Metal cache),
pipeline creation: Bliss 6.3 s -> 1.8 s, Solas 5.2 s -> 1.6 s, Complementary
Unbound 9.0 s -> 2.3 s.
2026-11-06 09:58:20 +02:00
Horizon agentandClaude Opus 5.5 8e05ff9b80 Warden anger: a suspect angered again before its UUID lookup keeps its saved anger
AngerManagement.increaseAnger adds angerByUuid's anger for a new suspect
(removeInt, uncapped). The mob layer knows no UUIDs, so convertFromUuids' port
dropped the saved anger of a suspect that was angered again first (a reloaded
warden hearing the player within its first anger tick); it is added there now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:52:28 +01:00
Horizon agent c05ef4bc91 Merge #10330 (bullet target, bridge, main) into #10365
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 08:51:57 +01:00
Horizon agentandClaude Opus 5.5 a04eee71ba Shulker bullet load: wrap Dir as from3DDataValue, keep a negative Steps
Direction.LEGACY_ID_CODEC reads a byte through from3DDataValue, which wraps it
(abs(b % 6)), and getIntOr("Steps", 0) keeps any int; the read clamped Steps to
0 and passed an out-of-range Dir through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:51:18 +01:00
Horizon agent c29349aaf0 Merge #10304 (UUID bridge, with main) into #10330
Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	crates/horizon-client/src/entity_nbt.rs
2026-11-06 08:50:35 +01:00
Horizon agentandClaude Opus 5.5 8e35638625 Merge main into #10304
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:50:22 +01:00
GabrielNichols 3dd2820398 Open Recipes settings from the inventory browser 2026-11-06 04:38:32 -03:00
martipopsandClaude Opus 5.5 1cfaf0bc96 Controllers: each choice of a controller setting describes itself
The description at the bottom of the page stayed the same whichever value was
picked, on the Gameplay Guide, Button Glyphs, On-screen Keyboard and Lightbar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:13:53 -05:00
Florian ERNSTandCodex 3443abbde5 Keep superclass air refill for NoAI Nautilus
Skip only the Nautilus custom air handler under NoAI, preserving the existing Breath/drown path and normal custom behavior. Cover actual Game and full Mob ticks against fresh Java.

Co-Authored-By: Codex <[email protected]>
2026-11-06 07:59:32 +01:00
voidsip 23dcce8ab0 Merge main and retain both mob regression suites 2026-11-06 00:43:10 -06:00
voidsip e58593bb50 Fix wet mob extinguishing after fire damage 2026-11-06 00:39:49 -06:00
martipopsandClaude Opus 5.5 426786c862 Controllers: A takes hold of a slider, and the gameplay guide has levels
Left and right changed a slider as soon as the focus passed over it, so moving
through a settings page could change a value. A now takes hold of it, left and
right move it, A or B lets go and up or down moves on, with a white outline
while held.

The gameplay guide's on/off switch becomes Full, Basic and Off. Basic keeps the
right column. A file saved with the old switch keeps its meaning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:16:58 -05:00
voidsip 3e90d34739 Respect breathing effects and bubble columns for mobs 2026-11-06 00:13:10 -06:00
martipopsandClaude Opus 5.5 a7954d4bdc Merge the pad fix (#10474) into the gameplay guide
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:51:17 -05:00
masa-masa-0314andZCode 42849387d1 Realms: the template rows' website and trailer buttons
`RealmsSelectWorldTemplateScreen.Entry`'s 15x15 `icon/link` and
`icon/video_link` buttons beside each row's version, opening the
template's page or trailer; the drawn rectangles are kept for the click.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 13:05:17 +08:00
voidsip b67b146635 Validate forced Overworld respawn points 2026-11-05 20:03:51 -06:00
voidsip 04f7aa963b Keep treasure map targets when carrying undecorated copies 2026-11-05 19:55:51 -06:00
b2423d6d62 Write chunk-section blocks with one position lookup and walked words
Part of #9996.

BlockSection::set and BlockSection::fill_run are 7.0% of the proto phase
in the overworld and 9.4% in the Nether (callgrind on generate_proto).
Each write located its packed word twice, and fill_run rebuilt the word
mask for every block.

PackedStorage now hands out a Slot (word and bit shift) that read_at and
write_at take, so set locates the entry once; the palette-growth branch,
which re-packs the words, looks it up again. A run in one column moves
256 entries per Y, so while a word holds a whole number of those steps
(every width generation produces) the bit shift stays the same along the
run and WordRun writes each block with one load, mask and store. Widths
that do not divide the stride go one set per block, with the same palette
edits in the same order.

Live palette slots hold distinct states (sections loaded from disk are
rebuilt through set), so "already this state" is old_id == id and "was
air" is old_id == air_id, both resolved once per call. set on a uniform
section has its own small path.

Palettes, their order, the free list, counts, the chosen width and the
packed words are unchanged: the 8 chunkgen_bench snapshot files (terrain
and decorated; overworld seeds 12345 and -1, the Nether, the End) are
byte-identical to main, and a new unit test replays random set/fill_run
sequences over every palette width against main's fill_run.

Proto-phase instructions: overworld 6.497 G -> 6.340 G (-2.41%), Nether
2.974 G -> 2.859 G (-3.84%).

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
2026-11-06 02:52:28 +01:00
MSedgeMC 6fa16423ac Entity saves: a warden's anger keeps its suspects by UUID
`Warden.addAdditionalSaveData` stores an `AngerManagement`: a `suspects` list of
{uuid, anger} pairs, and nothing else about the anger. Horizon wrote an empty
`anger` compound and read nothing back, so a reloaded warden was calm.

`AngerManagement` now keeps the suspects that are not in the level by UUID next
to the live ones, both calming by one an anger tick, and looks the UUIDs up
again every two anger ticks through the game layer's map
(`Game::who_by_uuid`, reached from the mob layer through `MobWorld`'s new
`WhoLookup` seam). The chunk save names a suspect by its UUID, minting one for
a loaded suspect that has none yet.
2026-11-06 01:09:42 +00:00
MSedgeMC 712305de32 Entity saves: a UUID bridge for references to a mob
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.
2026-11-06 01:09:42 +00:00
TheMrZZandCodex gpt-6.1-sol 94b5bf1aa0 Fix dropped-item Health short conversion
Floor floating Health values before short narrowing and preserve integer narrowing. Add an actual decode-to-save regression and an isolated Game/Java command fixture.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 01:18:04 +01:00
MSedgeMC fc8db41b0d fix: a shulker's bullet keeps its target through a save
ShulkerBullet.addAdditionalSaveData writes `Target` (the UUID it homes on, only
while it has one), `Dir` (`Direction.LEGACY_ID_CODEC`, the step it is on, absent
until it picks one), `Steps` and the per-axis target deltas `TXD`/`TYD`/`TZD`,
over `Projectile`'s owner and LeftOwner; readAdditionalSaveData reads them back
and keeps `Target` as an `EntityReference` it re-resolves every tick.

Horizon's `saved_with_chunk` turned a bullet with a target away - which is every
bullet a shulker fires - and `spit_or_bullet_to_nbt` refused it too, so the chunk
save dropped the bullet, and nothing read a step or a delta back.

The writer writes those keys now, the reader reads `Dir`, `Steps` and the deltas
into the kind, and `Target` goes through the UUID bridge: `Game::save_who_uuid`
answers it with `who_uuid` over `EntityMemory::uuid_of` (nothing for a mob the
level has no UUID for, never a made-up one), and `Game::bullet_target` resolves
the stored UUID with `Game::who_by_uuid`, which names the player or a mob that is
loaded. A bullet whose target is not loaded comes back with none and falls, as
`ShulkerBullet.tick` does with a target it cannot find, rather than homing on
whatever its old id means now.

A loaded bullet's own random stream is seeded from where it is, since
`Entity.random` is not saved: the same stand-in `Projectile::new` gives an
arrow's.
2026-11-06 00:03:08 +00:00
MSedgeMC b07b8f865b Entity saves: a UUID bridge for references to a mob
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.
2026-11-06 00:03:08 +00:00
masa-masa-0314andZCode 8db0ec744e Realms: the reset-world screen, world templates and world upload
RealmsResetWorldScreen's tiles, RealmsSelectWorldTemplateScreen's paged
list, the local-world picker and RealmsUploadScreen with the tar.gz
packer and the upload body. Follows the 26.3 bytecode of those screens,
client.worldupload.RealmsWorldUpload/RealmsUploadWorldPacker/FileUpload
and RealmsClient.fetchWorldTemplates/resetWorldWithTemplate/
requestUploadInfo/putIntoMinigameMode.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 07:42:06 +08:00
martipopsandClaude Opus 5.5 0c0d33cb29 Controller e2e: run cases at once with --jobs
Each case already has its own game, home and socket. One at a time stays the default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:04 -05:00
martipopsandClaude Opus 5.5 edc49a0118 Controllers: the gameplay guide, what the buttons do now, in play and in inventories
In play the HUD's top corners name the buttons for what the crosshair is on, what's held and how the player is
moving. In an inventory the bottom corners do: plain face buttons at the bottom right, everything else at the
bottom left, by what the crosshair is on and what's carried. Rows follow bindings and step around what was drawn.
The mouse or keyboard taking over, a prompt, F1, F3 and the setting hide them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:04 -05:00
Florian ERNSTandCodex 27d81ba1b9 Omit empty Tags from modeled entity saves
Co-Authored-By: Codex <[email protected]>
2026-11-05 23:35:58 +01:00
TheMrZZandCodex gpt-6.1-sol e5e49e6242 Save spectral arrow duration and its default
Keep spectral duration in the loaded arrow state and always write its integer
value, including the default 200. Cover the live command route and load/save
without raw input while retaining broader type and display diagnostics.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 23:33:29 +01:00
MSedgeMC e08c7a3ee0 port_map: seven citations name functions the tree does not have
A ledger build from today's upstream/main reports seven map gaps, all rows
whose where cell names a function that no longer exists: PrimedTnt.explode
points at Game::tick_tnt (it is tick_one_tnt), FlatLevelSource.createState at
FlatTerrain::structure_settings (from_settings), PacketProcessor's hand-off at
conn::read_loop (io_pool::Pool::push), three ServerCommonPacketListenerImpl
keep-alive rows at conn::write_loop (that loop is wait_alive), and
PackSelectionScreen.Watcher is a class row written as a port.
2026-11-05 22:29:55 +00:00
MSedgeMC a33e5b574a The ledger knows a cloud's save is ported
AreaEffectCloud.addAdditionalSaveData was listed missing, which was right: only
the reader existed. game/potions.rs::potion_cloud_to_nbt writes it now, from
entity_chunks.
2026-11-05 22:12:59 +00:00
MSedgeMC 78cf6e36a0 Test that a lingering cloud survives a save
A cloud with a poison and a slowness effect, one of them hidden and one ambient,
written with potion_cloud_to_nbt and read back with potion_cloud_from_nbt: the
position, age, duration, radius, the four use settings, the duration scale,
RadiusPerTick, the effect list and the colour all come back. The test also
checks that the compound carries the field names vanilla's
readAdditionalSaveData looks for, and that it is saved as
minecraft:area_effect_cloud.
2026-11-05 22:12:59 +00:00
MSedgeMC bd991e7917 A lingering potion's cloud survives a save
AreaEffectCloud.addAdditionalSaveData writes the cloud's age, duration, radius,
the four use settings, RadiusPerTick, potion_duration_scale and its
potion_contents, and the client already read every one of them back
(game/potions.rs::load_potion_cloud): a cloud that was loaded from a save came
back whole. Nothing ever wrote one, though. entity_chunks writes the dragon, the
crystals, the item frames, the fangs and the rest kind by kind, and the
lingering clouds -- which live in breath_clouds, not in entity_lists! -- were in
none of those, so every replacement pass dropped the cloud a thrown potion or a
creeper left behind.

The save side now writes them: game/potions.rs::potion_cloud_to_nbt builds the
compound on the entity fields entity_nbt::entity_base_tag gives it, and
entity_chunks adds one tag per potion-bearing cloud of the chunk. A dragon's own
breath carries no potion and is not written.

The reader moved out of the Game method into potion_cloud_from_nbt, so the pair
can be tested against itself. The client keeps no potion id, so the effects are
written as custom_effects with the colour the cloud was drawn in, and the custom
particle and owner reference the vanilla cloud also saves have no counterpart
here.
2026-11-05 22:12:58 +00:00
MSedgeMC 18f1a16d12 test: a jukebox that stops posts jukebox_stop_play
A windowless game with a jukebox at (0, 64, 0): a disc put in and popped back
out posts one G 0,64,0 minecraft:jukebox_stop_play, a song ticked to its end
posts one, a broken jukebox posts none from this path (the world's spill posts
it) and a jukebox that never held a disc posts nothing.
2026-11-05 22:04:17 +00:00
MSedgeMC c7fc74cb26 fix: a jukebox that stops playing tells the listeners
JukeboxSongPlayer.stop posts GameEvent.JUKEBOX_STOP_PLAY at the block (and level
event 1011), and both ways a song ends reach it: popOutTheItem, when the disc is
taken out or a hopper pulls it, and tick's own hasFinished branch.

Horizon posted that event only from the world's removal path, so a broken
jukebox was heard but a jukebox whose disc was pulled out by hand, or whose song
ran out, was silent to a sculk sensor beside it. The paths that keep the block
now go through stop_song_heard; jukebox_removed keeps the plain stop_song,
because the world's spill already posts the same event once for it.
2026-11-05 22:04:17 +00:00
MSedgeMC eb649762c3 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-05 21:03:30 +00:00
TheMrZZandCodex gpt-6.1-sol 93365662aa Save default icon flags in custom potion effects
Resolve an absent saved show_icon from show_particles while preserving
explicit icon bytes and the remaining potion fields. Keep this in the
saved item encoder, without changing the general component view.

Add resource-free decoded/modelled codec coverage and a guarded,
zero-tick chest command regression against the pinned Java behavior.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 21:52:58 +01:00
TheMrZZandCodex gpt-6.1-sol 62d0687fea Merge main and retain the crafting map regression group
Keep the map-crafting, transmute, mob-effect and frame-map groups and their
isolation sets. The merged game passes the focused 62-record Java replay.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 20:51:24 +01:00
TheMrZZandCodex gpt-6.1-sol 751423b897 Defer villager trade reputation to the eligible AI phase
Retain the real last trader on the villager when a trade is accepted,
then deliver the coalesced reputation event after Brain.tick. Keep it
across menu close and live NoAI edits, and refresh current offer prices
when the event actually runs.

Add an actual-owner merchant regression with complete raw capture and
separate strict-all byte-order diagnostics.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 20:39:57 +01:00
DUzzL 219a56f260 Unstick arrows and tridents the server sets moving (AbstractArrow.lerpMotion) 2026-11-05 20:27:46 +01:00
Riccardo GuerrieroandClaude Opus 5.5 e2b9c29ff0 Breaking a painting, a cushion or a boat makes a vibration
Killing an entity that is no mob posts ENTITY_DIE (frequency 15) at the
entity, as vanilla's kill paths do: BlockAttachedEntity.kill (cushions
punched, burnt, unsupported, struck or blown up; paintings; empty item
frames), ArmorStand.kill (broken, burnt out, blown up, /kill's
generic_kill), VehicleEntity.destroy (boats and minecarts, not a creative
hit, which only discards them) and Entity.kill from /kill for the other
non-living kinds. The attacker is the source when a hit kills it
(attributedTo), otherwise the entity itself. A frame holding an item only
gives the item up and does not die (ItemFrame.hurtServer). The event runs
on the server's path, so single player, LAN guests and dedicated servers
all get it.

New lan_e2e check entity-die: a sneaking host and LAN guest break each
kind 3 blocks from a sculk sensor, which now hears 15.

Part of #10046

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8LUjYucp4kaN6UfSg7
2026-11-05 20:06:34 +01:00
Florian ERNSTandCodex ff0a8b31f8 Read selected guest progress for advancement selectors
Co-Authored-By: Codex <[email protected]>
2026-11-05 19:11:09 +01:00
TheMrZZandCodex gpt-6.1-sol 9365b979ac Expose live boat leash data to entity commands
Apply the existing leash writer after the boat's base command tag so live
holders and pending links override stale input data. Keep holder policy
and boat behavior in their existing implementations.

Add an actual Game getter regression and a guarded Java/Game fixture
covering owner clicks, repeated reads, pending UUIDs and detached links.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 17:32:38 +01:00
some-du6eandClaude Opus 5.5 a99b380175 Format the client lib's re-exports
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:17:37 -06:00
some-du6eandClaude Opus 5.5 6d7496f62a OptiFine custom skies: draw a pack's sky layers over the sky
Each frame picks the layers of the Overworld's or End's sky whose day,
weather, biome and height hold at the camera, eases them over their
transition, and draws them as 3x2 skyboxes with their blend mode, over the
vanilla sky's disc and sunrise (the End's box) and under the sun, moon and
stars. Packs without optifine/sky draw nothing new.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 10:10:20 -06:00
MSedgeMC e28eb5363a port_map: isBaby and canBeABaby name the species table
AgeableMob.isBaby is now a row of its own pointing at Mob::is_baby, which is
`can_be_a_baby && getAge() < 0`; canBeABaby points at
species::can_be_a_baby, and the six classes that override it to false carry a
row each so the false arms are not read as unported.
2026-11-05 15:40:15 +00:00
MSedgeMC 24267c893b test: pin the species a negative age cannot make a baby
living.rs::a_negative_age_is_a_baby_only_where_can_be_a_baby_allows_it drives
Mob::new on the loading table's own types and sets Age to -24000, the
BABY_START_AGE a save or a /summon carries, then asks is_baby, dims and
voice_pitch - the three the game draws and hears from.

A parrot, a tadpole, a frog, a zombie nautilus, a camel husk, a slime, a magma
cube, a piglin brute and a creeper keep their type's box (the tadpole 0.4 by
0.3, the camel husk 1.7 by 2.375, the piglin brute 0.6 by 1.95) and voice at
1.0 ± 0.2. The control is a cow at the same age: is_baby true, Cow's
BABY_DIMENSIONS (0.45 by 0.7, eyes at 0.69) and 1.5 ± 0.2, and an adult box and
voice again at age 0. With is_baby back to `age < 0` the test stops at the
parrot.

entity_nbt's save test now runs a frog and a camel husk beside the slime, the
magma cube and the pig: the first four write no Age and keep none of a raw
compound's Age, the pig writes its modeled -37.
2026-11-05 15:40:15 +00:00
MSedgeMC 3c7bba9bdb fix: a mob that cannot be a baby is not one at a negative age
AgeableMob.isBaby is `canBeABaby() && getAge() < 0`, and canBeABaby is true
across the ageable tree but for the six classes that override it to false:
Frog, Parrot, ZombieNautilus, CamelHusk, Slime and MagmaCube (an
AbstractCubeMob is an AgeableMob). Everything outside that tree inherits
LivingEntity.isBaby's false, so a Giant, a Creeper, a PiglinBrute (it extends
AbstractPiglin and declares no isBaby of its own) and a Tadpole, whose own Age
runs up from -24000, are never babies either; only the zombies, the piglins and
the zoglin answer a synced DATA_BABY_ID instead of an age.

Mob::is_baby read `extra.age < 0` alone. The list of who can be a baby existed
already, but only as mobs::drops::can_be_baby, which gates the metadata baby
flag and the loot table: the box, the voice, breeding, growth and the goals
read the age and so called a frog, a parrot, a creeper or a tadpole with a
negative Age a baby. The list is now species::can_be_a_baby, on the species
table, and is_baby is `can_be_a_baby && age < 0`.

What that changes: a tadpole (Age -24000, its own DEFAULT_AGE) is drawn at
getAgeScale's 1.0 instead of 0.5 and speaks at 1.0 ± 0.2 instead of 1.5 ± 0.2,
as vanilla does; a parrot, a frog, a zombie nautilus, a camel husk, a slime, a
magma cube, a piglin brute, a creeper or anything else the table cannot make a
baby keeps an adult's box, voice, breeding, growth and drops. The Parrot arm of
voice_pitch was there for the same reason and is dead now: Parrot.getVoicePitch
is `getPitch(random)`, the plain range an adult already gets.

AgeableMob.addAdditionalSaveData writes Age only when canBeABaby, and the
branch that drops the values a raw compound carried named four classes by hand;
it asks the ageable table now, which also covers the frog and the camel husk
that were re-saving an Age vanilla writes for neither.
2026-11-05 15:40:15 +00:00
DANILIAMMOandCodex GPT-6 86c0211c35 launcher: preserve manually installed Nightlies during pruning
Record a marker only for Nightlies installed by automatic updates. Keep
manual and legacy Nightlies out of GUI and CLI cleanup, while retaining
the newest managed Nightlies separately from game releases.

Add regression coverage for manual, legacy, selected and pinned builds.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-05 20:34:10 +05:00
masa-masa-0314andZCode dd35df0f18 Realms: the backup list and backup info screens
The Worlds tab's Backup button now opens RealmsBackupScreen: a row per automatic backup
with its date (mco.backup.entry), Restore per row, and Back. A row opens
RealmsBackupInfoScreen: the backup's metadata as translated key/value rows
(mco.backup.entry.<key> when the language has it). Restoring asks nothing (vanilla's
Restore button restores at once) and the list reads again when it is done.

RealmsClient gains backupsFor (GET worlds/{id}/backups) and restoreWorld
(PUT worlds/{id}/backups?backupId={id}, vanilla's 40 s/600 s timeouts), with Backup's DTO
(backupId, lastModified, size, metadata, changeList). The backup id goes into the query
percent-encoded, so it cannot add a parameter.

Download Latest still needs the download-and-unpack flow and is left for its own PR; the
rows show the date and the metadata screen the details (size included).

Co-Authored-By: ZCode <[email protected]>
2026-11-05 23:18:13 +08:00
DANILIAMMOandCodex GPT-6 921a31e8a1 launcher: include dated Nightlies in automatic version pruning
The GUI runs the shared pruning path after saving its selection, but its
release-only filter excludes every dated Nightly. Include those builds
so the existing retention, selection, instance-pin and in-use protections
also apply to GUI Nightly updates.

Add regression coverage for following updates, selected and pinned
Nightlies, and retention across Nightly/release/local builds.

Co-Authored-By: Codex GPT-6 <[email protected]>
2026-11-05 20:03:57 +05:00
TheMrZZandCodex gpt-6.1-sol 038365e19b Post-process maps taken from crafting results
Use the level's backed maps for crafting recipe matching and run map
post-processing on the actual stacks taken by swap, pickup and quick-move.
Keep the original-copy callback used by quick-move and preserve cartography.

Add a real Game/Menu regression and a guarded connected-owner Java/Game
fixture covering previews, consumed inputs, transferred maps and saved backing.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 15:52:08 +01:00
masa-masa-0314andZCode 3b29a9fbc5 Realms: the invite-code list and its edit screen
The Players tab's second button now opens RealmsInviteCodesScreen: the count in the
title, the subtitle, a row per code with its expiration (or Never), Copy / Edit / Delete
per row, and Create. RealmsEditInviteCodeScreen edits one code: its active flag, the
seven InviteCodeExpiration choices (a click on the slider track picks the nearest — this
screen has no drag event yet), Copy to the clipboard (a new Outcome::Copy the title
turns into Action::CopyText), Delete, Done and Back.

RealmsClient gains inviteCodes (GET api/v1.0/invite/codes?realmId={id}),
createInviteCode (POST .../invite/codes), updateInviteCode (POST .../update) and
deleteInviteCode (DELETE .../delete/{code}), all on the invite-code service with the
token in x-mojangauth, with InviteCode's DTO. The existing redeemInviteCode now goes
through the same helper. An existing expiration reads back as the closest of the seven
choices (InviteCodeExpiration.closestTo).

Co-Authored-By: ZCode <[email protected]>
2026-11-05 22:51:09 +08:00
masa-masa-0314andZCode c3d63d1e98 Realms: the settings tab and the region preference screen
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]>
2026-11-05 22:04:46 +08:00
TheApple05andCodex cc3ee16e46 Merge upstream main after language-key validation fix
Preserve the existing adapter and maintainer history while including the
upstream removal of stale Menu Navigation translation keys.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 15:01:06 +00:00
TheApple05andCodex 42372aa760 Merge current upstream main after older-server review fixes
Preserve published history and upstream shader changes.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:59:28 +00:00
TheApple05andCodex d29c8eb63f Fix legacy attribute, llama carpet and block entity conversions
Select attribute and carpet registry names from the actual server release.
Upgrade incoming pre-component block entities on both update and chunk paths,
retaining tag identity and vanilla typed-field remainders.

Add independent registry and vanilla DFU regression fixtures, including
missing IDs, removed types and unchanged current-version tags.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:58:52 +00:00
TheApple05andCodex 8852c0e0b8 Merge current upstream main before older-server review fixes
Preserve published compatibility commits and newer maintainer changes before addressing the protocol-764 review. This merge alone claims no new compatibility or parity result.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 12:18:11 +00:00
rdbtCVSandCodex gpt-6.1-sol d3e25901ff Adapt native level particle speed to the per-axis packet representation
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:14:10 +00:00
rdbtCVSandCodex gpt-6.1-sol 54bef9b2fb Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:48 +00:00
rdbtCVSandCodex gpt-6.1-sol 4e294c46f1 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:45 +00:00
rdbtCVSandCodex gpt-6.1-sol 67268cb5b4 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:43 +00:00
rdbtCVSandCodex gpt-6.1-sol 6bd3217727 Merge main and reconcile parallel changes
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-04 12:01:41 +00:00
TheApple05andCodex 21f7031e72 Merge upstream main before resuming ViaHorizon compatibility
Preserve the published older-server contribution and current upstream changes as separate parents before the next tested extension. No new compatibility or parity result is claimed by this merge.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 10:43:04 +00:00
TheApple05andCodex 7dde5f518c Add experimental native Minecraft 1.20.2 protocol adapter
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 05:15:58 +00:00
TheApple05andCodex b65d5f5dcd Fix resource-pack instance identity across replacement and reconnect
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 05:12:56 +00:00
TheApple05andCodex f88c50772f Merge upstream main during protocol 764 development
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:41:58 +00:00
TheApple05andCodex d064372d9d Merge upstream main before protocol 764 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:13:35 +00:00
TheApple05andCodex ca0a90eee3 Add native protocol 765 compatibility for Minecraft 1.20.3 and 1.20.4
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:01:11 +00:00
TheApple05andCodex c6780b8daf test: establish TCP backpressure before disconnect assertions
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 04:01:10 +00:00
TheApple05andCodex aca15c91d4 Merge upstream main before adding 1.20.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 02:40:26 +00:00
TheApple05andCodex 01166b25ae Add experimental Minecraft 1.20.5 and 1.20.6 server compatibility
Translate protocol 766 attributes, food, horse inventories, projectile power,
chat holders and item use into the pinned protocol. Supply the old static
enchantment and painting names, and normalize legacy wolf variant holders.

Reuse verified recipe and boat layouts and cover changed codecs with
independent vanilla wire fixtures, including full chat packet translations.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 02:29:55 +00:00
TheApple05andCodex 311cf0d31f Merge upstream main before adding 1.20.6 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:54:44 +00:00
TheApple05andCodex 9e9078f17f Add experimental Minecraft 1.21 and 1.21.1 server compatibility
Translate protocol 767 packets, recipes, item components and generic boat
variants into the pinned protocol. Preserve old movement and interaction
layouts, and correct simple spell particles across the older adapters.

Add independent vanilla wire fixtures and document the experimental scope.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:39:23 +00:00
TheApple05andCodex d00c6bcd25 Merge upstream main before adding 1.21.1 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 01:02:51 +00:00
TheApple05andCodex a96e04043e Extend native older-server support to Minecraft 1.21.2 and 1.21.3
Translate protocol 768 particles, player-list defaults, vehicle movement and
integer model data through the existing older-server pipeline. Preserve old
creaking heart states and transient entities through explicit registry aliases,
and keep legacy trail lifetimes in the client particle provider.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:50:10 +00:00
TheApple05andCodex 424f045246 Merge upstream main before adding 1.21.3 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:29:05 +00:00
TheApple05andCodex dfd4b135ef Extend native older-server support to Minecraft 1.21.4
Translate legacy chunk framing, item patches, recipe previews, chat and
entity metadata into the pinned client representation. Preserve old click
operations through authoritative server resynchronization and cover older
stack/profile layouts with independent vanilla wire fixtures.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:04:47 +00:00
TheApple05andCodex baceff1b55 Merge upstream main before adding 1.21.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-04 00:01:44 +00:00
TheApple05andCodex de808d2d30 Merge upstream main before adding 1.21.4 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 23:20:25 +00:00
TheApple05andCodex 44338647fa Extend native older-server support to Minecraft 1.21.5
Map protocol 770's packet and registry ids, remove vanilla's unused chunk
padding, renumber command parsers before reading their properties, and
translate player actions, crouch transitions and hanging-entity metadata.
Convert pre-1.21.6 attribute and equippable components in both directions,
with independent vanilla wire fixtures and protocol regression coverage.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 23:15:23 +00:00
TheApple05andCodex 3f41fda7a1 Merge upstream main before adding 1.21.5 compatibility
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:57:14 +00:00
TheApple05andCodex 57bc596fe1 Extend the native older-server adapter to 1.21.6
Reuse the packet layouts shared with 1.21.7/1.21.8, map the item and sound
registry ids before the lava chicken insertion, and generate inherited tables
only where the vanilla reports differ. Cover shifted discs and sound holders
and exercise the shared metadata and packet conversion cases on both protocols.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:53:22 +00:00
TheApple05andCodex 4090de57e0 Merge upstream main before extending older-server support
Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:35:53 +00:00
TheApple05andCodex 1d78698703 Add experimental native protocol support for 1.21.8
Generate vanilla packet and registry mappings, translate the changed layouts
and player metadata, preserve renamed chain states, and extend the offline
protocol probe to fail on decode errors and follow dimension changes.

Co-Authored-By: Codex (GPT-6) <[email protected]>
2026-11-03 22:18:41 +00:00
levzzz 9a344e0c31 Merge main into modapi/entity-state 2026-11-03 01:08:57 +05:00
levzzzandClaude Opus 5.5 d48b349d72 Native mods: teleport, rotation, head yaw, facing, size and pushes through EntityRef (#7387)
teleport_to and set_rotation move and turn any entity as /tp and /rotate
do; head_yaw, set_head_yaw, direction, dimensions and push.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:19:06 +05:00
levzzzandClaude Opus 5.5 3568cc4cc7 Native mods: name the shared sound and particle checks outside the hook catalog
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:11:41 +05:00
levzzzandClaude Opus 5.5 d68ada1e13 Native mods: an entity's state flags through EntityRef (#7387)
is_shift_key_down, is_crouching, is_sprinting, is_in_water, is_in_lava,
fire_immune, fire_ticks, air_supply, max_air_supply, is_silent,
set_silent, is_invisible and pose, for players and mobs, and the ones
Horizon keeps for armor stands and items.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:10:46 +05:00
levzzzandClaude Opus 5.5 c367cb95a2 Native mods: sounds and particles in a level through LevelRef (#7387)
level.play_sound and level.send_particles send to every player in range,
as ServerLevel.playSound and sendParticles do, from any server hook (not
only a level hook's LevelWriteHost). The checks the level-hook versions
make move into native_sound_ok and native_particles_ok, shared by both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-03 00:03:19 +05:00
levzzzandClaude Opus 5.5 25d9cc84c2 Native mods: a level's seed, difficulty, game rules and weather through LevelRef (#7387)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 22:05:17 +05:00
1600 changed files with 193926 additions and 22338 deletions

No files matched your search

+17 -16
View File
@@ -51,7 +51,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings), modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -65,6 +65,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
@@ -92,8 +93,8 @@ reads `README.md`, `launcher/links.json`, `docs/api/`, `roadmap/roadmap.json` an
```sh
python3 tools/pr_ready.py # the self-check: fmt, clippy -D warnings, touched crates' tests, harnesses; paste its block in the PR
python3 tools/pr_ready.py --quick # same without harnesses
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the 104 examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the 5 slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo test -p horizon-client -p horizon-client-lib --lib --bins --tests # skips the examples (pr_ready does this); `--doc` separately
cargo test -p horizon-client-lib --lib mod_content::tests:: -- --ignored # the slow content-root tests, #[ignore]d; pr_ready runs them when mod content, assets, blocks or items change
cargo build --profile fast -p horizon-client # optimized, quick to link: use it to play or run lan_e2e
python3 tools/parity.py --changed origin/main # the harnesses your branch can affect (CI's parity job)
python3 tools/parity.py --changed-files paths.txt --list # which harnesses cover these files, without running them
@@ -105,8 +106,8 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
- Harnesses and the game need the vanilla data in `.local/` (`minecraft/`, `horizon-client/26.3`, `datagen/`,
`tools/`): see [docs/parity-workflow.md](docs/parity-workflow.md). Without it, pr_ready lists the
harnesses for CI.
- A harness = `tools/test_<name>_parity.py` (324 of them) + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (horizon-world 175, horizon-client 104),
- A harness = `tools/test_<name>_parity.py` + a Java oracle (`mods/horizon/src/test/java/dev/horizonlab/parity/Vanilla*Oracle.java`)
+ a Rust probe: `crates/<crate>/examples/<name>_{probe,fixture}.rs` (mostly horizon-world and horizon-client),
or the game binary with a fixture env var (`game/dispense_fixture.rs`, `game/sculk_fixture.rs`,
`game/command_harness.rs`, `game/net_harness.rs`). Copy a nearby harness for a new one.
- Developing a native mod? Run `horizon-launcher settings native-profile dev` once. `dev` builds the game crate
@@ -120,7 +121,7 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
Source paths remain in `crates/horizon-client/`. Measure actual harness selection on refreshed
records with `tools/parity.py --changed-files paths.txt --list`; historical counts are not a guarantee.
- Rust integration tests: `crates/horizon-client/tests/` (`lan_bot.rs`, `generation_saves.rs`, `streaming_meshes.rs`).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio and controllers (the server build).
- Linux needs `libasound2-dev libudev-dev`; `--no-default-features` drops audio, controllers, `discord`, `friends` and `heartbeat` (the server build).
- Run and drive the game (starts the `horizon-client` beside `horizon-ctl`; other arguments go to the game):
`target/fast/horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --offscreen --wait --resources $PWD/.local/horizon-client/26.3`,
then `horizon-ctl --socket … state|keys|turn|command|screenshot|quit`
@@ -151,23 +152,23 @@ state stays separate from the library tests. This compiles gameplay twice for ex
| Subsystem | Files |
|---|---|
| Game loop, tick, frame | `game.rs`: `fn run`, `impl ApplicationHandler for Game` (`window_event`, `about_to_wait`), `fn tick`, `fn tick_world`, `fn render`; L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs` |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `postprocess.rs`; game.rs `fn update_world` |
| Game loop, tick, frame | `game.rs`: `fn run`, `fn tick`, `fn tick_world`, `fn render`; G: `window_handler.rs` (`impl ApplicationHandler for Game`: `window_event`, `about_to_wait`); L: `clock.rs`, `spans.rs`; `horizon-client-input/src/window.rs` |
| Rendering | L: `gfx.rs` (`Renderer::new`, `Renderer::render`), `gfx/` (cull, occlusion, translucency, upload, timing, lod, weather, stars, end_sky), `*.wgsl`, `particles.rs`, `block_entity_render.rs`, `item_render.rs`, `fog.rs`, `clouds.rs`, `camera.rs`, `first_person.rs`; G: `render_tick.rs`, `item_draw.rs`, `screen_backgrounds.rs` (panorama, menu blur, border wall) |
| Chunks, streaming, light | L: `streaming.rs` (worker pool: generate, light, mesh), `world.rs`, `chunk.rs`, `chunk_map.rs`, `light.rs`, `mesher.rs` + `mesher/`, `vanillagen.rs` (binds horizon-world's generator), `lod.rs` + `lod/`, `server_terrain.rs` (a server's kept chunks), `bluemap.rs` + `bluemap_detect.rs` (far terrain from its web map), `ticket_levels.rs`; G: `world_loading.rs`, `world_entry.rs`, `postprocess.rs`; game.rs `fn update_world` |
| World generation | `crates/horizon-world/src/worldgen/` (`generator*.rs`, `feature_graph.rs`, `structure/`, `carver/`) |
| Block behaviour | `crates/horizon-world/src/level/` (`mod.rs`, `tick.rs`, `hooks.rs`, `redstone.rs`, `place.rs`, `interact.rs`); game.rs `fn tick_blocks`, `fn process_level_events` |
| Mobs | L: `mobs.rs` (types), `mobs/` (`ai.rs`, `goals.rs`, `brain.rs`, `*_brain.rs`, `nav.rs`, `path.rs`, `species.rs`, `spawning.rs`, `render.rs`, `anim.rs`); G: `creatures.rs` (ticking, spawning, mob events), one file per mob family (`dragon.rs`, `wither.rs`, `villagers.rs`, `piglins.rs`, `golems.rs`...), `natural_spawning.rs` |
| Other entities | L: `entities.rs`, `projectiles.rs`, `arrows.rs`; G: `boats.rs`, `minecarts.rs`, `mounts.rs`, `armor_stands.rs`, `paintings.rs`, `frames.rs`, `fireworks.rs`, `tnt.rs`, `explosion.rs` |
| Player actions | game.rs (`fn use_hand`, `fn use_item`, `fn attack`, `fn tick_mining`, `fn break_block`); G: `hand.rs`, `combat.rs`, `consume.rs`, `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs` |
| Player actions | game.rs (`fn use_hand`, `fn tick_mining`, `fn break_block`); G: `item_actions.rs` (`fn use_item`), `melee.rs` (`fn attack`), `hand.rs`, `combat.rs`, `consume.rs`, `using_item.rs` (an item in use, tick by tick), `potions.rs`, `shield.rs`, `crossbow.rs`, `trident.rs`, `fishing.rs`; L: `motion.rs` + `motion/` (movement), `collisions.rs`; crate `horizon-items`: `items.rs`, `item_data.rs`, `enchant.rs`, `blocks.rs` |
| Inventory and containers | L: `menu.rs` (`AbstractContainerMenu.clicked`), `container_screen.rs`, `containers.rs`, `creative.rs`, `crafting.rs`, `recipe_book*.rs`, `stations.rs`, `merchant.rs`; game.rs `fn menu_ops`, `fn click_inventory`; G: `stations.rs`, `trading.rs`, `recipe_book.rs`, `machines.rs`, `inventory_input.rs` (keys and clicks over a container), `inventory_slots.rs` (slot layout and drawing) |
| Commands | parse: crate `horizon-command` (`args.rs`, `selector.rs`, `tree_*.rs`, `vanilla.rs`); run: G: `commands.rs` (dispatch) + `commands/*.rs` (`admin`, `execute`, `items`, `players`, `world_cmds`...); L: `scoreboard_cmd.rs`, `data_cmd.rs`; `horizon-client-core`: `nbt_path.rs`, `scoreboard.rs`; G: `functions.rs`, `command_blocks.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs` |
| UI screens | `Screen` enum in game.rs; L: `title.rs` + `title/`, `options_ui.rs`, `multiplayer.rs`, `pack_screen.rs`, `graphics_ui.rs`, `screen_draw.rs`, `ui.rs` (widgets), `hud.rs`, `hud_layout.rs`, `hud_overlays.rs`, `hud_kit/` (Horizon overlays' `Room` and `Plate`, [docs/ui-kit.md](docs/ui-kit.md)), `chat.rs`, `tab_list.rs`; G: `startup_menu.rs`, `loading_screen.rs` (the loading screen, the pre-game `App`), `screens.rs` (pause, death), `options.rs`, `lan_screen.rs`, `chat.rs`, `sign_edit.rs`, `books.rs`, `creative_screen.rs` |
| Multiplayer client | L: `net.rs` (network thread); G: `remote.rs` (`fn handle_packet`, `fn tick_remote`), `remote_*.rs` (mob data, objects, projectiles, vehicles, stations) |
| LAN host / server side | L: `lan/` (`conn.rs` handshake + login threads, `hub.rs` + `hub/` play state, `chunks.rs`, `entity.rs`, `tracking.rs`, `guest.rs`, `playerdata.rs`); G: `lan.rs` (`LanHost`), `lan_actions.rs` (`fn as_guest`, `fn tick_guests`), `lan_*.rs` per feature, `lan_entities.rs` (tracked entities) |
| Integrated server | G: `integrated.rs` (the server thread), `levels.rs` (one level per dimension, `fn tick_server`), `server_travel.rs` |
| Dedicated server | G: `dedicated.rs`, `dedicated/status.rs`; crates `horizon-server-files`, `horizon-server`, `horizon-anticheat` (L: `lan/anticheat.rs`) |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`, `fn key_pressed`, `fn play_mouse`; crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs` |
| Save and load | L: `save.rs` (level.dat, regions, playerdata), `entity_nbt.rs`, `options_file.rs`; `horizon-client-core/src/block_entity_nbt.rs`; G: `saving.rs`, `persist.rs` (entities), `world_session.rs` (opening and closing a world); `crates/horizon-world/src/{region.rs,nbt.rs,datafix/}` |
| Input | game.rs `fn window_event`; G: `play_keys.rs` (`fn key_pressed`), `play_input.rs` (`fn play_mouse`, toggled keys, the hotbar wheel); crate `horizon-client-input` (`keybinds.rs`, `keymapping.rs`, `key_names.rs`, `mouse.rs`, `input.rs`, `interaction_input.rs`, `gamepad.rs`, `clipboard.rs`); L: `osk.rs`; G: `pad.rs`, `pad_menu.rs`, `pad_craft.rs`, `pointer_capture.rs` (mouse look, pointer grab) |
| Audio | L: `audio.rs` (mixer), `sound_engine.rs`, `sound_events.rs`, `music.rs`, `ambience.rs`; G: `music.rs`, `subtitle_hud.rs` |
| Resource packs | L: `packs.rs` + `packs/`, `pack_screen.rs`, `atlas_sources.rs`, `sprites.rs`, `mipmap.rs`, `model_library.rs`, `font.rs`, `mobs/cem.rs` + `mobs/cem_anim.rs` (OptiFine/EMF entity models and their animations); `horizon-client-core/src/lang.rs`; crates `horizon-assets`, `horizon-optifine`; G: `packs.rs` |
| Agent control | L: `control.rs` (socket), `bin/horizon-ctl/`; G: `control.rs`, `agent.rs`, `events.rs`, `ui_control.rs` ([docs/agent-control.md](docs/agent-control.md)) |
@@ -176,7 +177,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
## How `Game` is organised
- `struct Game` (game.rs, ~290 fields) holds everything. Its methods are spread over ~180 files: game.rs keeps
- `struct Game` (game.rs) holds everything. Its methods are spread over many files: game.rs keeps
the core (tick, render, input, inventory, world open/close) and each `game/<feature>.rs` adds an
`impl Game<'_>` block for one feature, with its own state types. Fields stay private to the gameplay module and its descendants. Gameplay tests run in the gameplay library, separate from reusable-library tests.
- Find a feature's code: `grep -rn "fn tick_<thing>\|fn <verb>_<thing>" crates/horizon-client/src/game*`;
@@ -190,7 +191,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
`levels.rs` swaps whole dimensions in the same way. Changes reach clients through the block log, level
events and the entity tracker (`lan/tracking.rs`, `game/lan_entities.rs`).
- Shared read-only data: `Resources` (`res`: blocks, items, recipes, mob types, biomes) and `PackResources`
(`pack`), both in game.rs.
(`pack`), both in game.rs, their methods in `game/resource_tables.rs`.
## Single player runs on a server thread
+5 -1
View File
@@ -33,7 +33,7 @@ queue (the hub, or `/suggest` on Discord) rather than to issues, which are for b
Wherever vanilla has a feature, vanilla is the specification, down to the order of random calls. "Parity" here means
Horizon gives the same results as the pinned 26.3 client and server for the same inputs, checked by a **harness**: a
`tools/test_*_parity.py` script that runs a small Java oracle against the real game and a Rust probe against Horizon,
and compares their outputs. There are about 290 of them.
and compares their outputs.
- **Read the vanilla code before writing Horizon's.** `tools/rea_vanilla.py` queries the pinned game bytecode:
`python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom`.
@@ -61,6 +61,10 @@ another project's source may also be read and used, **only when that project's l
"source available" licenses, all rights reserved (OptiFine), and any project with no license or an unclear one.
Don't read, copy, translate or paraphrase their source, and don't send code an AI tool wrote from it. Being able
to download a mod or a pack does not give you the right to read its files.
- **Java's own library is GPL too.** OpenJDK's source (`java.lang.Math`, `StrictMath`, `FdLibm`, `HashMap`, the
sorts, `Double.toString`, HotSpot's intrinsics...) is GPL-2.0, so it is off limits like the rest. Reproduce Java's
behaviour from its API documentation (the Javadoc) and from what a JVM returns, or from a permissive original such
as Sun's fdlibm C sources (see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)); see #6741.
- **Check the license yourself.** Read the LICENSE file at the commit you use and the header of each file you take:
some repositories mix licenses, and a file under another license is off limits. A license that changed: the one at
your commit counts. In doubt, don't: ask in #contributors first.
Generated
+56 -101
View File
@@ -131,9 +131,9 @@ dependencies = [
[[package]]
name = "arboard"
version = "3.6.1"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917"
dependencies = [
"clipboard-win",
"log",
@@ -142,11 +142,20 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"wl-clipboard-rs",
"x11rb",
]
[[package]]
name = "arc-swap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
dependencies = [
"rustversion",
]
[[package]]
name = "arcstr"
version = "1.2.0"
@@ -243,6 +252,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "base64ct"
version = "1.8.3"
@@ -1411,6 +1426,7 @@ dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"crc32fast",
"dunce",
"flate2",
"fs4",
@@ -1493,6 +1509,7 @@ dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"crc32fast",
"flate2",
"fs4",
"getrandom 0.3.4",
@@ -1552,7 +1569,7 @@ name = "horizon-client-render"
version = "0.1.0"
dependencies = [
"ab_glyph",
"base64",
"base64 0.22.1",
"bytemuck",
"chrono",
"cpal",
@@ -1571,6 +1588,7 @@ dependencies = [
"horizon-mod-api-gen",
"horizon-narrator",
"horizon-optifine",
"horizon-polytone",
"horizon-protocol",
"horizon-shader-api",
"horizon-shaders",
@@ -1621,7 +1639,7 @@ dependencies = [
name = "horizon-cosmetics"
version = "0.1.0"
dependencies = [
"base64",
"base64 0.22.1",
"glam",
"png",
"serde",
@@ -1649,7 +1667,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"socket2 0.5.10",
"socket2 0.6.5",
"spake2",
]
@@ -1754,6 +1772,15 @@ dependencies = [
"regex",
]
[[package]]
name = "horizon-polytone"
version = "0.1.0"
dependencies = [
"log",
"regex",
"serde_json",
]
[[package]]
name = "horizon-port-pack"
version = "0.1.0"
@@ -1793,6 +1820,7 @@ dependencies = [
"serde_json",
"sha1",
"sha2",
"socket2 0.5.10",
]
[[package]]
@@ -1808,7 +1836,7 @@ dependencies = [
name = "horizon-release-sign"
version = "0.1.0"
dependencies = [
"base64",
"base64 0.22.1",
"ring",
]
@@ -1817,7 +1845,7 @@ name = "horizon-server"
version = "0.1.0"
dependencies = [
"ab_glyph",
"base64",
"base64 0.22.1",
"chrono",
"getrandom 0.3.4",
"horizon-anticheat",
@@ -1848,7 +1876,7 @@ dependencies = [
name = "horizon-server-files"
version = "0.1.0"
dependencies = [
"base64",
"base64 0.22.1",
"horizon-core",
"horizon-protocol",
"horizon-world",
@@ -2052,12 +2080,12 @@ dependencies = [
[[package]]
name = "if-addrs"
version = "0.13.4"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2381,9 +2409,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
version = "0.16.4"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
[[package]]
name = "lru-slab"
@@ -2482,10 +2510,11 @@ checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]]
name = "mysql"
version = "27.0.0"
version = "28.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090068bc0a24762663338da78414148333d1ee246a7293c10a517ddf1548b291"
checksum = "ca3de8a73b7c70f76dbcdd4b6abc9b53bbb6c4f7bd6c79d8e8a7ea2157c02487"
dependencies = [
"arc-swap",
"bufstream",
"bytes",
"crossbeam-queue",
@@ -2509,11 +2538,11 @@ dependencies = [
[[package]]
name = "mysql_common"
version = "0.36.2"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20e7f6a15265801679ddf1ddb38fd74b4555a677a78378d4561fbe5f0b1d4d72"
checksum = "0f27695f286b461da077b8c2f72f47feaa04ce3c3f9c0976257410e90e21208a"
dependencies = [
"base64",
"base64 0.22.1",
"bitflags 2.13.2",
"btoi",
"byteorder",
@@ -3129,7 +3158,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb9bf5222606eb712d3bb30e01bc9420545b00859970897e70c682353a034f2"
dependencies = [
"base64",
"base64 0.22.1",
"cbc",
"cms",
"der",
@@ -3181,11 +3210,11 @@ dependencies = [
[[package]]
name = "pem"
version = "3.0.6"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
dependencies = [
"base64",
"base64 0.23.1",
"serde_core",
]
@@ -4367,9 +4396,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.53.2"
version = "1.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
dependencies = [
"libc",
"mio",
@@ -4548,7 +4577,7 @@ version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"base64 0.22.1",
"log",
"once_cell",
"rustls",
@@ -5237,15 +5266,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -5279,30 +5299,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
@@ -5324,12 +5327,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5342,12 +5339,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5360,24 +5351,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5390,12 +5369,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5408,12 +5381,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5426,12 +5393,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5444,12 +5405,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winit"
version = "0.30.13"
+17 -1
View File
@@ -37,6 +37,7 @@ members = [
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-optifine",
"crates/horizon-polytone",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-deps",
@@ -47,7 +48,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
rust-version = "1.85"
rust-version = "1.95"
# Ports mirror vanilla's Java signatures, index loops and nested generic types
# one to one so they can be checked against the decompiled sources.
@@ -81,6 +82,21 @@ libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
[profile.dev]
debug = "line-tables-only"
# The native mod API is generated by a build script that parses every file of game/ with syn on
# each gameplay edit; unoptimized that took ~6 s of every rebuild, optimized ~1.5 s.
[profile.dev.package.horizon-mod-api-gen]
opt-level = 3
[profile.dev.package.syn]
opt-level = 3
[profile.dev.package.proc-macro2]
opt-level = 3
[profile.release.package.horizon-mod-api-gen]
opt-level = 3
[profile.release.package.syn]
opt-level = 3
[profile.release.package.proc-macro2]
opt-level = 3
[profile.release]
lto = "thin"
codegen-units = 1
+12
View File
@@ -49,6 +49,18 @@ Everything below is in the current release.
<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>
## Performance
**Horizon draws more frames per second than Java Edition, and loads faster, on the same machine, world and path.** `tools/perf_bench.py` runs each game in turn, from a fresh world (seed 1234), waiting for the terrain and then flying it over a village, a forest, an ocean and a mountain. Measured on 2026-10-29/30 on an idle Windows 11 machine (Ryzen 7 9800X3D, RTX 5080), Horizon v0.2.97 as the launcher installs it, 1280x720 in a window, VSync off, three games per figure. Frames per second, mean of the four scenes, with the slowest 1% of frames in brackets:
| Render distance | Horizon v0.2.97 | Vanilla 26.3 | Vanilla + mods |
| --- | --- | --- | --- |
| 8 chunks | 3,289 (1,327) | 2,437 (640) | 2,375 (709) |
| 16 chunks | 2,074 (598) | 1,661 (375) | 1,586 (432) |
| 32 chunks | 1,207 (333) | 1,031 (176) | 879 (264) |
"Vanilla + mods" is Sodium, Lithium, FerriteCore and ImmediatelyFast. Horizon reaches the title screen in 2.9–3.2 s against vanilla's 7.5–7.7 s (the modded client's 10.8–11.1 s), loads a scene's chunks 2.9–3.8 times faster, and playing holds a steady working set of 1.1–1.7 GB against 1.9–2.9 GB. Every scene with its confidence interval and the run's load log: [the 2026-10-29 reports](benchmarks/perf/windows-9800x3d-5080-2026-10-29.md). Method, and how to measure your own machine: [docs/benchmark.md](docs/benchmark.md). [playhorizon.dev/benchmarks](https://playhorizon.dev/benchmarks) puts these runs beside a 2016 laptop, a Mac mini and a gaming desktop.
## 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):
-10
View File
@@ -410,16 +410,6 @@ The panel's later features (bans, worlds, server.properties import, logs and cra
</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.
<details><summary>Technical</summary>
Issue [#1270](https://github.com/horizon-rust/horizon/issues/1270) asks for Ely.by and Yggdrasil-compatible servers with an account selector in the launcher. It builds on the Microsoft sign-in ([docs/microsoft-account.md](docs/microsoft-account.md)).
</details>
#### Join servers on older versions
Join servers that run older Minecraft versions, such as 1.21 or 1.8 PvP servers, from Horizon.
+107 -2
View File
@@ -1,12 +1,56 @@
# Third-party notices
## Code removed for its license
Horizon once shipped three pieces of code under MIT that came from projects whose licenses don't allow that. All
three are gone from main. They are listed here for anyone who holds an older commit, tag or release: in those
versions, that code is not covered by Horizon's MIT license. It stays under its authors' copyright and its original
license.
- **Shader packs written from Iris's source.** `crates/horizon-shaderpack` and the render stages that used it were
written from the source of [Iris](https://github.com/IrisShaders/Iris) (LGPL-3.0, copyright the Iris contributors).
They were added from 15 October 2026 (`b22583d057`), shipped in the game releases `v2026.10.16.02` to `v0.2.46`,
and were removed on 24 October 2026 ([#1561](https://github.com/horizon-rust/horizon/pull/1561)) after a developer
of Iris showed us. Shader packs came back in `crates/horizon-shaders`, first written from a description of the
pack format.
- **Math functions translated from OpenJDK.** In `crates/horizon-core/src/jmath.rs`, `fdlibm::{log, log10, cbrt,
hypot}` were translated from OpenJDK's `java/lang/FdLibm.java` and `round_f32` from its `java/lang/Math.java`
(GPL-2.0 with the Classpath Exception), and `aarch64::{sin, cos}` from HotSpot's `macroAssembler_aarch64_trig.cpp`
(GPL-2.0); copyright Oracle and/or its affiliates, and Cavium for that file. The `fdlibm` and `aarch64` functions
were added on 13 October 2026 (`558d140fca`, first released in `v2026.10.13`). `round_f32` was added on 14 October
2026 as `java_round` in `crates/horizon-client/src/mipmap.rs` (`95db0fce9d`) and moved into `jmath.rs` on 3
November ([#7770](https://github.com/horizon-rust/horizon/pull/7770)). All of them were replaced in
[#11944](https://github.com/horizon-rust/horizon/pull/11944) by code written without OpenJDK's sources, from Sun's
fdlibm C sources, the Java SE API documentation and a description of the behaviour, and checked against a JVM
([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
- **Java's `HashMap` translated from OpenJDK.** `crates/horizon-world/src/java_hash_map.rs`, which reproduces the
iteration order of `java.util.HashMap`, was translated from OpenJDK's `java/util/HashMap.java` (GPL-2.0 with the
Classpath Exception; copyright Oracle and/or its affiliates). It was added on 7 October 2026 (`118e93ea51` and
`194db0dbb4`, first released in `v2026.10.12`). It was replaced in
[#12043](https://github.com/horizon-rust/horizon/pull/12043) by code written without OpenJDK's sources, from a
description of the behaviour, the Java SE API documentation and the textbook red-black tree, and checked against a
JVM ([#6741](https://github.com/horizon-rust/horizon/issues/6741)).
## Catnip / Ponder
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
UV rules of Catnip's `FluidRenderHelper` from Ponder 1.0.82 for Minecraft 1.21.1
(`net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1`). Copyright (c) 2022 The Create Team,
licensed under MIT (`LICENSE_Ponder` in that artifact); the full license heads the Rust file.
The native renderer uses Horizon's own block-sprite arrays, pose and light packing. The
development-only oracle calls the unchanged helper from the player's local artifact. No
original classes, game shaders, textures or prepared resources are distributed here.
## wgpu-hal
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with upstream fixes applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124),
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
[gfx-rs/wgpu#10714](https://github.com/gfx-rs/wgpu/pull/10714)). Its GL backend also reports
the context's sampled textures and samplers per shader stage (usually 32 on desktop GL,
where WebGL2's minimum 16 was reported before), so a pipeline layout the driver accepts is
not refused. It is licensed under MIT or Apache-2.0; both licenses are in that folder
(`LICENSE.MIT`, `LICENSE.APACHE`).
## naga
@@ -212,3 +256,64 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
## Just Enough Items
The Recipes GUI sprites and settings icon under `crates/horizon-client-render/src/jei_recipes` are from
[Just Enough Items 31.17.0.108](https://github.com/mezz/JustEnoughItems/tree/0371fa6fafa14635a356019e4604f5580b26a34f)
by mezz and contributors, under the MIT license in that directory (`LICENSE.txt`).
## Flywheel native model directional shading
The two-light diffuse expression and transformed-normal policy in
`terrain.wgsl`'s native model entries follow Flywheel 1.0.6
(`internal/diffuse.glsl`, `instance/transformed.vert`, `internal/common.vert`).
No Flywheel classes, compiled shaders or original model/texture assets are bundled.
Copyright (c) 2021-2024 Jozufozu
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## fdlibm
`crates/horizon-core/src/jmath/fdlibm.rs` and the `fdlibm_asin`, `fdlibm_atan` and
`fdlibm_atan2` functions in `crates/horizon-core/src/geometry/trig.rs` are Rust ports of
[fdlibm](https://www.netlib.org/fdlibm/) 5.3, Sun's freely distributable math library, whose
results the Java SE API specifies for `StrictMath`. `fdlibm.rs` ports `e_log.c`, `e_log10.c`,
`s_cbrt.c` and `e_hypot.c`, and, with fused multiply-adds, `s_sin.c`, `s_cos.c`, `k_sin.c`,
`k_cos.c` and `e_rem_pio2.c`; `trig.rs` ports `e_asin.c`, `s_atan.c` and `e_atan2.c`. This
notice also heads both files:
```
Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
Developed at SunSoft, a Sun Microsystems, Inc. business.
Permission to use, copy, modify, and distribute this
software is freely granted, provided that this notice
is preserved.
```
## Starling
`crates/horizon-client-render/src/touch/layout.rs` is adapted from the touch layout of
[Starling](https://github.com/starling-client/starling), an Android port of Horizon, at commit
[150a414b09a1](https://github.com/starling-client/starling/tree/150a414b09a18c900489d9462a7fc035dae744b7)
(`android` branch), written mostly by Vasysik. Starling is licensed under the same MIT license as
Horizon (Copyright (c) 2026 Ludovic Marie and the Horizon contributors).
+1 -1
View File
@@ -3,7 +3,7 @@ name = "cargo-horizon-mod"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
rust-version.workspace = true
description = "cargo horizon-mod new/build/run/check: make Horizon content mods (docs/mods.md)"
# `cargo binstall --git https://github.com/horizon-rust/horizon cargo-horizon-mod` (#2120) reads this: the archive
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-anticheat"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.85"
rust-version.workspace = true
description = "The dedicated server's anticheat: its [anticheat] settings, violation levels and the checks' rules, with no game state (docs/anticheat.md)"
[dependencies]
@@ -34,7 +34,10 @@ pub fn element(v: &Element) -> Value {
.collect();
json!({"from":bits(v.from),"to":bits(v.to),"faces":faces,"rotation":v.rotation.as_ref().map(rotation),"shade":v.shade_direction_override.map(Direction::name),"light":v.light_emission})
}
#[allow(dead_code)] // Shared by fixtures which normalize different model stages.
#[allow(
dead_code,
reason = "shared by fixtures which normalize different model stages"
)]
pub fn model(v: &CuboidModel) -> Value {
let textures: BTreeMap<_, _> = v
.textures
+10 -2
View File
@@ -34,6 +34,8 @@ pub struct ModelQuad {
/// NeoForge's `render_type` names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderType {
/// A declared render type unknown to this baker; distinct from an absent default.
Unsupported,
Solid,
Cutout,
CutoutMipped,
@@ -234,7 +236,10 @@ fn bake_into(
skip_flat: bool,
output: &mut Vec<ModelQuad>,
) -> Result<()> {
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
let render_type = model
.render_type
.as_deref()
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
for element in model.elements() {
let element = element
.as_ref()
@@ -292,7 +297,10 @@ fn bake_obj(
transform: BlockModelTransform,
output: &mut Vec<ModelQuad>,
) -> Result<()> {
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
let render_type = model
.render_type
.as_deref()
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
for face in obj.faces.iter().filter(|f| !hidden.contains(&f.object)) {
let slot = match obj.materials.get(&face.material) {
Some(MtlTexture::Sprite(sprite)) => {
+9 -9
View File
@@ -179,19 +179,19 @@ impl Condition {
let fields = object(raw)?;
if fields.len() == 1 {
for name in ["OR", "AND"] {
if let Some(raw) = fields.get(name) {
if let Ok(values) = array(raw).and_then(|values| {
if let Some(raw) = fields.get(name)
&& let Ok(values) = array(raw).and_then(|values| {
values
.into_iter()
.map(Self::decode)
.collect::<Result<Vec<_>>>()
}) {
return Ok(if name == "OR" {
Self::Or(values)
} else {
Self::And(values)
});
}
})
{
return Ok(if name == "OR" {
Self::Or(values)
} else {
Self::And(values)
});
}
}
}
+55 -14
View File
@@ -917,6 +917,46 @@ impl NumericProperty {
/// (`None` removes a prototype component).
pub type Components = BTreeMap<String, Value>;
/// A stack's components read in place: its patch first, then its item's defaults. Reading one copies
/// nothing, where [`ItemModels::components`] clones the defaults for a stack with a patch (an enchanted
/// sword's are a few dozen JSON values, on every draw of it).
#[derive(Clone, Copy)]
pub struct ComponentsView<'a> {
prototype: &'a Components,
patch: &'a BTreeMap<String, Option<Value>>,
}
impl<'a> ComponentsView<'a> {
pub fn get(&self, key: &str) -> Option<&'a Value> {
match self.patch.get(key) {
Some(patched) => patched.as_ref(),
None => self.prototype.get(key),
}
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
/// Components with no patch: `components` as they are.
pub fn of(components: &'a Components) -> Self {
static NO_PATCH: BTreeMap<String, Option<Value>> = BTreeMap::new();
Self {
prototype: components,
patch: &NO_PATCH,
}
}
/// The merged map, for what needs one whole.
pub fn to_map(&self) -> Components {
let mut out = self.prototype.clone();
for (key, value) in self.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
}
out
}
}
/// An item stack as the resolver reads it.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StackView {
@@ -1087,14 +1127,15 @@ impl ItemModels {
if stack.patch.is_empty() {
return std::borrow::Cow::Borrowed(prototype);
}
let mut out = prototype.clone();
for (key, value) in &stack.patch {
match value {
Some(v) => out.insert(key.clone(), v.clone()),
None => out.remove(key),
};
std::borrow::Cow::Owned(self.components_view(stack).to_map())
}
/// The stack's components, read in place (see [`ComponentsView`]).
pub fn components_view<'a>(&'a self, stack: &'a StackView) -> ComponentsView<'a> {
static NONE: Components = BTreeMap::new();
ComponentsView {
prototype: self.prototypes.get(&stack.item).unwrap_or(&NONE),
patch: &stack.patch,
}
std::borrow::Cow::Owned(out)
}
/// `ItemModelResolver.updateForTopItem`.
pub fn resolve(&self, stack: &StackView, ctx: &Context, env: &dyn ItemEnv) -> ResolvedItem {
@@ -1112,7 +1153,7 @@ impl ItemModels {
ctx: &Context,
env: &dyn ItemEnv,
) {
let components = self.components(stack);
let components = self.components_view(stack);
let Some(Value::String(model_id)) = components.get("minecraft:item_model") else {
return;
};
@@ -1122,7 +1163,7 @@ impl ItemModels {
let mut state = Resolve {
models: self,
stack,
components: &components,
components,
ctx,
env,
};
@@ -1140,7 +1181,7 @@ impl ItemModels {
self.properties(stack).2
}
fn properties(&self, stack: &StackView) -> (bool, bool, f32) {
match self.components(stack).get("minecraft:item_model") {
match self.components_view(stack).get("minecraft:item_model") {
Some(Value::String(id)) => identifier(id)
.ok()
.and_then(|id| self.definitions.get(&id))
@@ -1159,7 +1200,7 @@ impl ItemModels {
struct Resolve<'a> {
models: &'a ItemModels,
stack: &'a StackView,
components: &'a Components,
components: ComponentsView<'a>,
ctx: &'a Context<'a>,
env: &'a dyn ItemEnv,
}
@@ -1455,9 +1496,9 @@ impl Resolve<'_> {
fn numeric(&self, property: &NumericProperty) -> f32 {
let owner = self.ctx.owner;
match property {
NumericProperty::BundleFullness => {
self.env.bundle_fullness(self.stack, self.components)
}
NumericProperty::BundleFullness => self
.env
.bundle_fullness(self.stack, &self.components.to_map()),
NumericProperty::Compass {
wobble,
target,
@@ -63,7 +63,7 @@ pub(super) fn decode(m: &Map<String, Value>) -> Result<Value> {
/// with a matching level. An empty predicate list still needs the component.
pub(super) fn matches(
predicate: &Value,
components: &Components,
components: ComponentsView<'_>,
env: &dyn ItemEnv,
) -> Option<bool> {
let (id, expected) = predicate.as_object()?.iter().next()?;
@@ -143,13 +143,17 @@ mod tests {
json!({"minecraft:density":0}),
)]);
assert_eq!(
matches(&json!({"minecraft:enchantments":[{}]}), &components, &NoEnv),
matches(
&json!({"minecraft:enchantments":[{}]}),
ComponentsView::of(&components),
&NoEnv
),
Some(true)
);
assert_eq!(
matches(
&json!({"minecraft:enchantments":[{"enchantments":"minecraft:density"}]}),
&components,
ComponentsView::of(&components),
&NoEnv
),
Some(false)
@@ -166,12 +170,21 @@ mod tests {
"minecraft:enchantments".into(),
json!({"minecraft:density":3}),
)]);
assert_eq!(matches(&predicate, &components, &Tags), Some(true));
assert_eq!(matches(&predicate, &components, &NoEnv), Some(false));
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(true)
);
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &NoEnv),
Some(false)
);
components.insert(
"minecraft:enchantments".into(),
json!({"minecraft:density":2}),
);
assert_eq!(matches(&predicate, &components, &Tags), Some(false));
assert_eq!(
matches(&predicate, ComponentsView::of(&components), &Tags),
Some(false)
);
}
}
@@ -1,4 +1,5 @@
use super::*;
use serde_json::json;
#[test]
fn identifier_adds_default_namespace() {
@@ -111,3 +112,49 @@ fn custom_condition_validates_its_name_and_both_branches() {
assert!(matches!(node("example:charged", empty).unwrap(),
ItemModelNode::Condition { property: ConditionProperty::Custom(name), .. } if name == "example:charged"));
}
#[test]
fn a_patched_stack_reads_its_patch_over_its_items_defaults() {
let mut models = ItemModels::default();
models.prototypes.insert(
"minecraft:diamond_sword".into(),
Components::from_iter([
(
"minecraft:item_model".into(),
json!("minecraft:diamond_sword"),
),
("minecraft:max_damage".into(), json!(1561)),
("minecraft:rarity".into(), json!("common")),
]),
);
let mut stack = StackView::new("minecraft:diamond_sword", 1);
stack.patch.insert(
"minecraft:enchantments".into(),
Some(json!({"minecraft:sharpness": 5})),
);
stack
.patch
.insert("minecraft:rarity".into(), Some(json!("rare")));
stack.patch.insert("minecraft:max_damage".into(), None);
let view = models.components_view(&stack);
assert_eq!(view.get("minecraft:rarity"), Some(&json!("rare")));
assert_eq!(view.get("minecraft:max_damage"), None, "a removed default");
assert_eq!(
view.get("minecraft:item_model"),
Some(&json!("minecraft:diamond_sword"))
);
assert!(view.contains_key("minecraft:enchantments"));
assert!(!view.contains_key("minecraft:custom_name"));
// Every key reads as the merged map has it.
let merged = models.components(&stack);
for key in [
"minecraft:rarity",
"minecraft:max_damage",
"minecraft:item_model",
"minecraft:enchantments",
"minecraft:custom_name",
] {
assert_eq!(view.get(key), merged.get(key), "{key}");
}
assert_eq!(view.to_map(), *merged);
}
+2 -2
View File
@@ -290,12 +290,12 @@ pub fn normalize_quaternion(q: [f32; 4]) -> [f32; 4] {
[x * inv, y * inv, z * inv, w * inv]
}
/// JOML `Math.sin(float)` without fast math.
fn joml_sin(rad: f32) -> f32 {
pub fn joml_sin(rad: f32) -> f32 {
f64::from(rad).sin() as f32
}
/// JOML `Math.cosFromSin(float, float)` without fast math: the magnitude from
/// the sine, the sign from the angle's quadrant (exact zeros at right angles).
fn cos_from_sin(sin: f32, angle: f32) -> f32 {
pub fn cos_from_sin(sin: f32, angle: f32) -> f32 {
let cos = (1.0 - sin * sin).sqrt();
let tau = (std::f64::consts::PI * 2.0) as f32;
let a = angle + std::f32::consts::FRAC_PI_2;
+17
View File
@@ -777,6 +777,23 @@ mod tests {
);
}
#[test]
fn an_unknown_declared_render_type_remains_distinct_from_absent() {
let json = LIGHTS.replace("minecraft:cutout", "fixture:custom_material");
let library = library(&[("block/custom", &json)]);
let model = library.resolve("block/custom").unwrap();
let quads = crate::bake::bake_block_model(
&model,
crate::bake::BlockModelTransform::new([0; 3], false),
)
.unwrap();
assert!(!quads.is_empty());
assert!(
quads
.iter()
.all(|q| q.render_type == Some(crate::bake::RenderType::Unsupported))
);
}
#[test]
fn render_type_comes_from_the_leaf_most_model_and_loses_its_prefix() {
let library = library(&[
("block/base", r#"{"render_type":"translucent"}"#),
+1
View File
@@ -2,6 +2,7 @@
name = "horizon-bench"
version = "0.1.0"
edition = "2021"
rust-version.workspace = true
license = "MIT"
description = "The benchmark report format (docs/benchmark-report.md), shared by the game, the launcher and the upload"
+6 -6
View File
@@ -694,7 +694,7 @@ fn costly(
Behaviour::FloodUse => {
// A block within reach: place stone there, or break what is there.
let at = [0, 1, 2].map(|k| block[k] + (random.unit() * 7.0) as i32 - 3);
if i % 2 == 0 {
if i.is_multiple_of(2) {
play::Serverbound::UseItemOn(play::UseItemOn {
hand: 0,
pos: at,
@@ -723,7 +723,7 @@ fn costly(
carried: None,
}),
Behaviour::FloodSuggest => {
let text = if i % 2 == 0 {
let text = if i.is_multiple_of(2) {
"/"
} else {
LONG_COMMAND.as_str()
@@ -1555,10 +1555,10 @@ fn main() {
});
if o.report.as_deref() == Some("-") {
println!("{summary:#}");
} else if let Some(path) = &o.report {
if let Err(e) = std::fs::write(path, format!("{summary:#}\n")) {
eprintln!("horizon-bots: {path}: {e}");
}
} else if let Some(path) = &o.report
&& let Err(e) = std::fs::write(path, format!("{summary:#}\n"))
{
eprintln!("horizon-bots: {path}: {e}");
}
let errors: Vec<String> = bots
.iter()
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-chat-filter"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.88"
rust-version.workspace = true
description = "The dedicated server's chat filter: its [chat_filter] settings, the slur list's matcher and the mutes, with no game state (docs/chat-filter.md)"
[dependencies]
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-client-core"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "The client's lowest layer: chat text and translations, SNBT and NBT paths, the scoreboard, boss bars, game rules and the registry and particle tables commands name"
[dependencies]
@@ -217,9 +217,10 @@ pub fn stack(tag: &Tag) -> Option<Tag> {
Some(Tag::compound(out))
}
/// `ItemStackTemplate.CODEC`: an item id or `{id, count?, components?}`.
/// Encoding uses the primary record form, including for a bare item id.
fn template(tag: &Tag) -> Option<Tag> {
match tag {
Tag::String(_) => Some(tag.clone()),
Tag::String(_) => Some(Tag::compound([("id", tag.clone())])),
Tag::Compound(_) => {
let mut out = vec![("id", string(&text(tag.get("id"))?))];
if let Some(count) = int(tag.get("count")).filter(|c| *c != 1) {
@@ -1011,3 +1012,69 @@ mod tests {
assert_eq!(items[1].get("count"), Some(&Tag::Int(5)));
}
}
#[cfg(test)]
mod pot_template_tests {
use super::*;
use horizon_world::nbt::parse_snbt;
#[test]
fn decorated_pot_string_template_load_save_uses_item_compound() {
let parse = |s: &str| parse_snbt(s).expect("valid input NBT").0;
let input = |front: &str| {
let mut tag = parse(
r#"{id:"minecraft:decorated_pot",x:4,y:200,z:4,sherds:{back:{id:"minecraft:brick"},right:{id:"minecraft:archer_pottery_sherd"}},item:{id:"minecraft:wheat",count:3,components:{"minecraft:custom_name":"H290 item"}}}"#,
);
let Tag::Compound(root) = &mut tag else {
unreachable!()
};
let Some(Tag::Compound(sides)) = root.get_mut(&"sherds".into()) else {
unreachable!()
};
sides.insert("front".into(), parse(front));
tag
};
let other_fields = |loaded: &Tag, saved: &Tag| {
assert_eq!(saved.get("id"), loaded.get("id"));
for key in ["x", "y", "z", "item"] {
assert_eq!(saved.get(key), loaded.get(key), "{key}");
}
let loaded_sides = loaded.get("sherds").unwrap();
let saved_sides = saved.get("sherds").unwrap();
for side in ["back", "right"] {
assert_eq!(saved_sides.get(side), loaded_sides.get(side), "{side}");
}
assert_eq!(saved_sides.get("left"), None);
};
let plain = r#"{id:"minecraft:angler_pottery_sherd"}"#;
let defaults = r#"{id:"minecraft:angler_pottery_sherd",count:1,components:{}}"#;
let rich = r#"{id:"minecraft:angler_pottery_sherd",count:3,components:{"minecraft:custom_data":{h290:1},"minecraft:custom_name":"H290 sherd"}}"#;
// Actual canonical load/save entry, not the private template helper.
for (front, expected) in [(plain, plain), (defaults, plain), (rich, rich)] {
let loaded = input(front);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(expected))
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
assert_eq!(loaded, original, "read-only canonicalization");
}
println!(
"H290 actual canonical compound/default/count/components, other sides/item/name and idempotency controls admitted"
);
let loaded = input(r#""minecraft:angler_pottery_sherd""#);
let original = loaded.clone();
let saved = canonical(&loaded).expect("actual decorated-pot codec");
other_fields(&loaded, &saved);
assert_eq!(loaded, original, "read-only canonicalization");
assert_eq!(
saved.get("sherds").unwrap().get("front"),
Some(&parse(plain)),
"string template must save the primary item compound",
);
assert_eq!(canonical(&saved).as_ref(), Some(&saved));
}
}
+41 -3
View File
@@ -412,10 +412,11 @@ pub fn to_text_with(
.collect()
})
.unwrap_or_default();
let translated = Text::translate(key, &args);
// TranslatableContents.decompose: Language.getOrDefault(key, fallback), so the
// fallback is the template itself, never looked up as another key.
match o.get("fallback").and_then(Value::as_str) {
Some(fallback) if translated.plain() == key => Text::translate(fallback, &args),
_ => translated,
Some(fallback) => Text::translate_with_fallback(key, fallback, &args),
None => Text::translate(key, &args),
}
} else if let Some(sel) = o.get("selector").and_then(Value::as_str) {
resolve(sel)
@@ -513,6 +514,43 @@ pub fn name_text(tag: &Tag) -> Text {
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::{self, Language, ResourceSource};
struct Mem(std::collections::HashMap<String, Vec<u8>>);
impl ResourceSource for Mem {
fn read(&self, path: &str) -> Option<Vec<u8>> {
self.0.get(path).cloned()
}
}
/// TranslatableContents.decompose: Language.getOrDefault(key, fallback). A missing key
/// shows the fallback as written, even when the fallback is another key the language has.
#[test]
fn a_translatable_fallback_stays_literal() {
let pack = Mem([(
"assets/minecraft/lang/en_us.json".to_owned(),
br#"{"real.key": "Translated", "greet": "Hello %s"}"#.to_vec(),
)]
.into());
let ((), _) = lang::scoped(Language::load(&[&pack], "en_us"), || {
let parse = |v: Value| to_text(&v, &mut |_| Text::empty()).plain();
let missing = serde_json::json!({"translate": "missing.key", "fallback": "real.key"});
assert_eq!(parse(missing), "real.key");
// The fallback is a template: its arguments are filled in.
let args = serde_json::json!({"translate": "missing.key", "fallback": "Hi %s", "with": ["Steve"]});
assert_eq!(parse(args), "Hi Steve");
// A key the language has wins over its fallback.
let found =
serde_json::json!({"translate": "greet", "fallback": "real.key", "with": ["Alex"]});
assert_eq!(parse(found), "Hello Alex");
// A component built with the constructor keeps its meaning through its JSON.
let built = Text::translate_with_fallback("missing.key", "real.key", &[]);
let again = to_text(&built.component(), &mut |_| Text::empty());
assert_eq!(again.plain(), built.plain());
assert_eq!(again.component(), built.component());
});
}
/// SnbtGrammar: a compound or list may end with one comma; two in a row
/// stay an error.
#[test]
+18
View File
@@ -46,6 +46,21 @@ thread_local! {
}
/// The current language's `bidirectional`, read for every drawn string.
static BIDIRECTIONAL: AtomicBool = AtomicBool::new(false);
/// Counts the languages set (`set`): another count is another language, so text looked up
/// before it may read otherwise.
static GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
thread_local! {
/// Counts the scopes this thread entered and left (`scoped`), which no other thread sees.
static SCOPE_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// Which language text looks up in now, on this thread, as a count that changes whenever that
/// can: both counts only grow, so their sum changes when either does.
pub fn generation() -> u64 {
GENERATION.load(Ordering::Relaxed) + SCOPE_GENERATION.get()
}
fn scope_changed() {
SCOPE_GENERATION.set(SCOPE_GENERATION.get() + 1);
}
/// A source of resource files: the prepared root plus packs, highest priority last.
pub trait ResourceSource {
@@ -524,6 +539,7 @@ pub fn available(sources: &[&dyn ResourceSource]) -> Vec<LanguageInfo> {
pub fn set(language: Language) {
BIDIRECTIONAL.store(language.bidirectional, Ordering::Relaxed);
*CURRENT.write().unwrap_or_else(|e| e.into_inner()) = Some(Arc::new(language));
GENERATION.fetch_add(1, Ordering::Relaxed);
}
pub fn current() -> Option<Arc<Language>> {
if let Some(language) = SCOPE.with(|s| s.borrow().as_ref().map(|s| s.language.clone())) {
@@ -543,6 +559,7 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
impl Drop for Restore {
fn drop(&mut self) {
SCOPE.with(|s| *s.borrow_mut() = self.0.take());
scope_changed();
}
}
let outer = SCOPE.with(|s| {
@@ -552,6 +569,7 @@ pub fn scoped<R>(language: Language, f: impl FnOnce() -> R) -> (R, Vec<String>)
})
});
let _restore = Restore(outer);
scope_changed();
let result = f();
let missed = SCOPE.with(|s| {
s.borrow_mut()
+1 -20
View File
@@ -1,6 +1,7 @@
//! NBT paths (`NbtPathArgument`): `foo.bar[0]`, `[]`, `list[{id:1}]`,
//! `{root:match}`, with get, set, insert and remove on a tag tree, and the
//! partial NBT comparison (`NbtUtils.compareNbt`) selectors and conditions use.
pub use horizon_world::nbt::compare_nbt;
use horizon_world::nbt::{Compound, JavaString, Tag, parse_snbt};
/// An error carrying the translation key and its arguments.
@@ -47,26 +48,6 @@ fn get_child<'a>(c: &'a Compound, name: &str) -> Option<&'a Tag> {
c.get(&key(name))
}
/// `NbtUtils.compareNbt(expected, actual, true)`.
pub fn compare_nbt(expected: &Tag, actual: Option<&Tag>) -> bool {
let Some(actual) = actual else { return false };
if std::mem::discriminant(expected) != std::mem::discriminant(actual) {
return false;
}
match (expected, actual) {
(Tag::Compound(e), Tag::Compound(a)) => {
a.len() >= e.len() && e.iter().all(|(k, v)| compare_nbt(v, a.get(k)))
}
(Tag::List(e), Tag::List(a)) => {
if e.is_empty() {
return a.is_empty();
}
a.len() >= e.len() && e.iter().all(|x| a.iter().any(|y| compare_nbt(x, Some(y))))
}
_ => expected == actual,
}
}
// ---- list-like tags ----------------------------------------------------------
/// The elements of a list or array tag.
@@ -89,6 +89,31 @@ pub struct Registries {
pub trigger_objectives: Vec<String>,
/// Objectives with the `trigger` criterion -> holder -> whether its score is locked.
pub trigger_scores: BTreeMap<String, BTreeMap<String, bool>>,
/// The `ConfiguredFeature` codec for an inline `minecraft:worldgen/feature`
/// value (`ResourceOrIdArgument`), where the source has the server's live
/// features; None leaves the decode to when the command runs.
pub inline_feature: Option<InlineFeatureCheck>,
}
/// A codec check of an inline value: its error message when it does not decode.
#[derive(Clone)]
pub struct InlineFeatureCheck(
std::sync::Arc<dyn Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync>,
);
impl InlineFeatureCheck {
pub fn new(
check: impl Fn(&horizon_world::nbt::Tag) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
Self(std::sync::Arc::new(check))
}
pub fn check(&self, value: &horizon_world::nbt::Tag) -> Result<(), String> {
(self.0)(value)
}
}
impl std::fmt::Debug for InlineFeatureCheck {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("InlineFeatureCheck(..)")
}
}
/// The ids and tag ids of one registry.
#[derive(Clone, Debug, Default)]
@@ -275,6 +300,25 @@ impl Registries {
.clone_from(&e.advancement_criteria);
}
}
/// Add the damage catalog's types ([`Self::damage_types`]: content mods'
/// and the world's packs' with vanilla's) to the commands'
/// `minecraft:damage_type` ids, which the export lists for vanilla alone.
/// `ResourceArgument` looks a type up in the server's registries, so
/// `/damage` takes every type the world has. The export's tags stay. The
/// ids are replaced, not extended: `by_registry` outlives a world, and a
/// pack's type must not stay accepted in the next world opened.
pub fn sync_damage_type_ids(&mut self) {
if self.damage_types.is_empty() {
return;
}
let mut ids = self.damage_types.clone();
ids.sort();
ids.dedup();
self.by_registry
.entry("minecraft:damage_type".into())
.or_default()
.ids = ids;
}
/// The element ids of a registry (sorted).
pub fn ids(&self, registry: &str) -> &[String] {
if let Some(r) = self.by_registry.get(registry) {
@@ -312,3 +356,49 @@ impl Registries {
}
}
}
#[cfg(test)]
mod tests {
use super::Registries;
/// A content mod's damage type joins vanilla's in the commands' ids, the
/// export's tags stay, and a second world open lists each type once.
#[test]
fn damage_type_ids_take_the_catalogs_types() {
let export = Registries::from_export(&serde_json::json!({
"by_registry": {"minecraft:damage_type": {
"ids": ["minecraft:fall", "minecraft:magic"],
"tags": ["minecraft:is_fall"],
}},
}));
let mut reg = Registries {
damage_types: vec![
"minecraft:magic".into(),
"example:test".into(),
"minecraft:fall".into(),
],
..Registries::default()
};
reg.fill_from_exported(&export);
assert_eq!(
reg.ids("minecraft:damage_type"),
["minecraft:fall", "minecraft:magic"],
"the export alone lists vanilla's types"
);
for _ in 0..2 {
reg.sync_damage_type_ids();
assert_eq!(
reg.ids("minecraft:damage_type"),
["example:test", "minecraft:fall", "minecraft:magic"]
);
assert_eq!(reg.tags("minecraft:damage_type"), ["minecraft:is_fall"]);
}
// The next world, without the pack: its type is no longer accepted.
reg.damage_types = vec!["minecraft:fall".into(), "minecraft:magic".into()];
reg.sync_damage_type_ids();
assert_eq!(
reg.ids("minecraft:damage_type"),
["minecraft:fall", "minecraft:magic"]
);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ name = "horizon-client-input"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "The client's input layer: key bindings and their names, mouse, controllers, clipboard and window options"
[dependencies]
+69 -3
View File
@@ -490,6 +490,37 @@ impl GlyphStyle {
}
}
/// How much the gameplay guide says.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GuideDetail {
#[default]
Full,
/// Only what can be done to what's in front of the player or under the crosshair.
Basic,
Off,
}
impl GuideDetail {
pub const ALL: [GuideDetail; 3] = [GuideDetail::Full, GuideDetail::Basic, GuideDetail::Off];
/// The name in `horizon-controller.txt`.
pub fn name(self) -> &'static str {
match self {
GuideDetail::Full => "full",
GuideDetail::Basic => "basic",
GuideDetail::Off => "off",
}
}
/// Reads a name, or the `true` and `false` this option first saved. Anything else is full.
pub fn parse(value: &str) -> Self {
match value {
"basic" => GuideDetail::Basic,
"off" | "false" => GuideDetail::Off,
_ => GuideDetail::Full,
}
}
}
/// The on-screen keyboard A opens on a text field in focus mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TextKeyboard {
@@ -537,6 +568,8 @@ pub fn steam_keyboard(choice: TextKeyboard) -> bool {
pub struct PadSettings {
pub enabled: bool,
pub vibration: bool,
/// The pad keeps driving the game while its window has no focus.
pub play_in_background: bool,
/// A DualSense's triggers push back (Horizon's; docs/controllers.md).
pub adaptive_triggers: bool,
pub lightbar: Lightbar,
@@ -550,12 +583,18 @@ pub struct PadSettings {
/// The sneak button flips sneaking (Bedrock's default) instead of
/// holding it, unless vanilla's toggle crouch is on already.
pub toggle_sneak: bool,
/// LT + A on a recipe crafts on every server, not only in single player and on LAN worlds
/// found in the server list. Off by default: a server's rules may count a repeated craft as a
/// macro.
pub craft_on_servers: bool,
pub glyphs: GlyphStyle,
/// A Nintendo pad's face buttons go by their letters (A confirms and jumps, as on the
/// Switch's own menus and keyboard) instead of their places (B, at the bottom, does): on
/// by default on the Switch only.
pub nintendo_letters: bool,
pub keyboard: TextKeyboard,
/// The gameplay guide: what the buttons do now, in the HUD's corners while the pad plays.
pub guide_detail: GuideDetail,
/// Menu pointer multiplier, scroll lines/second, and repeat times in seconds.
pub pointer_speed: f32,
pub scroll_speed: f32,
@@ -569,6 +608,7 @@ impl Default for PadSettings {
Self {
enabled: true,
vibration: true,
play_in_background: false,
adaptive_triggers: true,
lightbar: Lightbar::Health,
look_sensitivity: 0.5,
@@ -578,9 +618,11 @@ impl Default for PadSettings {
look_deadzone: 0.12,
trigger_threshold: 0.4,
toggle_sneak: true,
craft_on_servers: false,
glyphs: GlyphStyle::Auto,
nintendo_letters: cfg!(target_os = "horizon"),
keyboard: TextKeyboard::Auto,
guide_detail: GuideDetail::Full,
pointer_speed: 1.0,
scroll_speed: 12.0,
repeat_delay: 0.35,
@@ -635,6 +677,7 @@ impl PadSettings {
match key.trim() {
"enabled" => s.enabled = flag(s.enabled),
"vibration" => s.vibration = flag(s.vibration),
"playInBackground" => s.play_in_background = flag(s.play_in_background),
"adaptiveTriggers" => s.adaptive_triggers = flag(s.adaptive_triggers),
"lightbar" => s.lightbar = Lightbar::parse(value),
"lookSensitivity" => s.look_sensitivity = unit(s.look_sensitivity),
@@ -646,9 +689,11 @@ impl PadSettings {
s.trigger_threshold = unit(s.trigger_threshold).clamp(0.1, 0.9)
}
"toggleSneak" => s.toggle_sneak = flag(s.toggle_sneak),
"craftOnServers" => s.craft_on_servers = flag(s.craft_on_servers),
"glyphs" => s.glyphs = GlyphStyle::parse(value),
"nintendoLetters" => s.nintendo_letters = flag(s.nintendo_letters),
"keyboard" => s.keyboard = TextKeyboard::parse(value),
"gameplayGuide" => s.guide_detail = GuideDetail::parse(value),
"pointerSpeed" => s.pointer_speed = bounded(s.pointer_speed, 0.25, 3.0),
"scrollSpeed" => s.scroll_speed = bounded(s.scroll_speed, 1.0, 36.0),
"repeatDelay" => s.repeat_delay = bounded(s.repeat_delay, 0.1, 1.0),
@@ -706,7 +751,9 @@ impl PadSettings {
self.keyboard.name(),
);
text.push_str(&format!(
"pointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
"craftOnServers:{}\ngameplayGuide:{}\npointerSpeed:{}\nscrollSpeed:{}\nrepeatDelay:{}\nrepeatInterval:{}\n",
bool_text(self.craft_on_servers),
self.guide_detail.name(),
self.pointer_speed, self.scroll_speed, self.repeat_delay, self.repeat_interval,
));
for (action, button) in &self.bindings {
@@ -715,6 +762,10 @@ impl PadSettings {
button.map_or("none", Button::name)
));
}
text.push_str(&format!(
"playInBackground:{}\n",
bool_text(self.play_in_background)
));
text.push_str("bindLayout:1\n");
text
}
@@ -786,7 +837,8 @@ pub fn stick_axes(
{
// Default map: RightStickX is the stick's Y, LeftZ its X; RightStickY
// is the right trigger and must not drive look.
(left, [left_z, right[0]])
// gilrs turns over only the axes it calls Y, and this Y is on one it doesn't.
(left, [left_z, -right[0]])
} else {
(left, right)
}
@@ -1752,6 +1804,15 @@ bind_key.hotbar.2:north
}
}
#[test]
fn playing_in_the_background_is_off_until_saved_on() {
assert!(!PadSettings::parse("vibration:false\n").play_in_background);
let mut on = PadSettings::default();
assert!(!on.play_in_background);
on.play_in_background = true;
assert!(PadSettings::parse(&on.serialize()).play_in_background);
}
#[test]
fn a_file_from_before_these_defaults_keeps_its_buttons() {
// Saved settings that never bound D-pad down: it stays on the advancements.
@@ -1793,6 +1854,7 @@ bind_key.hotbar.2:north
s.look_sensitivity = 0.75;
s.glyphs = GlyphStyle::PlayStation;
s.nintendo_letters = !s.nintendo_letters;
s.guide_detail = GuideDetail::Basic;
let text = s.serialize();
assert!(text.contains(&format!("nintendoLetters:{}\n", s.nintendo_letters)));
assert!(text.contains("bind_key.jump:east\n"));
@@ -1820,7 +1882,11 @@ bind_key.hotbar.2:north
false,
);
assert_eq!(left, [0.2, 0.3]);
assert_eq!(right, [-0.5, 0.7], "X from LeftZ, Y from RightStickX");
assert_eq!(
right,
[-0.5, -0.7],
"X from LeftZ, Y from RightStickX, turned over"
);
let kept = stick_axes(
[0.2, 0.3],
[0.7, -0.9],
@@ -0,0 +1,962 @@
//! The gameplay guide: what the buttons do right now, in the corners of the HUD while a pad plays
//! and of an inventory it drives (Horizon's; vanilla has no gamepad input). The game reads the facts
//! from its own hit results and capabilities (`game/pad/guide.rs`); this turns them into rows, and
//! places the rows around what was drawn.
use crate::gamepad::{Button, GuideDetail, PadSettings, Rect};
/// What a use does, by what takes it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verb {
Use,
Open,
Ride,
Trade,
Feed,
Place,
Eat,
Drink,
Shoot,
Load,
Throw,
Raise,
Equip,
Fish,
Zoom,
Boost,
}
impl Verb {
fn caption(self) -> &'static str {
match self {
Verb::Use => "horizon.controller.guide.use",
Verb::Open => "horizon.controller.guide.open",
Verb::Ride => "horizon.controller.guide.ride",
Verb::Trade => "horizon.controller.guide.trade",
Verb::Feed => "horizon.controller.guide.feed",
Verb::Place => "horizon.controller.guide.place",
Verb::Eat => "horizon.controller.guide.eat",
Verb::Drink => "horizon.controller.guide.drink",
Verb::Shoot => "horizon.controller.guide.shoot",
Verb::Load => "horizon.controller.guide.load",
Verb::Throw => "horizon.controller.guide.throw",
Verb::Raise => "horizon.controller.guide.raise",
Verb::Equip => "horizon.controller.guide.equip",
Verb::Fish => "horizon.controller.guide.fish",
Verb::Zoom => "horizon.controller.guide.zoom",
Verb::Boost => "horizon.controller.guide.boost",
}
}
}
/// `Minecraft.hitResult`, with what each button would do to it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Target {
#[default]
None,
Block {
breakable: bool,
/// The block takes the use before the held item gets its turn.
takes: Option<Verb>,
},
Entity {
attackable: bool,
takes: Option<Verb>,
},
}
/// An item in use, by what letting go of it does.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Active {
Shoot,
Throw,
Stop,
}
/// What the guide knows of the player this moment.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Facts {
pub spectator: bool,
pub target: Target,
/// What `Item.use` does on the target or in the air: the main hand's, else the off hand's.
pub item_use: Option<Verb>,
/// Pick Block finds something it can select or, in creative, add.
pub can_pick: bool,
pub using: Option<Active>,
/// `LocalPlayer.isHandsBusy`: paddling a boat.
pub hands_busy: bool,
pub riding: bool,
/// The vehicle jumps when told (a saddled horse).
pub vehicle_jumps: bool,
pub on_ground: bool,
pub in_water: bool,
pub flying: bool,
pub may_fly: bool,
pub gliding: bool,
/// In the air with a glider on: a jump would start gliding.
pub can_glide: bool,
/// Sneaking that the button latched, so the next press stands up.
pub sneak_latched: bool,
pub can_sprint: bool,
pub main_hand_item: bool,
pub off_hand_item: bool,
}
/// One button's line: the button and the translation key of what it does.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Row {
/// Shown before `button`: held with it, or its other half (LB and RB for pages).
pub with: Option<Button>,
pub button: Button,
pub caption: &'static str,
}
impl Row {
fn new(button: Button, caption: &'static str) -> Self {
Row {
with: None,
button,
caption,
}
}
fn pair(with: Button, button: Button, caption: &'static str) -> Self {
Row {
with: Some(with),
button,
caption,
}
}
}
/// The two columns, most useful row first.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Guide {
/// Top left: movement and the player's own actions.
pub left: Vec<Row>,
/// Top right: break, use and pick block.
pub right: Vec<Row>,
}
impl Guide {
/// The rows this detail keeps. Basic is the right column: what the buttons do to what's in
/// front of the player, or the plain face buttons in an inventory.
pub fn at_detail(self, detail: GuideDetail) -> Self {
match detail {
GuideDetail::Full => self,
GuideDetail::Basic => Guide {
left: Vec::new(),
right: self.right,
},
GuideDetail::Off => Guide {
left: Vec::new(),
right: Vec::new(),
},
}
}
}
/// The rows for these facts on these bindings. An unbound action has no row, and neither has one
/// on Start, which is always pause.
pub fn resolve(facts: &Facts, settings: &PadSettings) -> Guide {
let row = |action: &str, caption: Option<&'static str>| {
let button = settings
.binding(action)
.filter(|button| *button != Button::Start)?;
Some(Row::new(button, caption?))
};
let player = !facts.spectator;
let any_item = facts.main_hand_item || facts.off_hand_item;
Guide {
left: [
row("key.jump", jump_caption(facts)),
row("key.sneak", sneak_caption(facts)),
row("key.sprint", facts.can_sprint.then_some("key.sprint")),
row("key.inventory", player.then_some("key.inventory")),
row(
"key.drop",
(player && facts.main_hand_item).then_some("horizon.controller.guide.drop"),
),
row(
"key.swapOffhand",
(player && any_item).then_some("horizon.controller.guide.swap"),
),
]
.into_iter()
.flatten()
.collect(),
right: [
row("key.attack", attack_caption(facts)),
row("key.use", use_caption(facts)),
row("key.pickItem", pick_caption(facts)),
]
.into_iter()
.flatten()
.collect(),
}
}
fn jump_caption(f: &Facts) -> Option<&'static str> {
if f.riding {
f.vehicle_jumps.then_some("key.jump")
} else if f.flying {
Some("horizon.controller.guide.fly_up")
} else if f.gliding {
None
} else if f.in_water {
Some("horizon.controller.guide.swim_up")
} else if f.on_ground {
Some("key.jump")
} else if f.can_glide {
Some("horizon.controller.guide.glide")
} else {
f.may_fly.then_some("horizon.controller.guide.fly")
}
}
fn sneak_caption(f: &Facts) -> Option<&'static str> {
if f.riding {
Some("horizon.controller.guide.dismount")
} else if f.flying {
Some("horizon.controller.guide.fly_down")
} else if f.gliding {
None
} else if f.in_water && !f.on_ground {
Some("horizon.controller.guide.swim_down")
} else if f.sneak_latched {
Some("horizon.controller.guide.stand")
} else {
Some("key.sneak")
}
}
/// `Minecraft.handleKeybinds` spends attack clicks while an item is in use, and
/// `startAttack` does nothing with busy hands.
fn attack_caption(f: &Facts) -> Option<&'static str> {
if f.using.is_some() || f.hands_busy {
return None;
}
match f.target {
Target::Entity {
attackable: true, ..
} if f.spectator => Some("horizon.controller.guide.spectate"),
Target::Entity {
attackable: true, ..
} => Some("horizon.controller.guide.attack"),
Target::Block {
breakable: true, ..
} => Some("horizon.controller.guide.break"),
_ => None,
}
}
/// `Minecraft.startUseItem`: what is aimed at first, then the item.
fn use_caption(f: &Facts) -> Option<&'static str> {
match f.using {
Some(Active::Shoot) => return Some("horizon.controller.guide.release_shoot"),
Some(Active::Throw) => return Some("horizon.controller.guide.release_throw"),
Some(Active::Stop) => return Some("horizon.controller.guide.release_stop"),
None => {}
}
if f.hands_busy {
return None;
}
let takes = match f.target {
Target::Block { takes, .. } | Target::Entity { takes, .. } => takes,
Target::None => None,
};
takes.or(f.item_use).map(Verb::caption)
}
fn pick_caption(f: &Facts) -> Option<&'static str> {
if f.spectator {
// key.spectatorHotbar is this button: it opens the teleport menu.
return Some("horizon.controller.guide.spectator_menu");
}
(f.can_pick && f.using.is_none()).then_some("key.pickItem")
}
/// What the crosshair is on in an inventory.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Under {
#[default]
Nothing,
/// A slot. `same` is the carried stack being its kind; `result` a crafting result, which is
/// only ever taken whole.
Slot {
item: bool,
same: bool,
result: bool,
/// The carried stack can go here.
accepts: bool,
},
Recipe {
direct_craft: bool,
alternatives: bool,
},
/// One of the recipe book's other controls.
Control,
}
/// What an inventory's rows are resolved from.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MenuFacts {
pub under: Under,
pub carrying: bool,
/// The recipe book is showing: the bumpers turn its pages, the triggers its categories.
pub book: bool,
/// The bumpers switch tabs.
pub tabs: bool,
}
/// An inventory's rows, by what they ask of the player: at the bottom right one face button, at the
/// bottom left anything else (a trigger held with a button, the bumpers, View, a stick click). What
/// the crosshair is on comes first in its column, the screen's own rows after, except Close, which
/// stays lowest so the rest don't move it. Menu buttons aren't rebindable, so these name them.
pub fn resolve_menu(f: &MenuFacts) -> Guide {
use Button::{East, LeftBumper, LeftStick, LeftTrigger, North, RightBumper, RightTrigger};
use Button::{Select, South, West};
const TAKE: &str = "horizon.controller.guide.take";
const MOVE: &str = "horizon.controller.guide.quick_move";
let under = match f.under {
// A result takes only into empty hands or the same stack.
Under::Slot {
item: true,
result: true,
same,
..
} if !f.carrying || same => vec![Row::new(South, TAKE), Row::new(North, MOVE)],
Under::Slot { result: true, .. } => Vec::new(),
Under::Slot { accepts: false, .. } if f.carrying => Vec::new(),
Under::Slot { item, same, .. } if f.carrying => vec![
Row::new(
South,
if item && !same {
"horizon.controller.guide.swap_stack"
} else {
"horizon.controller.guide.put"
},
),
Row::new(West, "horizon.controller.guide.put_one"),
],
Under::Slot { item: true, .. } => vec![
Row::new(South, TAKE),
Row::new(West, "horizon.controller.guide.take_half"),
Row::new(North, MOVE),
Row::new(Select, "horizon.controller.guide.drop"),
Row::new(LeftStick, "horizon.controller.guide.drop_stack"),
],
Under::Nothing | Under::Slot { .. } => Vec::new(),
Under::Recipe {
direct_craft,
alternatives,
} => [
Some(Row::new(South, "horizon.controller.guide.place_recipe")),
direct_craft.then_some(Row::pair(
LeftTrigger,
South,
"horizon.controller.direct_craft",
)),
Some(Row::new(North, "horizon.controller.guide.place_recipe_all")),
alternatives.then_some(Row::new(West, "horizon.controller.guide.alternatives")),
]
.into_iter()
.flatten()
.collect(),
Under::Control => vec![Row::new(South, "horizon.controller.guide.select")],
};
let screen = [
f.book.then_some(Row::pair(
LeftBumper,
RightBumper,
"horizon.controller.guide.pages",
)),
f.book.then_some(Row::pair(
LeftTrigger,
RightTrigger,
"horizon.controller.guide.categories",
)),
(f.tabs && !f.book).then_some(Row::pair(
LeftBumper,
RightBumper,
"horizon.controller.guide.tabs",
)),
];
let close = Row::new(East, "horizon.controller.guide.close");
let rows = std::iter::once(close)
.chain(under)
.chain(screen.into_iter().flatten());
let (right, left) =
rows.partition(|r| r.with.is_none() && matches!(r.button, South | East | West | North));
Guide { left, right }
}
/// A row's height and the view's margin, in GUI pixels.
pub const ROW_HEIGHT: f32 = 11.0;
const MARGIN: f32 = 2.0;
fn overlap(a: &Rect, b: &Rect) -> bool {
a[0] < b[0] + b[2] && b[0] < a[0] + a[2] && a[1] < b[1] + b[3] && b[1] < a[1] + a[3]
}
/// Where a column's rows go, down from a top corner of the `[width, height]` view: under whatever
/// a row would overlap in `taken`, or nowhere when that leaves the view's upper three quarters (the
/// row is dropped: the rows come in the order they matter). The placed rows join `taken`.
pub fn place_column(
view: [f32; 2],
left: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
place(view, left, false, widths, taken)
}
/// `place_column`'s rows up from a bottom corner, over what they would overlap, within the view's
/// lower quarter: the first row is the lowest.
pub fn place_column_up(
view: [f32; 2],
left: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
place(view, left, true, widths, taken)
}
fn place(
view: [f32; 2],
left: bool,
up: bool,
widths: &[f32],
taken: &mut Vec<Rect>,
) -> Vec<Option<Rect>> {
let mut y = if up {
view[1] - MARGIN - ROW_HEIGHT
} else {
MARGIN
};
widths
.iter()
.map(|&width| {
if width > view[0] - 2.0 * MARGIN {
return None;
}
let x = if left {
MARGIN
} else {
view[0] - MARGIN - width
};
let mut at = y;
loop {
let outside = if up {
at < view[1] * 0.75
} else {
at + ROW_HEIGHT > view[1] * 0.75
};
if outside {
return None;
}
let rect = [x, at, width, ROW_HEIGHT];
let hit = taken.iter().filter(|t| overlap(&rect, t));
let past = if up {
hit.map(|t| t[1]).fold(f32::INFINITY, f32::min)
} else {
hit.map(|t| t[1] + t[3]).fold(f32::NEG_INFINITY, f32::max)
};
if past.is_infinite() {
taken.push(rect);
y = if up {
at - ROW_HEIGHT - 1.0
} else {
at + ROW_HEIGHT + 1.0
};
return Some(rect);
}
at = if up {
past.floor() - ROW_HEIGHT - 1.0
} else {
past.ceil() + 1.0
};
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn walking() -> Facts {
Facts {
on_ground: true,
can_sprint: true,
main_hand_item: true,
..Facts::default()
}
}
fn on_block(f: Facts, breakable: bool, takes: Option<Verb>) -> Facts {
Facts {
target: Target::Block { breakable, takes },
..f
}
}
fn rows(rows: &[Row]) -> Vec<(Button, &'static str)> {
rows.iter().map(|r| (r.button, r.caption)).collect()
}
fn captions(rows: &[Row]) -> Vec<&'static str> {
rows.iter().map(|r| r.caption).collect()
}
#[test]
fn survival_on_a_block_shows_break_use_and_pick_on_the_default_buttons() {
let facts = Facts {
can_pick: true,
item_use: Some(Verb::Place),
..on_block(walking(), true, None)
};
let guide = resolve(&facts, &PadSettings::default());
assert_eq!(
rows(&guide.right),
[
(Button::RightTrigger, "horizon.controller.guide.break"),
(Button::LeftTrigger, "horizon.controller.guide.place"),
(Button::DPadLeft, "key.pickItem"),
]
);
assert_eq!(
rows(&guide.left),
[
(Button::South, "key.jump"),
(Button::RightStick, "key.sneak"),
(Button::LeftStick, "key.sprint"),
(Button::North, "key.inventory"),
(Button::DPadDown, "horizon.controller.guide.drop"),
(Button::West, "horizon.controller.guide.swap"),
]
);
}
#[test]
fn a_rebound_action_moves_its_row_and_an_unbound_one_loses_it() {
let mut settings = PadSettings::default();
settings.bind("key.attack", Some(Button::North));
settings.bind("key.pickItem", None);
settings.bind("key.jump", Some(Button::Start));
let facts = Facts {
can_pick: true,
..on_block(walking(), true, None)
};
let guide = resolve(&facts, &settings);
assert_eq!(
rows(&guide.right),
[(Button::North, "horizon.controller.guide.break")]
);
// Start only ever pauses.
assert!(!captions(&guide.left).contains(&"key.jump"));
}
#[test]
fn what_a_block_or_entity_takes_comes_before_the_held_item() {
let settings = PadSettings::default();
let chest = Facts {
item_use: Some(Verb::Place),
..on_block(walking(), true, Some(Verb::Open))
};
assert_eq!(
captions(&resolve(&chest, &settings).right),
[
"horizon.controller.guide.break",
"horizon.controller.guide.open"
]
);
let villager = Facts {
item_use: Some(Verb::Eat),
target: Target::Entity {
attackable: true,
takes: Some(Verb::Trade),
},
..walking()
};
assert_eq!(
captions(&resolve(&villager, &settings).right),
[
"horizon.controller.guide.attack",
"horizon.controller.guide.trade"
]
);
// A mob the item means nothing to: the item is used on.
let cow = Facts {
target: Target::Entity {
attackable: true,
takes: None,
},
..villager
};
assert_eq!(
captions(&resolve(&cow, &settings).right)[1],
"horizon.controller.guide.eat"
);
}
#[test]
fn nothing_to_do_leaves_the_right_column_empty() {
let settings = PadSettings::default();
assert!(resolve(&walking(), &settings).right.is_empty());
// An unbreakable block too (bedrock, a creative sword, adventure without can_break).
assert!(
resolve(&on_block(walking(), false, None), &settings)
.right
.is_empty()
);
}
#[test]
fn an_item_in_use_leaves_only_letting_go() {
let facts = Facts {
using: Some(Active::Shoot),
can_pick: true,
..on_block(walking(), true, Some(Verb::Open))
};
assert_eq!(
captions(&resolve(&facts, &PadSettings::default()).right),
["horizon.controller.guide.release_shoot"]
);
}
#[test]
fn busy_hands_neither_attack_nor_use() {
let facts = Facts {
hands_busy: true,
riding: true,
item_use: Some(Verb::Eat),
..on_block(Facts::default(), true, None)
};
assert!(resolve(&facts, &PadSettings::default()).right.is_empty());
}
#[test]
fn movement_follows_what_the_player_is_doing() {
let settings = PadSettings::default();
let first_two = |f: Facts| {
let left = resolve(&f, &settings).left;
captions(&left).into_iter().take(2).collect::<Vec<_>>()
};
assert_eq!(
first_two(Facts {
flying: true,
may_fly: true,
..Facts::default()
}),
[
"horizon.controller.guide.fly_up",
"horizon.controller.guide.fly_down"
]
);
assert_eq!(
first_two(Facts {
in_water: true,
..Facts::default()
}),
[
"horizon.controller.guide.swim_up",
"horizon.controller.guide.swim_down"
]
);
// In the air, only a glider or creative flight makes a jump do anything.
assert_eq!(first_two(Facts::default()), ["key.sneak", "key.inventory"]);
assert_eq!(
first_two(Facts {
can_glide: true,
..Facts::default()
})[0],
"horizon.controller.guide.glide"
);
assert_eq!(
first_two(Facts {
sneak_latched: true,
on_ground: true,
..Facts::default()
})[1],
"horizon.controller.guide.stand"
);
// A boat can't jump; a saddled horse can; both dismount.
assert_eq!(
first_two(Facts {
riding: true,
on_ground: true,
..Facts::default()
})[0],
"horizon.controller.guide.dismount"
);
assert_eq!(
first_two(Facts {
riding: true,
vehicle_jumps: true,
..Facts::default()
}),
["key.jump", "horizon.controller.guide.dismount"]
);
// Gliding: neither jump nor sneak does anything.
assert_eq!(
first_two(Facts {
gliding: true,
..Facts::default()
}),
["key.inventory"]
);
}
#[test]
fn a_spectator_only_flies_opens_spectates_and_has_its_menu() {
let facts = Facts {
spectator: true,
flying: true,
may_fly: true,
main_hand_item: true,
target: Target::Entity {
attackable: true,
takes: None,
},
..Facts::default()
};
let guide = resolve(&facts, &PadSettings::default());
assert_eq!(
captions(&guide.right),
[
"horizon.controller.guide.spectate",
"horizon.controller.guide.spectator_menu"
]
);
assert_eq!(
captions(&guide.left),
[
"horizon.controller.guide.fly_up",
"horizon.controller.guide.fly_down"
]
);
}
#[test]
fn rows_stack_from_their_corner() {
let mut taken = Vec::new();
let left = place_column([200.0, 200.0], true, &[50.0, 30.0], &mut taken);
assert_eq!(left[0], Some([2.0, 2.0, 50.0, 11.0]));
assert_eq!(left[1], Some([2.0, 14.0, 30.0, 11.0]));
let right = place_column([200.0, 200.0], false, &[40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 2.0, 40.0, 11.0]));
}
#[test]
fn a_row_goes_under_what_it_would_overlap() {
// The effect icons in the top right corner.
let mut taken = vec![[150.0, 1.0, 50.0, 24.0]];
let right = place_column([200.0, 200.0], false, &[40.0, 40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 26.0, 40.0, 11.0]));
assert_eq!(right[1], Some([158.0, 38.0, 40.0, 11.0]));
}
#[test]
fn the_columns_of_a_narrow_view_never_overlap_and_late_rows_drop() {
// One quarter of a small window, a boss bar across its top: the rows have 31..67 left.
let view = [160.0, 90.0];
let bar = [0.0, 12.0, 160.0, 19.0];
let mut taken = vec![bar];
let right = place_column(view, false, &[90.0, 90.0, 90.0], &mut taken);
let left = place_column(view, true, &[90.0; 6], &mut taken);
let placed: Vec<Rect> = right.iter().chain(&left).flatten().copied().collect();
for (i, a) in placed.iter().enumerate() {
assert!(a[1] + a[3] <= 67.5 && a[0] >= 0.0 && a[0] + a[2] <= 160.0);
assert!(!overlap(a, &bar));
for b in &placed[i + 1..] {
assert!(!overlap(a, b), "{a:?} overlaps {b:?}");
}
}
// Three rows' worth of room, and the right column came first.
assert_eq!(right.iter().flatten().count(), 3);
assert_eq!(left.iter().flatten().count(), 0);
}
#[test]
fn rows_stack_up_from_a_bottom_corner_and_drop_above_its_quarter() {
let view = [200.0, 100.0];
let mut taken = vec![[0.0, 80.0, 60.0, 5.0]];
let left = place_column_up(view, true, &[50.0, 50.0], &mut taken);
assert_eq!(left[0], Some([2.0, 87.0, 50.0, 11.0]));
// Over the panel at 80 would be 68, under the lower quarter's 75.
assert_eq!(left[1], None);
let right = place_column_up(view, false, &[40.0], &mut taken);
assert_eq!(right[0], Some([158.0, 87.0, 40.0, 11.0]));
}
fn short(rows: &[Row]) -> Vec<&str> {
rows.iter()
.map(|r| r.caption.rsplit('.').next().unwrap_or(""))
.collect()
}
#[test]
fn an_inventorys_face_buttons_go_right_and_the_rest_left() {
let on = |under, carrying| {
resolve_menu(&MenuFacts {
under,
carrying,
..MenuFacts::default()
})
};
let slot = |item, same, result| Under::Slot {
item,
same,
result,
accepts: true,
};
let close_only = ["close"];
let empty: [&str; 0] = [];
for quiet in [Under::Nothing, slot(false, false, false)] {
let guide = on(quiet, false);
assert_eq!(short(&guide.right), close_only);
assert_eq!(short(&guide.left), empty);
}
let item = on(slot(true, false, false), false);
assert_eq!(
short(&item.right),
["close", "take", "take_half", "quick_move"]
);
assert_eq!(short(&item.left), ["drop", "drop_stack"]);
assert_eq!(
short(&on(slot(false, false, false), true).right),
["close", "put", "put_one"]
);
assert_eq!(
short(&on(slot(true, false, false), true).right),
["close", "swap_stack", "put_one"]
);
assert_eq!(
short(&on(slot(true, true, false), true).right),
["close", "put", "put_one"]
);
assert_eq!(
short(&on(slot(true, false, true), false).right),
["close", "take", "quick_move"]
);
let recipe = on(
Under::Recipe {
direct_craft: true,
alternatives: true,
},
false,
);
assert_eq!(
short(&recipe.right),
["close", "place_recipe", "place_recipe_all", "alternatives"]
);
assert_eq!(recipe.left.len(), 1);
assert_eq!(recipe.left[0].with, Some(Button::LeftTrigger));
assert_eq!(recipe.left[0].button, Button::South);
let plain = on(
Under::Recipe {
direct_craft: false,
alternatives: false,
},
false,
);
assert_eq!(
short(&plain.right),
["close", "place_recipe", "place_recipe_all"]
);
assert!(plain.left.is_empty());
}
#[test]
fn an_inventorys_screen_rows_follow_its_book_and_tabs() {
let left = |book, tabs| {
resolve_menu(&MenuFacts {
book,
tabs,
..MenuFacts::default()
})
.left
};
assert!(left(false, false).is_empty());
assert_eq!(short(&left(true, false)), ["pages", "categories"]);
assert_eq!(short(&left(false, true)), ["tabs"]);
}
#[test]
fn a_row_wider_than_the_view_is_dropped() {
let mut taken = Vec::new();
assert_eq!(
place_column([50.0, 200.0], true, &[60.0, 20.0], &mut taken),
[None, Some([2.0, 2.0, 20.0, 11.0])]
);
}
}
#[cfg(test)]
mod detail_tests {
use super::*;
fn rows() -> Guide {
Guide {
left: vec![Row::new(Button::South, "left")],
right: vec![Row::new(Button::East, "right")],
}
}
#[test]
fn full_keeps_both_columns() {
let guide = rows().at_detail(GuideDetail::Full);
assert_eq!((guide.left.len(), guide.right.len()), (1, 1));
}
#[test]
fn basic_keeps_the_right_column() {
let guide = rows().at_detail(GuideDetail::Basic);
assert_eq!((guide.left.len(), guide.right.len()), (0, 1));
}
#[test]
fn off_keeps_nothing() {
let guide = rows().at_detail(GuideDetail::Off);
assert_eq!((guide.left.len(), guide.right.len()), (0, 0));
}
#[test]
fn a_file_saved_with_the_old_switch_keeps_its_meaning() {
assert_eq!(GuideDetail::parse("true"), GuideDetail::Full);
assert_eq!(GuideDetail::parse("false"), GuideDetail::Off);
assert_eq!(GuideDetail::parse("basic"), GuideDetail::Basic);
}
}
#[cfg(test)]
mod slot_tests {
use super::*;
/// The rows for the slot: South or West, whichever column they're in.
fn rows(item: bool, same: bool, result: bool, accepts: bool, carrying: bool) -> usize {
let guide = resolve_menu(&MenuFacts {
under: Under::Slot {
item,
same,
result,
accepts,
},
carrying,
..MenuFacts::default()
});
[guide.left, guide.right]
.iter()
.flatten()
.filter(|row| matches!(row.button, Button::South | Button::West))
.count()
}
#[test]
fn a_slot_that_refuses_the_carried_stack_offers_nothing() {
assert_eq!(rows(false, false, false, false, true), 0);
assert_eq!(rows(true, true, false, false, true), 0);
assert!(rows(false, false, false, true, true) > 0);
}
#[test]
fn a_result_is_taken_only_into_empty_hands_or_the_same_stack() {
assert!(rows(true, false, true, true, false) > 0);
assert!(rows(true, true, true, true, true) > 0);
assert_eq!(rows(true, false, true, true, true), 0);
}
}
+1
View File
@@ -6,6 +6,7 @@
pub mod actions;
pub mod clipboard;
pub mod gamepad;
pub mod gameplay_guide;
pub mod held;
pub mod input;
pub mod interaction_input;
+38 -31
View File
@@ -393,38 +393,17 @@ impl FullscreenState {
let reapply = self.set_exclusive(exclusive);
self.set_preferred_mode(preferred);
let mode_change = !in_video_screen && self.take_mode_change();
if wanted && window.is_maximized() {
window.set_maximized(false);
window.set_fullscreen(Some(fullscreen_request(
match request {
Some(false) => window.set_fullscreen(None),
Some(true) => enter_fullscreen(
window,
exclusive,
preferred.as_ref(),
)));
} else {
match request {
Some(false) => window.set_fullscreen(None),
Some(true) => {
if window.is_maximized() {
window.set_maximized(false);
}
window.set_fullscreen(Some(fullscreen_request(
window,
exclusive,
preferred.as_ref(),
)));
}
None if reapply || mode_change => {
if window.is_maximized() {
window.set_maximized(false);
}
window.set_fullscreen(Some(fullscreen_request(
window,
exclusive,
preferred.as_ref(),
)));
}
None => {}
}
fullscreen_request(window, exclusive, preferred.as_ref()),
),
None if reapply || mode_change => enter_fullscreen(
window,
fullscreen_request(window, exclusive, preferred.as_ref()),
),
None => {}
}
}
@@ -602,6 +581,34 @@ fn monitor_current_mode(
.max_by_key(|m| (m.refresh_rate_millihertz(), m.bit_depth()))
}
/// `window.set_fullscreen(Some(fullscreen))`. On Windows a maximized window leaves its maximized
/// state first: winit keeps a maximized window's client area inside the work area, so a
/// borderless fullscreen kept the taskbar over its bottom and pushed its top off screen (#9984).
pub fn enter_fullscreen(window: &winit::window::Window, fullscreen: winit::window::Fullscreen) {
#[cfg(target_os = "windows")]
if window.is_maximized() {
window.set_maximized(false);
}
window.set_fullscreen(Some(fullscreen));
}
/// Borderless fullscreen on the window's own monitor (the primary one when it has none).
pub fn borderless_fullscreen(window: &winit::window::Window) -> winit::window::Fullscreen {
winit::window::Fullscreen::Borderless(
window
.current_monitor()
.or_else(|| window.primary_monitor()),
)
}
/// A window created fullscreen that the system also marked maximized enters it again
/// unmaximized ([`enter_fullscreen`]); only Windows does that.
pub fn settle_created_fullscreen(window: &winit::window::Window, fullscreen: bool) {
if cfg!(target_os = "windows") && fullscreen && window.is_maximized() {
enter_fullscreen(window, borderless_fullscreen(window));
}
}
/// `Window.applyFullscreen`: the fullscreen the window asks for. Exclusive fullscreen takes
/// `Monitor.getPreferredVideoMode` (the preferred mode if the monitor has it, else the desktop
/// mode); a monitor with no usable mode falls back to borderless, as vanilla does.
+13 -5
View File
@@ -6,7 +6,7 @@ autotests = false
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "Reusable client systems, control tool and parity probes"
[lib]
@@ -42,7 +42,8 @@ horizon-chat-filter = { path = "../horizon-chat-filter" }
horizon-shaders = { path = "../horizon-shaders" }
# Horizon's own shader packs: the format, the stages and the check (docs/shaders.md).
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-friends = { path = "../horizon-friends" }
# Play with Friends: invite codes and their tunnel; the `friends` feature.
horizon-friends = { path = "../horizon-friends", optional = true }
# The narrator (GameNarrator): text-to-speech of chat, system messages and screens.
horizon-narrator = { path = "../horizon-narrator" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
@@ -73,9 +74,10 @@ mio = { version = "1.2.4", features = ["os-poll", "net"] }
xxhash-rust = { version = "0.8.19", features = ["xxh3"] }
horizon-theme = { path = "../horizon-theme" }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`), the `heartbeat` feature.
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-bench = { path = "../horizon-bench" }
crc32fast = "1"
zip = { version = "9.1.3", default-features = false, features = ["deflate"] }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
gilrs = { version = "0.11", optional = true }
@@ -112,12 +114,18 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
default = ["audio", "controllers"]
default = ["audio", "controllers", "discord", "friends", "heartbeat"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
audio = ["horizon-client-render/audio"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense", "horizon-client-input/controllers"]
# "Playing Horizon" on the player's Discord profile (`discord_presence`).
discord = []
# Play with Friends, joining side (`net.rs`): without it no invite code joins.
friends = ["dep:horizon-friends"]
# The anonymous "I'm playing" ping (`heartbeat.rs`, docs/telemetry.md).
heartbeat = ["horizon-telemetry/live"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
# Compatibility with earlier experiment commands: main already uses mimalloc.
+3 -2
View File
@@ -3,7 +3,7 @@ name = "horizon-client-render"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
rust-version.workspace = true
description = "The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
@@ -23,10 +23,11 @@ horizon-narrator = { path = "../horizon-narrator" }
# OptiFine-format pack files: random entity textures (hud.rs), connected textures
# (connected.rs) and custom item texture rules (cit.rs).
horizon-optifine = { path = "../horizon-optifine" }
horizon-polytone = { path = "../horizon-polytone" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-theme = { path = "../horizon-theme" }
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
@@ -0,0 +1,55 @@
//! Headless production brewing-fuel component fixture.
use horizon_client_render::{
brewing::{Brewing, BrewingStand, brewing_fuel},
providers::Providers,
};
use horizon_items::{
item_data::decode_components,
items::{ItemStack, Items},
};
use horizon_world::loot::PackOverlay;
use std::{
io::{BufRead, Write},
path::Path,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let items = Items::catalog(root, &PackOverlay::NONE)?;
let providers = Providers::load(root);
let brewing = Brewing {
recipes: vec![],
reagents: Default::default(),
inputs: Default::default(),
};
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[3])?);
for line in std::io::BufReader::new(std::fs::File::open(&args[2])?).lines() {
let input: serde_json::Value = serde_json::from_str(&line?)?;
let mut stack = ItemStack::new(
input["item"].as_str().unwrap(),
input["count"].as_u64().unwrap() as u32,
);
let components = horizon_client_core::scoreboard::json_to_tag(&input["components"]);
let (_, extra) = decode_components(Some(&components));
stack.extra = extra;
let accepted = brewing_fuel(&items, &stack).is_some();
let mut stand = BrewingStand::new();
// A newly constructed vanilla block entity has no saved total yet.
stand.total_fuel = 0;
stand.items[4] = Some(stack);
for _ in 0..2 {
stand.tick(&brewing, &items, &providers);
writeln!(
out,
"{} {} {} {} {}",
accepted,
stand.fuel,
stand.total_fuel,
stand.speed.to_bits(),
stand.items[4].as_ref().map_or(0, |s| s.count)
)?;
}
}
Ok(())
}
@@ -32,6 +32,11 @@ fn pack(properties: &str, shadow: Option<&str>, reads_shadow: bool) -> horizon_s
write("final.fsh", if reads_shadow { SHADOW_READ } else { COLOUR });
write("gbuffers_entities.vsh", VERTEX);
write("gbuffers_entities.fsh", COLOUR);
// Terrain programs keep the load free of "nothing draws it" notes; no terrain is drawn.
for terrain in ["gbuffers_terrain", "gbuffers_water"] {
write(&format!("{terrain}.vsh"), VERTEX);
write(&format!("{terrain}.fsh"), COLOUR);
}
if let Some(shadow) = shadow {
write("shadow.vsh", VERTEX);
write("shadow.fsh", shadow);
@@ -67,6 +72,7 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
true,
false,
false,
))?;
assert!(!renderer.pack_entity_shadows(), "no selected pack");
renderer.set_shader_pack(0, None, &[]).unwrap();
@@ -225,6 +225,7 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
false,
false,
false,
))?;
drop(pixels);
render.pixels.pack();
@@ -244,6 +245,7 @@ fn replay(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
settings.fxaa,
settings.taa,
settings.upscale,
0.85,
);
let mut notes = Vec::new();
let mut note = |what: &str, result: horizon_client_render::Result<()>| {
@@ -659,6 +661,7 @@ fn remesh(
blend_radius,
smooth_lighting,
cutout_leaves,
opaque_ice: false,
translucent_layers: &[],
leaf_culling,
};
@@ -50,10 +50,14 @@ fn pack(renderer: &mut Renderer, folder: &Path, valid: bool) {
std::fs::create_dir_all(&shaders).unwrap();
let vertex = "#version 120\nvarying vec2 uv; void main(){gl_Position=ftransform();uv=gl_MultiTexCoord0.xy;}\n";
let fragment = "#version 120\nuniform int entityId; uniform int blockEntityId; uniform vec4 entityColor; void main(){gl_FragData[0]=vec4(float(entityId)/255.0,entityColor.a,float(blockEntityId)/255.0,1.0);}\n";
// The terrain programs keep the load free of "nothing draws it" notes; this
// fixture draws no terrain.
for program in [
"gbuffers_entities",
"gbuffers_block",
"gbuffers_entities_translucent",
"gbuffers_terrain",
"gbuffers_water",
] {
std::fs::write(shaders.join(format!("{program}.vsh")), vertex).unwrap();
std::fs::write(
@@ -127,6 +131,7 @@ fn fixture(window: Arc<Window>) -> Result<(), Box<dyn std::error::Error>> {
false,
true,
false,
false,
))?;
renderer.resize(320, 180);
assert!(
@@ -349,6 +354,7 @@ fn fixture_frame(hud: &HudBatch) -> Frame<'_> {
skybox: false,
end_sky: false,
end_sky_sprite: None,
boss_world_fog: false,
environment_fog: None,
water_fog: [0.0; 3],
fog: None,
+343 -17
View File
@@ -47,6 +47,53 @@ struct Channel {
world: u32,
/// The sound file it started and when, to tell another player's copy of it.
started: Option<(String, std::time::Instant)>,
/// Its sound event plays clearly through a mod's muffle (`Audio::muffle`).
unmuffled: bool,
}
/// The cutoff of the muffle's low-pass at full strength: the world heard through ringing ears.
const MUFFLED_CUTOFF: f32 = 300.0;
/// The cutoff it sweeps down from as its strength rises.
const OPEN_CUTOFF: f32 = 20_000.0;
/// How long the muffle takes to glide to a new level: a game tick's step (50 ms) would click.
const MUFFLE_GLIDE: f32 = 0.02;
/// A mod's muffle (`Audio::muffle`) in the mixer: the other sounds are mixed apart, low-passed by
/// two one-pole stages and turned down, then added to the sounds it leaves clear. While it is off
/// (never set, or faded out) the sounds mix straight into the output, as without it.
#[derive(Default)]
struct Muffle {
/// `[amount, lowpass]`, each 0 to 1, as the game's tick last set them.
target: [f32; 2],
/// Where the output is now, gliding to `target`.
current: [f32; 2],
/// The low-pass stages' state for the left and right output channels.
stages: [[f32; 2]; 2],
/// The muffled sounds of one buffer, reused.
bus: Vec<f32>,
}
impl Muffle {
fn active(&self) -> bool {
self.target != [0.0; 2] || self.current.iter().any(|v| *v > 1e-4)
}
/// Adds `bus` (the muffled sounds) to `data`, low-passed and turned down.
fn apply(&mut self, bus: &[f32], data: &mut [f32], channels: usize, rate: u32) {
let rate = rate as f32;
let glide = 1.0 - (-1.0 / (MUFFLE_GLIDE * rate)).exp();
for (frame, muffled) in data.chunks_mut(channels).zip(bus.chunks(channels)) {
for (current, target) in self.current.iter_mut().zip(self.target) {
*current += (target - *current) * glide;
}
let [amount, lowpass] = self.current;
let gain = 1.0 - amount;
let cutoff = OPEN_CUTOFF * (MUFFLED_CUTOFF / OPEN_CUTOFF).powf(lowpass);
let a = 1.0 - (-std::f32::consts::TAU * cutoff / rate).exp();
for ((out, s), stage) in frame.iter_mut().zip(muffled).zip(&mut self.stages) {
stage[0] += (s - stage[0]) * a;
stage[1] += (stage[0] - stage[1]) * a;
// Dry at no strength, so a muffle that only turns sounds down leaves them as they were.
*out += (s + (stage[1] - s) * lowpass) * gain;
}
}
}
}
#[derive(Default)]
struct Mixer {
@@ -65,6 +112,8 @@ struct Mixer {
cameras: Vec<([f64; 3], Basis, u32)>,
/// Per-source gain/pan/step cache for one buffer, reused across callbacks.
voices: Vec<(ChannelId, [f32; 3], f64)>,
/// A mod's muffle of the sounds it does not exempt.
muffle: Muffle,
limiter_peak: f32,
/// The output device the stream was opened on (`Library.currentDeviceName`).
#[cfg(feature = "audio")]
@@ -100,6 +149,21 @@ pub struct Heard {
pub range: f32,
pub looping: bool,
}
/// A mod's muffle (`Audio::muffle`): its full strength, and the ticks it fades over and has left.
#[derive(Clone, Copy, Debug, PartialEq)]
struct MuffleFade {
amount: f32,
lowpass: f32,
ticks: u32,
left: u32,
}
impl MuffleFade {
/// `[amount, lowpass]` now: full when set, falling in a straight line to none.
fn level(&self) -> [f32; 2] {
let left = self.left as f32 / self.ticks as f32;
[self.amount * left, self.lowpass * left]
}
}
/// The mixer side of the engine (`Backend`).
pub struct MixerBackend {
mixer: Arc<Mutex<Mixer>>,
@@ -247,6 +311,8 @@ pub struct Audio {
pub stopped: std::collections::BTreeMap<String, u32>,
/// The volume sliders' `SoundPreviewHandler`.
preview: crate::sound_preview::SoundPreview,
/// A mod's muffle fading out (`muffle`), advanced by `tick`.
muffle: Option<MuffleFade>,
/// `SoundEngine.lastSeenDevices`: the system default output last seen.
#[cfg(feature = "audio")]
default_seen: Option<String>,
@@ -261,6 +327,40 @@ pub struct Audio {
/// Ticks since the output devices were last looked at (`DeviceTracker.tick`).
#[cfg(feature = "audio")]
device_ticks: u32,
/// The look at the output devices running on its own thread, until a tick
/// takes its answer: listing them can take 100 ms (#11490).
#[cfg(feature = "audio")]
device_probe: Option<PendingProbe>,
}
/// What one look at the output devices found: the system default, the
/// fingerprint, and the device list when those say it may have changed.
#[cfg(feature = "audio")]
struct DeviceProbe {
default: Option<String>,
cards: Option<String>,
devices: Option<Vec<String>>,
}
#[cfg(feature = "audio")]
type PendingProbe = Arc<Mutex<Option<DeviceProbe>>>;
/// Looks at the output devices as `should_change_device` did on the render
/// thread: the device list only when the default or the fingerprint moved.
#[cfg(feature = "audio")]
fn probe_devices(default_seen: Option<String>, cards_seen: Option<String>) -> DeviceProbe {
let default = default_output_name();
let cards = card_fingerprint();
// Listing the devices probes every one (on Linux every built-in PCM,
// printing ALSA's errors for the plugins this system lacks; on macOS
// every Core Audio device, about 90 ms with twenty of them; on Windows
// every endpoint, with no fingerprint to skip it); only the fingerprint's
// devices can change the list, and it is cheap.
let devices = (default != default_seen
|| cards_changed(cards_seen.as_deref(), cards.as_deref()))
.then(output_devices);
DeviceProbe {
default,
cards,
devices,
}
}
fn decode(path: &Path) -> Option<(Vec<f32>, u32, u16)> {
use symphonia::core::{
@@ -405,6 +505,7 @@ impl Backend for MixerBackend {
owner: self.owner,
world: self.world,
started: None,
unmuffled: false,
},
);
Some(id)
@@ -432,6 +533,9 @@ impl Backend for MixerBackend {
fn set_relative(&mut self, channel: ChannelId, relative: bool) {
self.with(channel, |c| c.relative = relative);
}
fn set_unmuffled(&mut self, channel: ChannelId, unmuffled: bool) {
self.with(channel, |c| c.unmuffled = unmuffled);
}
/// `getCompleteBuffer` (cached, else decoded on the decoder thread) or
/// `getStream` (decoded on a thread of its own), then attach and
/// `alSourcePlay`. A sound that cannot load leaves its source initial,
@@ -668,6 +772,13 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
voices.push((*id, [left, right, gain], step));
}
// A mod's muffle: the sounds it doesn't exempt mix into a bus of their own first.
let muffling = mixer.muffle.active();
let mut bus = std::mem::take(&mut mixer.muffle.bus);
if muffling {
bus.clear();
bus.resize(data.len(), 0.0);
}
for (id, [left, right, gain], step) in voices.iter().copied() {
let Some(c) = mixer.channels.get_mut(&id) else {
continue;
@@ -677,7 +788,12 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
};
let source_channels = source_channels as usize;
let frames = samples.len() / source_channels;
for frame in data.chunks_mut(channels) {
let out: &mut [f32] = if muffling && !c.unmuffled {
&mut bus
} else {
&mut *data
};
for frame in out.chunks_mut(channels) {
let mut i = c.position as usize;
if i + 1 >= frames {
if c.looping && frames > 1 {
@@ -712,6 +828,14 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
}
}
mixer.voices = voices;
if muffling {
mixer.muffle.apply(&bus, data, channels, rate);
} else {
// Off: the next muffle starts from silence, not from the last one's filter.
mixer.muffle.current = [0.0; 2];
mixer.muffle.stages = [[0.0; 2]; 2];
}
mixer.muffle.bus = bus;
// Limit the whole buffer together, preserving waveform and stereo balance
// instead of flattening each overloaded sample; release over 200 ms
// (OpenAL Soft's output limiter, `ALC_OUTPUT_LIMITER_SOFT`, releases
@@ -1037,12 +1161,13 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: 1.0,
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
random: crate::random_source::create(),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
#[cfg(feature = "audio")]
default_seen: if live { default_output_name() } else { None },
#[cfg(feature = "audio")]
@@ -1051,6 +1176,8 @@ impl Audio {
devices_seen: devices,
#[cfg(feature = "audio")]
device_ticks: 0,
#[cfg(feature = "audio")]
device_probe: None,
}
}
#[cfg(not(feature = "audio"))]
@@ -1196,12 +1323,13 @@ impl Audio {
listener: [0.0; 3],
listener_yaw: 0.0,
master: self.master,
random: LegacyRandom::new(crate::sound_engine::unique_seed()),
random: crate::random_source::create(),
source_override: None,
capture: None,
heard: Default::default(),
stopped: Default::default(),
preview: Default::default(),
muffle: None,
// The device belongs to the first player's output, which looks after it.
#[cfg(feature = "audio")]
default_seen: self.default_seen.clone(),
@@ -1211,6 +1339,8 @@ impl Audio {
devices_seen: self.devices_seen.clone(),
#[cfg(feature = "audio")]
device_ticks: 0,
#[cfg(feature = "audio")]
device_probe: None,
}
}
/// Sync the volume options, refreshing the categories that changed
@@ -1232,7 +1362,8 @@ impl Audio {
/// `SoundEngine.shouldChangeDevice`: the open device was lost, the system
/// default changed (with no device chosen in the options), or the chosen
/// device came back; the output is then opened again. The devices are looked
/// at once a second.
/// at once a second, on a thread of their own: a later tick reads what it
/// found.
#[cfg(feature = "audio")]
fn should_change_device(&mut self) -> bool {
// A splitscreen share plays through the first player's output: it leaves the device,
@@ -1246,29 +1377,54 @@ impl Audio {
};
if lost {
log::info!("Horizon: audio device was lost");
self.device_probe = None;
self.default_seen = default_output_name();
self.cards_seen = card_fingerprint();
self.devices_seen = output_devices();
return true;
}
if let Some(pending) = &self.device_probe {
let Some(probe) = pending.lock().ok().and_then(|mut found| found.take()) else {
return false;
};
self.device_probe = None;
return self.devices_changed(probe, current);
}
self.device_ticks += 1;
if self._stream.is_none() || self.device_ticks < 20 {
return false;
}
self.device_ticks = 0;
let default = default_output_name();
let cards = card_fingerprint();
// Listing the devices probes every one (on Linux every built-in PCM,
// printing ALSA's errors for the plugins this system lacks; on macOS
// every Core Audio device, about 90 ms with twenty of them); only the
// fingerprint's devices can change the list, and it is cheap.
if default == self.default_seen
&& !cards_changed(self.cards_seen.as_deref(), cards.as_deref())
{
return false;
let pending = PendingProbe::default();
let out = Arc::clone(&pending);
let seen = (self.default_seen.clone(), self.cards_seen.clone());
let started = std::thread::Builder::new()
.name("audio-devices".into())
.spawn(move || {
let probe = probe_devices(seen.0, seen.1);
if let Ok(mut found) = out.lock() {
*found = Some(probe);
}
});
match started {
Ok(_) => self.device_probe = Some(pending),
Err(e) => log::warn!("Horizon: could not look at the audio devices: {e}"),
}
false
}
/// The rest of `shouldChangeDevice` with what a look at the devices found,
/// `current` being the device playing now.
#[cfg(feature = "audio")]
fn devices_changed(&mut self, probe: DeviceProbe, current: String) -> bool {
let DeviceProbe {
default,
cards,
devices,
} = probe;
let Some(devices) = devices else {
return false;
};
self.cards_seen = cards;
let devices = output_devices();
// Only a change in the device lists is looked at, as in vanilla: a
// preferred device that is listed but cannot open is not retried.
if default == self.default_seen && devices == self.devices_seen {
@@ -1306,6 +1462,50 @@ impl Audio {
}
self.sync_volumes();
self.engine.tick(paused, ctx, &mut self.backend);
// A paused game holds a mod's muffle where it is, as it holds the sounds.
if !paused && let Some(fade) = &mut self.muffle {
fade.left = fade.left.saturating_sub(1);
let level = fade.level();
if fade.left == 0 {
self.muffle = None;
self.engine.set_unmuffled(Vec::new(), &mut self.backend);
}
self.set_muffle_level(level);
}
}
/// A mod's muffle (`native_muffle_sounds`): `amount` (0 to 1) of every sound's volume taken
/// away and `lowpass` (0 to 1) dulling it, from untouched to a 300 Hz low-pass, fading in a
/// straight line to none over `ticks` client ticks; the sound events in `exempt` (namespaced,
/// `mymod:ear.ringing`) play clearly through it, those already playing too. It replaces any
/// muffle in place; `ticks` 0 or no strength ends it. The output's, so in splitscreen every
/// player hears it. False for an amount or strength outside 0 to 1.
pub fn muffle(&mut self, amount: f32, lowpass: f32, ticks: u32, exempt: &[&str]) -> bool {
if !(0.0..=1.0).contains(&amount) || !(0.0..=1.0).contains(&lowpass) {
return false;
}
let on = ticks > 0 && (amount > 0.0 || lowpass > 0.0);
let exempt = if on {
exempt
.iter()
.map(|e| crate::sound_events::normalize(e))
.collect()
} else {
Vec::new()
};
self.engine.set_unmuffled(exempt, &mut self.backend);
self.muffle = on.then_some(MuffleFade {
amount,
lowpass,
ticks,
left: ticks,
});
self.set_muffle_level(self.muffle.map_or([0.0; 2], |fade| fade.level()));
true
}
fn set_muffle_level(&mut self, level: [f32; 2]) {
if let Ok(mut m) = self.backend.mixer.lock() {
m.muffle.target = level;
}
}
/// The `uiSoundReserve` extra: positional sounds leave
/// `sound_engine::HEARD_RESERVE` static channels free (off: vanilla's pool).
@@ -1404,6 +1604,14 @@ impl Audio {
let id = self.engine.add(instance, false);
self.engine.play_delayed(id, ticks);
}
/// `SoundManager.playDelayed` of an instance the caller made: it starts
/// `ticks` sound-engine ticks from now.
pub fn play_instance_after(&mut self, instance: SoundInstance, ticks: i32) {
*self.heard.entry(instance.identifier.clone()).or_default() += 1;
self.sync_volumes();
let id = self.engine.add(instance, false);
self.engine.play_delayed(id, ticks);
}
/// `play` for this client only: a sound a packet's handler plays
/// (`playLocalSound`), which the guests play from that packet themselves.
pub fn play_unshared(&mut self, event: &str, at: Option<[f64; 3]>, volume: f32, pitch: f32) {
@@ -1418,7 +1626,7 @@ impl Audio {
"minecraft:ui.button.click",
1.0,
0.25,
SoundEngine::unseeded(),
crate::random_source::create(),
),
false,
);
@@ -1560,7 +1768,7 @@ impl Audio {
f64::from(pos[2]) + 0.5,
];
let event = crate::sound_events::normalize(event);
let instance = SoundInstance::for_jukebox_song(&event, at, SoundEngine::unseeded());
let instance = SoundInstance::for_jukebox_song(&event, at, crate::random_source::create());
self.play_instance(instance, true).0
}
/// Stop and drop a held instance.
@@ -1771,6 +1979,7 @@ mod tests {
owner: 0,
world: 0,
started: None,
unmuffled: false,
}
}
@@ -2191,6 +2400,123 @@ mod tests {
assert_eq!(data, [0.5; 2]);
}
/// One second of a `hz` sine at 48 kHz, amplitude 0.3 (looped, it repeats seamlessly).
fn sine(hz: f32) -> Vec<f32> {
(0..48_000)
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / 48_000.0).sin())
.collect()
}
fn rms(data: &[f32]) -> f32 {
(data.iter().map(|s| s * s).sum::<f32>() / data.len() as f32).sqrt()
}
/// `seconds` of the mixer's stereo output, in the 10 ms buffers an output stream asks for.
fn listen(m: &mut Mixer, seconds: f32) -> Vec<f32> {
let mut out = Vec::new();
let mut buffer = [0.0; 960];
for _ in 0..(seconds * 100.0).round() as usize {
mix(m, &mut buffer, 2, 48_000);
out.extend_from_slice(&buffer);
}
out
}
#[test]
fn a_muffle_dulls_and_quietens_every_sound_but_the_exempt_ones() {
// A looping tone heard through a muffle of `[amount, lowpass]`, once it has settled.
let heard = |hz: f32, muffle: [f32; 2], unmuffled: bool| {
let mut m = Mixer::default();
let mut c = channel(sine(hz), None, [0.0, 0.0, 2.0], true);
c.unmuffled = unmuffled;
m.channels.insert(1, c);
m.muffle.target = muffle;
listen(&mut m, 0.2);
listen(&mut m, 0.2)
};
let plain = |hz: f32| rms(&heard(hz, [0.0; 2], false));
// Fully low-passed, a 6 kHz whine all but goes and a 100 Hz hum keeps most of its level.
assert!(rms(&heard(6000.0, [0.0, 1.0], false)) < plain(6000.0) * 0.05);
assert!(rms(&heard(100.0, [0.0, 1.0], false)) > plain(100.0) * 0.8);
// Half the volume taken and no low-pass: half the level, the tone itself untouched.
let ducked = rms(&heard(6000.0, [0.5, 0.0], false)) / plain(6000.0);
assert!((ducked - 0.5).abs() < 1e-3, "{ducked}");
// An exempt sound plays through as if there were no muffle, sample for sample.
assert_eq!(
heard(6000.0, [0.5, 1.0], true),
heard(6000.0, [0.0; 2], false)
);
}
#[test]
fn a_faded_muffle_leaves_the_sound_as_it_was() {
let mixer = || {
let mut m = Mixer::default();
m.channels
.insert(1, channel(sine(3000.0), None, [2.0, 0.0, 1.0], true));
m
};
let (mut muffled, mut plain) = (mixer(), mixer());
muffled.muffle.target = [0.7, 1.0];
assert_ne!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
muffled.muffle.target = [0.0; 2];
listen(&mut muffled, 0.5);
listen(&mut plain, 0.5);
assert_eq!(listen(&mut muffled, 0.1), listen(&mut plain, 0.1));
}
#[test]
fn a_mods_muffle_fades_with_the_game_ticks_and_spares_its_exempt_sounds() {
let mut audio = Audio::silent(Path::new("/nonexistent/horizon-test-assets"));
audio.engine.registry.add_file(
"minecraft",
br#"{"ambient.cave":{"sounds":["ambient/cave/cave1"]}}"#,
&|_, _| true,
);
audio.engine.registry.add_file(
"hztest",
br#"{"ear.ringing":{"sounds":["ear/ringing"]}}"#,
&|_, _| true,
);
// The engine makes its channels; with no output, none loads a file.
audio.engine.loaded = true;
let marks = |audio: &Audio| {
let m = audio.backend.mixer.lock().unwrap();
let mut marks: Vec<_> = m
.channels
.iter()
.map(|(&id, c)| (id, c.unmuffled))
.collect();
marks.sort();
marks
};
let level = |audio: &Audio| audio.backend.mixer.lock().unwrap().muffle.target;
let close =
|a: [f32; 2], b: [f32; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6;
// Two sounds already playing, and one exempt sound started under the muffle.
audio.play("ambient.cave", None, 1.0, 1.0);
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert!(audio.muffle(0.8, 1.0, 4, &["hztest:ear.ringing"]));
audio.play("hztest:ear.ringing", None, 1.0, 1.0);
assert_eq!(marks(&audio), [(1, false), (2, true), (3, true)]);
assert!(close(level(&audio), [0.8, 1.0]));
let ticks = TickContext::default();
audio.tick(false, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
// A paused game holds it.
audio.tick(true, &ticks);
assert!(close(level(&audio), [0.6, 0.75]), "{:?}", level(&audio));
for _ in 0..3 {
audio.tick(false, &ticks);
}
assert_eq!(level(&audio), [0.0; 2]);
// Over: its exemptions go with it.
assert_eq!(marks(&audio), [(1, false), (2, false), (3, false)]);
// Out of range changes nothing.
assert!(!audio.muffle(1.5, 0.0, 10, &[]));
assert_eq!(level(&audio), [0.0; 2]);
}
#[test]
fn output_devices_are_looked_at_again_only_when_the_cards_change() {
// No fingerprint (not Linux, or the cards could not be read): the list
+6 -1
View File
@@ -191,13 +191,18 @@ pub fn discovery_url() -> String {
.unwrap_or_else(|| DEFAULT_DISCOVERY.to_owned())
}
fn agent() -> ureq::Agent {
pub(crate) fn agent() -> ureq::Agent {
ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.user_agent(&format!("Horizon/{}", horizon_core::release::VERSION))
.build()
}
/// Whether `HORIZON_API_DISCOVERY` names another services environment.
pub fn discovery_overridden() -> bool {
discovery_url() != DEFAULT_DISCOVERY
}
/// The discovery document, fetched once per game (a failure is retried next time).
pub(crate) fn discovery() -> Result<Arc<serde_json::Value>, JoinError> {
static CACHE: Mutex<Option<Arc<serde_json::Value>>> = Mutex::new(None);
@@ -0,0 +1,314 @@
//! GPU Culling's Auto mode: the setting switched by what limits the frame, measured here.
//!
//! GPU Culling moves the walk over the near sections and the listing of their draws from the
//! render thread to the GPU. That pays where the CPU is the slower of the two and costs where
//! the GPU is, and which a machine is can't be told from its parts: a fast CPU with a modest
//! GPU, a slow one with a strong GPU and every mix between depend on the scene, the render
//! distance and the resolution. So Auto watches the frames' own CPU and GPU times, and only
//! where the CPU is clearly the slower one it tries GPU Culling, measures the frame again, and
//! keeps it if the frame got faster. A trial that didn't help is undone and not repeated for
//! minutes of play, then for longer, then not until the next start, so a machine or a scene it
//! doesn't suit loses a few frames' time a few times, not every half minute.
//!
//! A verdict is the medians of a window of frames (`WINDOW`), which a few slow frames (a chunk
//! load, say) don't move. Nothing here reads the GPU or draws anything: the caller feeds it the
//! frames and applies what it answers.
/// Frames in one verdict.
pub const WINDOW: usize = 240;
/// The CPU's time must be this many times the GPU's for a window to count as CPU-bound.
const CPU_BOUND: f32 = 1.3;
/// Below this CPU time, in milliseconds, a frame is fast enough that neither matters.
const WORTH_TRYING_MS: f32 = 1.5;
/// A trial is kept when the frame's busier side dropped under this share of what it was.
const KEEP_BELOW: f32 = 0.97;
/// Windows of CPU-bound frames in a row before a trial.
const STREAK: u8 = 2;
/// Windows left out after a switch, while its effects settle.
const SETTLE: u8 = 1;
/// How long, in milliseconds of frames watched, after the first and the second trial that didn't
/// help before another; after a third there is none.
const RETRY_AFTER_MS: [f64; 2] = [5.0 * 60_000.0, 20.0 * 60_000.0];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum State {
/// Off, looking for a CPU-bound frame.
Watching,
/// On for a trial: left out `SETTLE` windows, then measured against the frame before.
Trial,
/// The trial made the frame faster: on from here.
Kept,
}
/// What Auto has decided and why, a step at a time.
#[derive(Clone, Debug)]
pub struct GpuCullingAuto {
cpu: Vec<f32>,
gpu: Vec<f32>,
on: bool,
state: State,
/// CPU-bound windows in a row.
streak: u8,
/// The frame's busier side before the trial, in milliseconds.
before: f32,
/// Windows still left out after a switch.
settling: u8,
/// The frames watched so far, in milliseconds of their own time.
clock_ms: f64,
/// When the next trial may start, on that clock.
retry_at_ms: f64,
/// Trials that didn't help.
rejected: usize,
}
impl Default for GpuCullingAuto {
fn default() -> Self {
Self::new()
}
}
impl GpuCullingAuto {
pub fn new() -> Self {
Self {
cpu: Vec::with_capacity(WINDOW),
gpu: Vec::with_capacity(WINDOW),
on: false,
state: State::Watching,
streak: 0,
before: 0.0,
settling: 0,
clock_ms: 0.0,
retry_at_ms: 0.0,
rejected: 0,
}
}
/// Whether Auto has GPU Culling on now.
pub fn on(&self) -> bool {
self.on
}
/// Where Auto is, for the status: `watching`, `trial` or `kept`.
pub fn state(&self) -> &'static str {
match self.state {
State::Watching => "watching",
State::Trial => "trial",
State::Kept => "kept",
}
}
/// Back to the start: off, with nothing measured (the mode changed, or the world did).
pub fn reset(&mut self) {
*self = Self::new();
}
/// One frame's CPU time, a GPU time where its timestamps have arrived, and its whole time,
/// in milliseconds. Answers the new state when this frame completed a window that changed it.
pub fn observe(&mut self, cpu_ms: f32, gpu_ms: Option<f32>, frame_ms: f32) -> Option<bool> {
self.clock_ms += f64::from(frame_ms);
self.cpu.push(cpu_ms);
if let Some(gpu) = gpu_ms {
self.gpu.push(gpu);
}
if self.cpu.len() < WINDOW {
return None;
}
let cpu = median(&mut self.cpu);
// A GPU that can't be timed leaves nothing to compare with: Auto stays as it is.
let gpu = (self.gpu.len() >= WINDOW / 4).then(|| median(&mut self.gpu));
self.cpu.clear();
self.gpu.clear();
self.verdict(cpu, gpu?)
}
fn verdict(&mut self, cpu: f32, gpu: f32) -> Option<bool> {
if self.settling > 0 {
self.settling -= 1;
return None;
}
match self.state {
State::Kept => None,
State::Watching => {
if self.clock_ms < self.retry_at_ms {
return None;
}
let bound = cpu >= WORTH_TRYING_MS && cpu > gpu * CPU_BOUND;
self.streak = if bound { self.streak + 1 } else { 0 };
if self.streak < STREAK {
return None;
}
self.streak = 0;
self.before = cpu.max(gpu);
self.state = State::Trial;
self.settling = SETTLE;
self.on = true;
Some(true)
}
State::Trial => {
if cpu.max(gpu) < self.before * KEEP_BELOW {
self.state = State::Kept;
return None;
}
self.state = State::Watching;
self.retry_at_ms = RETRY_AFTER_MS
.get(self.rejected)
.map_or(f64::INFINITY, |wait| self.clock_ms + wait);
self.rejected += 1;
self.on = false;
Some(false)
}
}
}
}
fn median(values: &mut [f32]) -> f32 {
values.sort_by(f32::total_cmp);
values[values.len() / 2]
}
#[cfg(test)]
mod tests {
use super::*;
/// A window of frames at constant times, each as long as its busier side; the last answer.
fn window(auto: &mut GpuCullingAuto, cpu: f32, gpu: f32) -> Option<bool> {
(0..WINDOW)
.filter_map(|_| auto.observe(cpu, Some(gpu), cpu.max(gpu)))
.last()
}
/// A machine whose CPU limits the frame is tried, and keeps GPU Culling when the frame
/// gets faster.
#[test]
fn a_cpu_bound_frame_tries_it_and_keeps_it_when_it_helps() {
let mut auto = GpuCullingAuto::new();
assert!(!auto.on());
// One CPU-bound window is not yet a verdict.
assert_eq!(window(&mut auto, 5.0, 2.0), None);
assert_eq!(window(&mut auto, 5.0, 2.0), Some(true));
assert!(auto.on());
assert_eq!(auto.state(), "trial");
// The window after the switch is left out, whatever it shows.
assert_eq!(window(&mut auto, 9.0, 9.0), None);
// The frame's busier side went from 5.0 to 3.0 ms.
assert_eq!(window(&mut auto, 3.0, 2.6), None);
assert_eq!(auto.state(), "kept");
assert!(auto.on());
// Kept stays kept, even if a later scene is GPU-bound.
assert_eq!(window(&mut auto, 1.0, 8.0), None);
assert!(auto.on());
}
/// Where it didn't help, it is undone, and not tried again for 5 minutes, then 20, then
/// not at all: a scene it doesn't suit is not tried every half minute.
#[test]
fn a_trial_that_does_not_help_is_undone_and_waits_longer_each_time() {
let mut auto = GpuCullingAuto::new();
// One window is 240 frames of 5 ms: 1.2 s. A trial, and its rejection:
let reject = |auto: &mut GpuCullingAuto| {
window(auto, 5.0, 2.0);
assert_eq!(window(auto, 5.0, 2.0), Some(true));
window(auto, 5.0, 2.0);
// The GPU took over the frame: 5.0 ms before, 5.4 after.
assert_eq!(window(auto, 2.5, 5.4), Some(false));
assert!(!auto.on());
assert_eq!(auto.state(), "watching");
};
reject(&mut auto);
// CPU-bound as ever, and left alone for 5 minutes of frames: 250 windows.
let mut waited = 0;
while window(&mut auto, 5.0, 2.0).is_none() {
waited += 1;
}
assert!((248..=252).contains(&waited), "{waited}");
// That window started the second trial; it is rejected too.
window(&mut auto, 5.0, 2.0);
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
// Now 20 minutes: 1000 windows.
let mut waited = 0;
while window(&mut auto, 5.0, 2.0).is_none() {
waited += 1;
}
assert!((998..=1002).contains(&waited), "{waited}");
window(&mut auto, 5.0, 2.0);
assert_eq!(window(&mut auto, 2.5, 5.4), Some(false));
// A third rejection is for good.
for _ in 0..5000 {
assert_eq!(window(&mut auto, 5.0, 2.0), None);
}
assert!(!auto.on());
}
/// A GPU-bound machine is never switched: its frame is the GPU's.
#[test]
fn a_gpu_bound_frame_is_left_alone() {
let mut auto = GpuCullingAuto::new();
for _ in 0..20 {
assert_eq!(window(&mut auto, 0.5, 3.6), None);
}
assert!(!auto.on());
}
/// Within the margin neither side limits the frame, and a single CPU-bound window among
/// others breaks no streak into a trial.
#[test]
fn neither_a_balanced_nor_an_occasional_cpu_bound_frame_is_tried() {
let mut auto = GpuCullingAuto::new();
for _ in 0..10 {
assert_eq!(window(&mut auto, 3.0, 2.6), None);
}
for _ in 0..10 {
assert_eq!(window(&mut auto, 5.0, 2.0), None);
assert_eq!(window(&mut auto, 2.0, 2.0), None);
}
assert!(!auto.on());
}
/// Fast frames aren't worth a trial, bound or not.
#[test]
fn a_frame_under_the_floor_is_not_tried() {
let mut auto = GpuCullingAuto::new();
for _ in 0..10 {
assert_eq!(window(&mut auto, 1.2, 0.2), None);
}
assert!(!auto.on());
}
/// Without GPU times (a GPU that can't be timed) there is nothing to compare.
#[test]
fn frames_without_gpu_times_decide_nothing() {
let mut auto = GpuCullingAuto::new();
for _ in 0..WINDOW * 10 {
assert_eq!(auto.observe(8.0, None, 8.0), None);
}
assert!(!auto.on());
}
/// A spike in a window doesn't move its verdict: the median ignores it.
#[test]
fn a_few_slow_frames_do_not_make_a_window_cpu_bound() {
let mut auto = GpuCullingAuto::new();
for _ in 0..4 {
let mut answer = None;
for i in 0..WINDOW {
// One frame in ten is a 30 ms chunk load, the rest are GPU-bound.
let cpu = if i % 10 == 0 { 30.0 } else { 0.8 };
answer = answer.or(auto.observe(cpu, Some(3.0), 3.0));
}
assert_eq!(answer, None);
}
assert!(!auto.on());
}
#[test]
fn reset_starts_over() {
let mut auto = GpuCullingAuto::new();
window(&mut auto, 5.0, 2.0);
window(&mut auto, 5.0, 2.0);
assert!(auto.on());
auto.reset();
assert!(!auto.on());
assert_eq!(auto.state(), "watching");
assert_eq!(window(&mut auto, 5.0, 2.0), None);
}
}
+36 -2
View File
@@ -17,11 +17,15 @@ pub struct BanDetails {
pub reason_message: Option<String>,
}
/// authlib's `UserProperties`, as far as the title screen reads it.
/// authlib's `UserProperties`, as far as the title screen and the friends service read it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UserProperties {
/// `UserFlag.SERVERS_ALLOWED` (`privileges.multiplayerServer.enabled`).
pub servers_allowed: bool,
/// `UserFlag.FRIENDS_ENABLED` (`friendsPreferences.friends`).
pub friends_enabled: bool,
/// `UserFlag.ACCEPT_FRIEND_INVITES` (`friendsPreferences.acceptInvites`).
pub accept_friend_invites: bool,
/// `bannedScopes.get("MULTIPLAYER")`.
pub multiplayer_ban: Option<BanDetails>,
}
@@ -30,6 +34,8 @@ impl Default for UserProperties {
fn default() -> Self {
Self {
servers_allowed: true,
friends_enabled: false,
accept_friend_invites: false,
multiplayer_ban: None,
}
}
@@ -209,6 +215,13 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
.pointer("/privileges/multiplayerServer/enabled")
.and_then(|e| e.as_bool())
.unwrap_or(false);
let toggle = |name: &str| {
v.pointer(&format!("/friendsPreferences/{name}"))
.and_then(|f| f.as_str())
.is_some_and(|f| f.eq_ignore_ascii_case("ENABLED"))
};
let friends_enabled = toggle("friends");
let accept_friend_invites = toggle("acceptInvites");
let multiplayer_ban = v
.pointer("/banStatus/bannedScopes/MULTIPLAYER")
.filter(|b| b.is_object())
@@ -222,6 +235,8 @@ pub fn parse_properties(body: &str) -> Option<UserProperties> {
});
Some(UserProperties {
servers_allowed,
friends_enabled,
accept_friend_invites,
multiplayer_ban,
})
}
@@ -275,6 +290,24 @@ mod tests {
assert!(clean.servers_allowed && clean.multiplayer_ban.is_none());
}
#[test]
fn friends_preferences_read_their_two_flags() {
let body = r#"{"privileges":{"multiplayerServer":{"enabled":true}},
"friendsPreferences":{"friends":"ENABLED","acceptInvites":"DISABLED"}}"#;
let p = parse_properties(body).unwrap();
assert!(p.friends_enabled);
assert!(!p.accept_friend_invites);
let other = parse_properties(
r#"{"friendsPreferences":{"friends":"DISABLED","acceptInvites":"ENABLED"}}"#,
)
.unwrap();
assert!(!other.friends_enabled);
assert!(other.accept_friend_invites);
// The offline fallback has neither flag, as `OFFLINE_PROPERTIES` does.
assert!(!UserProperties::default().friends_enabled);
assert!(!UserProperties::default().accept_friend_invites);
}
#[test]
fn parses_profile_actions() {
let a = parse_profile_actions(
@@ -297,6 +330,7 @@ mod tests {
properties: UserProperties {
servers_allowed: true,
multiplayer_ban: Some(ban.clone()),
..Default::default()
},
actions: ProfileActions {
forced_name_change: true,
@@ -317,7 +351,7 @@ mod tests {
let no_servers = AccountStatus {
properties: UserProperties {
servers_allowed: false,
multiplayer_ban: None,
..Default::default()
},
..Default::default()
};
+59 -1
View File
@@ -1,6 +1,7 @@
//! Biome colours from the pinned biome definitions and colormaps
//! (`Biome.getGrassColor`/`getFoliageColor`/..., `BlockTintSources`).
use crate::Result;
use horizon_client_core::particle_type::ParticleType;
use horizon_core::{noise::SimplexNoise, position::BlockPos, random::LegacyRandom};
use serde_json::Value;
use std::{
@@ -106,11 +107,31 @@ pub struct BiomeInfo {
/// The biome's own fog colour attribute; otherwise the dimension's applies.
pub fog: Option<u32>,
pub water_fog: u32,
/// `minecraft:visual/ambient_particles` entries and their probabilities.
pub ambient_particles: Vec<(ParticleType, f32)>,
/// `BiomeGenerationSettings.features`: the placed feature ids of each
/// `GenerationStep.Decoration` step, in order.
pub features: Vec<Vec<String>>,
/// The biome's environment attributes (`Biome.getAttributes`): its layer in the
/// environment attribute stack.
pub attributes: horizon_world::environment_attributes::AttributeMap,
}
impl BiomeInfo {
fn ambient_particles(v: &Value) -> Vec<(ParticleType, f32)> {
let list = &v["attributes"]["minecraft:visual/ambient_particles"];
let entries = list["argument"].as_array().or_else(|| list.as_array());
entries
.into_iter()
.flatten()
.filter_map(|entry| {
Some((
ParticleType::from_name(entry["particle"]["type"].as_str()?)?,
entry["probability"].as_f64().unwrap_or(0.0) as f32,
))
})
.collect()
}
/// `Biome.getHeightAdjustedTemperature`: the frozen modifier, then cooler
/// with height above `sea_level + 17`.
pub fn temperature_at(&self, pos: BlockPos, sea_level: i32) -> f32 {
@@ -245,6 +266,7 @@ impl BiomeInfo {
sky: color(&attributes["minecraft:visual/sky_color"]),
fog: color(&attributes["minecraft:visual/fog_color"]),
water_fog: color(&attributes["minecraft:visual/water_fog_color"]).unwrap_or(0x050533),
ambient_particles: Self::ambient_particles(v),
features: v["features"]
.as_array()
.into_iter()
@@ -258,6 +280,11 @@ impl BiomeInfo {
.collect()
})
.collect(),
attributes: horizon_world::environment_attributes::AttributeMap::parse(attributes)
.unwrap_or_else(|e| {
log::warn!("biome attributes: {e}");
Default::default()
}),
name,
}
}
@@ -374,6 +401,17 @@ impl Biomes {
pub fn get(&self, id: u16) -> &BiomeInfo {
&self.list[usize::from(id).min(self.list.len() - 1)]
}
/// The name of biome `id`, plains (as for an unknown name) when the id is
/// past this table: a column a server sent under its earlier, longer
/// registry (a proxy's previous server) keeps its ids until the next
/// login replaces it. `get` clamps such an id to the last entry instead,
/// so for that stale column the colours and this name can come from
/// different biomes until the login.
pub fn name(&self, id: u16) -> &str {
self.list
.get(usize::from(id))
.map_or("minecraft:plains", |b| b.name.as_str())
}
pub fn grass(&self, id: u16, x: f64, z: f64) -> u32 {
let b = self.get(id);
match b.modifier {
@@ -563,7 +601,9 @@ mod pack_tests {
#[test]
fn packs_replace_in_place_and_add_after_the_builtin_biomes() {
let base = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"}});
let red = json!({"temperature": 2.0, "effects": {"water_color": "#ff0000"},
"attributes": {"minecraft:visual/ambient_particles": {
"argument": [{"particle": {"type": "minecraft:ash"}, "probability": 0.25}]}}});
let hot = json!({"effects": {"grass_color": 0x123456}});
let files = [
("pack:zeta", &hot),
@@ -576,6 +616,9 @@ mod pack_tests {
assert_eq!(world.id("minecraft:river"), 2);
assert_eq!(world.get(2).water, 0xFF0000);
assert_eq!(world.get(2).temperature, 2.0);
assert_eq!(world.get(2).ambient_particles.len(), 1);
assert_eq!(world.get(2).ambient_particles[0].0.name(), "ash");
assert_eq!(world.get(2).ambient_particles[0].1, 0.25);
// New biomes follow, in registry order.
assert_eq!(world.id("minecraft:molten_peaks"), 3);
assert_eq!(world.id("pack:zeta"), 4);
@@ -600,6 +643,21 @@ mod pack_tests {
assert_eq!(other.get(2).water, 0x3F76E4);
assert!(!other.has_packs());
}
/// A column keeps the ids of the table it was decoded with. When a
/// proxy's next server lacks a biome the last one added, that id is past
/// the next table, and it names plains instead of indexing past the end
/// (#12148: "the len is 67 but the index is 67").
#[test]
fn an_id_past_the_table_names_plains() {
let next = builtin(&["minecraft:meadow", "minecraft:plains", "minecraft:river"]);
let last = next.with_packs([("limbo:void", &json!({}))]);
let stale = last.id("limbo:void");
assert_eq!((stale, last.name(stale)), (3, "limbo:void"));
assert_eq!(next.name(stale), "minecraft:plains");
assert_eq!(next.name(u16::MAX), "minecraft:plains");
assert_eq!(next.name(2), "minecraft:river");
}
}
#[cfg(test)]
@@ -77,6 +77,9 @@ pub struct BlockEntities {
/// The fluid tanks of mod blocks' block entities, by block state id;
/// built with `states`, like the containers.
mod_fluids: FxHashMap<u32, Arc<ModFluidTanks>>,
/// State-aware native factory ownership. Shared across level snapshots;
/// an empty slice means the declarations have not yet been compiled.
stateful_entities: Arc<[bool]>,
/// The energy buffers of mod blocks' block entities, by block state id;
/// built with `states`, like the containers.
mod_energy: FxHashMap<u32, Arc<ModEnergyBuffer>>,
@@ -209,6 +212,21 @@ impl BlockEntities {
pub fn mod_fluids(&self, state: u32) -> Option<&Arc<ModFluidTanks>> {
self.mod_fluids.get(&state)
}
pub fn set_stateful_entities(&mut self, states: Vec<bool>) {
self.stateful_entities = states.into();
}
pub fn has_stateful_entity_table(&self) -> bool {
!self.stateful_entities.is_empty()
}
pub fn stateful_entities(&self) -> Arc<[bool]> {
self.stateful_entities.clone()
}
pub fn stateful_entity(&self, state: u32) -> bool {
self.stateful_entities
.get(state as usize)
.copied()
.unwrap_or(false)
}
pub fn set_mod_energy(&mut self, energy: FxHashMap<u32, Arc<ModEnergyBuffer>>) {
self.mod_energy = energy;
}
@@ -3,20 +3,22 @@
//! decorated pots, conduits, enchanting-table and lectern books and copper
//! golem statues, End portals and End gateways. Models are vanilla's baked `LayerDefinitions` layers
//! (`entity-models.json`); transforms, animation and textures follow each
//! vanilla renderer. Faces are lit like the `entity` shader: the lightmap at
//! the block plus `minecraft_mix_light` with the level's two diffuse lights.
//! vanilla renderer. Faces carry their tint and world normal: the entity
//! shader (and a shader pack) applies `minecraft_mix_light` from the normal, as
//! vanilla's does.
use crate::{
Result,
bidi::visual,
block_entity_anim::{self, EnchantingBook},
breaking::entity_geometry,
fast_hash::FxHashMap,
font::Glyph,
font::{Font, Glyph},
gfx::mark_oit,
hud::Rect,
mesher::{CULL_BACK, Vertex},
models::load_visibility,
signs::{self, SignData, SignText},
text::{Style, Text},
world::World,
};
use glam::{DVec3, Mat4, Quat, Vec3};
@@ -128,6 +130,14 @@ fn pack_color(rgb: u32, alpha: u32, brightness: f32) -> u32 {
let c = |shift: u32| (((rgb >> shift) & 255) as f32 * brightness) as u32;
alpha << 24 | c(0) << 16 | c(8) << 8 | c(16)
}
/// A world normal as `Vertex::normal` holds it: RGBA8_SNORM, `w` positive so
/// the entity shader and a shader pack both shade by it. Block-entity models
/// carry no normals of their own (vanilla computes each face's from its
/// polygon, then `minecraft_mix_light` uses it in the shader).
fn pack_normal(normal: Vec3) -> [i8; 4] {
let c = |v: f32| (v.clamp(-1.0, 1.0) * 127.0).round() as i8;
[c(normal.x), c(normal.y), c(normal.z), 127]
}
/// Everything a draw call needs besides the geometry.
#[derive(Clone, Copy)]
@@ -165,6 +175,7 @@ pub fn draw_part(
light,
lights,
glow,
..
} = paint;
for poly in &part.polygons {
let pos = poly.map(|v| m.transform_point3(Vec3::new(v[0], v[1], v[2]) / 16.0));
@@ -177,11 +188,21 @@ pub fn draw_part(
} else {
normal
};
let color = pack_color(tint, 255, diffuse(normal, lights));
// The colour is the tint alone when the caller shades by the level's
// lights: the entity shader applies `minecraft_mix_light` from the
// normal, as vanilla does, so a shader pack receives the unshaded
// colour Iris gives it (a baked shade would be applied a second time
// by the pack's lighting). Other lights (an item preview's
// `Lighting.Entry`) are not the shader's to know, so those stay baked
// into the colour, as before.
let baked = !(lights == diffuse_lights(false) || lights == diffuse_lights(true));
let brightness = if baked { diffuse(normal, lights) } else { 1.0 };
let color = pack_color(tint, 255, brightness);
let normal = if baked { [0; 4] } else { pack_normal(normal) };
for (r, light) in std::iter::once((r, light)).chain(glow.map(|g| (g, FULL_BRIGHT))) {
for (v, position) in poly.iter().zip(pos) {
out.push(Vertex {
normal: [0; 4],
normal,
pos: position.to_array(),
uv: [r.u0 + (r.u1 - r.u0) * v[3], r.v0 + (r.v1 - r.v0) * v[4]],
layer: 0,
@@ -600,30 +621,26 @@ fn frac(x: f32) -> f32 {
x - x.floor()
}
/// `StringSplitter.splitLines(...).get(0)` for plain text: the first line,
/// wrapped at the last space that fits or mid-word when none does.
fn first_line(line: &str, max_width: f32, advance: &dyn Fn(char) -> f32) -> String {
/// `StringSplitter.findLineBreak`: first line, with styled advances.
fn first_line(line: &Text, max_width: f32, advance: &dyn Fn(char, &Style) -> f32) -> Text {
let mut width = 0.0;
let mut visible = false;
let mut last_space = None;
for (i, c) in line.char_indices() {
for (i, (c, style)) in line.chars().enumerate() {
if c == '\n' {
return line[..i].to_owned();
return line.slice(0, i);
}
if c == ' ' {
last_space = Some(i);
}
width += advance(c);
if width > max_width {
let end = match last_space {
Some(s) if s > 0 => s,
_ if i > 0 => i,
// The first char alone is too wide: take that one char.
_ => c.len_utf8(),
};
return line[..end].to_owned();
let step = advance(c, style);
width += step;
if visible && width > max_width.max(1.0) {
return line.slice(0, last_space.unwrap_or(i));
}
visible |= step != 0.0;
}
line.to_owned()
line.clone()
}
/// `AbstractSignRenderer.isOutlineVisible`: the camera within 16 blocks of
@@ -641,7 +658,7 @@ fn sign_text_near(pos: [i32; 3], camera: DVec3) -> bool {
/// `AbstractSignRenderer.submitSignText`: four centred lines, glowing text
/// full-bright with an 8x outline in the dark colour. The font colours each
/// glyph by its style (`Font.StringRenderOutput`): a line's own colour wins,
/// else the sign's. A line is one call per run of one colour.
/// else the sign's. Keep style boundaries for glyphs and decoration effects.
fn sign_text_calls(
scene: &Scene,
data: &SignData,
@@ -653,27 +670,43 @@ fn sign_text_calls(
let line_height = data.line_height();
let midpoint = (4.0 * line_height / 2.0).floor();
let dark = 0xFF00_0000 | text.dark_color();
let (color, outline, light) = if text.glowing {
let (color, outline, light, tier) = if text.glowing {
let c = text.text_color();
(
0xFF00_0000 | c,
if c == 0 || near { dark } else { 0 },
240 | 240 << 8,
crate::mesher::LOW,
)
} else {
(dark, 0, vertex_light(packed))
(dark, 0, vertex_light(packed), 0)
};
let advance = |c: char, style: &Style| {
let g = scene
.font
.map_or_else(|| (scene.glyph)(c), |f| f.glyph_styled(c, style));
g.advance + if style.is_bold() { g.offset } else { 0.0 }
};
let advance = |c: char| (scene.glyph)(c).advance;
let mut calls: Vec<TextCall> = Vec::with_capacity(text.lines.len());
for i in 0..text.lines.len() {
// The line that fits, in the order it is drawn (getVisualOrder).
let line = text.line_text(i);
let line = line.resolved();
let flat: String = line.chars().map(|(c, _)| c).collect();
let fits = first_line(&flat, data.max_line_width(), &advance);
let shown = line.slice(0, fits.chars().count());
let shown = if let Some(font) = scene.font {
font.split_formatted(&line, data.max_line_width())
.into_iter()
.next()
.unwrap_or_default()
.resolved()
.into_owned()
} else {
first_line(&line.resolved(), data.max_line_width(), &advance)
};
let shown = visual(&shown);
let width = shown.chars().map(|(c, _)| advance(c)).sum::<f32>().ceil();
let width = shown
.chars()
.map(|(c, s)| advance(c, s))
.sum::<f32>()
.ceil();
let x = -((width as i32) / 2) as f32;
let y = i as f32 * line_height - midpoint;
let first = calls.len();
@@ -683,19 +716,22 @@ fn sign_text_calls(
.style
.color
.map_or(color, |rgb| 0xFF00_0000 | (rgb & 0xFF_FFFF));
match calls[first..].last_mut() {
Some(last) if last.color == run => last.text.push_str(&span.text),
_ => calls.push(TextCall {
matrix: m,
x: x + pen,
y,
text: span.text.clone(),
color: run,
outline,
light,
}),
}
pen += span.text.chars().map(advance).sum::<f32>();
calls.push(TextCall {
matrix: m,
x: x + pen,
y,
text: span.text.clone(),
style: span.style.clone(),
color: run,
outline,
light,
tier,
});
pen += span
.text
.chars()
.map(|c| advance(c, &span.style))
.sum::<f32>();
}
if calls.len() == first {
// A line with no text is still a call, of no glyphs.
@@ -704,15 +740,28 @@ fn sign_text_calls(
x,
y,
text: String::new(),
style: Style::default(),
color,
outline,
light,
tier,
});
}
}
calls
}
fn sign_text_animated(data: &SignData) -> bool {
[&data.front, &data.back].into_iter().any(|side| {
(0..side.lines.len()).any(|i| {
side.line_text(i)
.resolved()
.chars()
.any(|(c, s)| c != ' ' && s.obfuscated == Some(true))
})
})
}
/// How far the text of an outlined call sits in front of its outline, in
/// text units (`Font.DisplayMode.POLYGON_OFFSET`, see `draw_text`).
const TEXT_OVER_OUTLINE: f32 = 0.05;
@@ -726,72 +775,131 @@ const TEXT_OVER_OUTLINE: f32 = 0.05;
/// reader instead (a twentieth of a font pixel, invisible) rather than
/// z-fighting with the outline under it.
fn draw_text(scene: &Scene, call: &TextCall, out: &mut Vec<Vertex>) {
text_vertices(scene.glyph, call, out);
let glyph = |c| {
let Some(font) = scene.font else {
return (scene.glyph)(c);
};
let g = font.glyph_styled(c, &call.style);
if call.style.obfuscated == Some(true) && c != ' ' {
font.random_glyph(&call.style, g.advance)
} else {
g
}
};
text_vertices(&glyph, scene.font.map(Font::white), call, out);
}
/// `draw_text` with the font's glyph cells and advances from `glyph`: the
/// `BakedSheetGlyph.renderChar` / `Font.PreparedTextBuilder.accept`: the
/// text of `call` as glyph quads in its matrix's plane, 1 unit a font pixel,
/// y down as the font's, with its outline when it has one. A native mod's
/// block entity draws its text this way (`native_draw_text`).
pub fn text_vertices(glyph: &dyn Fn(char) -> Glyph, call: &TextCall, out: &mut Vec<Vertex>) {
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32| {
let mut x = call.x + dx;
let y0 = call.y + dy;
for ch in call.text.chars() {
pub fn text_vertices(
glyph: &dyn Fn(char) -> Glyph,
white: Option<Rect>,
call: &TextCall,
out: &mut Vec<Vertex>,
) {
let bold = call.style.is_bold();
let mut quad = |corners: [(f32, f32); 4], cell: Rect, z: f32, argb: u32| {
let uvs = [
[cell.u0, cell.v1],
[cell.u1, cell.v1],
[cell.u1, cell.v0],
[cell.u0, cell.v0],
];
let color = 0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
for ((x, y), uv) in corners.into_iter().zip(uvs) {
out.push(Vertex {
normal: [0; 4],
pos: call.matrix.transform_point3(Vec3::new(x, y, z)).to_array(),
uv,
layer: u32::from(call.tier) << 16,
color,
light: call.light,
glint_uv: [0.0; 2],
});
}
};
let mut emit = |dx: f32, dy: f32, z: f32, argb: u32, outline: bool| {
let mut pen = call.x;
for (i, ch) in call.text.chars().enumerate() {
let g = glyph(ch);
let x = pen + dx * g.offset;
let y = call.y + dy * g.offset;
let advance = g.advance + if bold { g.offset } else { 0.0 };
if let Some(cell) = g.rect {
// BakedSheetGlyph: the glyph's own box from the pen, not a
// fixed 8x8 cell (a Unifont glyph is 0.5 units per texel).
let (x0, x1) = (x + g.left, x + g.right);
let (top, bottom) = (y0 + g.top, y0 + g.bottom);
let corners = [(x0, bottom), (x1, bottom), (x1, top), (x0, top)];
let uvs = [
[cell.u0, cell.v1],
[cell.u1, cell.v1],
[cell.u1, cell.v0],
[cell.u0, cell.v0],
];
let c =
0xFF00_0000 | (argb & 255) << 16 | (argb >> 8 & 255) << 8 | (argb >> 16 & 255);
for ((cx, cy), uv) in corners.into_iter().zip(uvs) {
out.push(Vertex {
normal: [0; 4],
pos: call
.matrix
.transform_point3(Vec3::new(cx, cy, z))
.to_array(),
uv,
layer: 0,
color: c,
light: call.light,
glint_uv: [0.0, 0.0],
});
let thick = if bold { 0.1 } else { 0.0 };
let shear = |v| {
if call.style.is_italic() {
1.0 - 0.25 * v
} else {
0.0
}
};
for copy in 0..=usize::from(bold) {
let x0 = x + copy as f32 * g.offset + g.left - thick;
let x1 = x + copy as f32 * g.offset + g.right + thick;
let top = y + g.top - thick;
let bottom = y + g.bottom + thick;
quad(
[
(x0 + shear(g.bottom), bottom),
(x1 + shear(g.bottom), bottom),
(x1 + shear(g.top), top),
(x0 + shear(g.top), top),
],
cell,
z + copy as f32 * 0.001,
argb,
);
}
}
x += g.advance;
// `Font.prepare8xTextOutline` discards effects: only the front
// text gets underline/strikethrough, including over spaces.
if !outline && let Some(white) = white {
let x0 = if i == 0 { x - 1.0 } else { x };
for (enabled, top) in [
(call.style.strikethrough, 3.5),
(call.style.underlined, 8.0),
] {
if enabled == Some(true) {
quad(
[
(x0, y + top + 1.0),
(x + advance, y + top + 1.0),
(x + advance, y + top),
(x0, y + top),
],
white,
z + 0.01,
argb,
);
}
}
}
pen += advance;
}
};
if call.outline != 0 {
for (dx, dy) in [
(-1.0, -1.0),
(0.0, -1.0),
(1.0, -1.0),
(-1.0, 0.0),
(1.0, 0.0),
(-1.0, 1.0),
(0.0, 1.0),
(1.0, 1.0),
] {
emit(dx, dy, 0.0, call.outline);
for dx in -1..=1 {
for dy in -1..=1 {
if dx != 0 || dy != 0 {
emit(dx as f32, dy as f32, 0.0, call.outline, true);
}
}
}
}
let z = if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
};
emit(0.0, 0.0, z, call.color);
emit(
0.0,
0.0,
if call.outline != 0 {
TEXT_OVER_OUTLINE
} else {
0.0
},
call.color,
false,
);
}
/// Block-entity fields the level ticks, at one position (vanilla's own field
@@ -824,12 +932,16 @@ pub struct TextCall {
pub x: f32,
pub y: f32,
pub text: String,
pub style: Style,
/// ARGB.
pub color: u32,
/// ARGB outline colour, 0 for none.
pub outline: u32,
/// Horizon packed light (sky*16 | block*16 << 8).
pub light: u32,
/// HDR emission tier of its glyphs (`mesher::hdr_tier`), 0 for none: glowing sign text
/// shines in HDR output and in shader packs (`horizonTier`), as full-bright text does in SDR.
pub tier: u8,
}
/// Inputs from the game for one frame of block-entity rendering.
@@ -868,6 +980,8 @@ pub struct Scene<'a> {
pub skull: &'a dyn Fn([i32; 3]) -> Option<String>,
/// Font glyph: atlas cell (none for blank glyphs such as the space), box and advance.
pub glyph: &'a dyn Fn(char) -> Glyph,
/// Font providers, effects and random glyphs for styled sign text.
pub font: Option<&'a Font>,
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
/// text laid out under another value is laid out again.
pub glyphs: u64,
@@ -1029,6 +1143,7 @@ struct SignTextEntry {
/// Game time it was last drawn.
used: i64,
vertices: Vec<Vertex>,
animated: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct StaticKey {
@@ -2335,9 +2450,11 @@ impl BlockEntityRenderer {
let entry = match signs.entry(pos) {
std::collections::hash_map::Entry::Occupied(mut occupied) => {
let entry = occupied.get_mut();
let changed = entry.data != *data;
if (entry.state, entry.packed, entry.near, entry.glyphs)
!= (state, packed, near, glyphs)
|| entry.data != *data
|| changed
|| entry.animated
{
entry.vertices.clear();
self.render_one(
@@ -2352,7 +2469,10 @@ impl BlockEntityRenderer {
&mut entry.vertices,
);
entry.state = state;
entry.data.clone_from(data);
if changed {
entry.animated = sign_text_animated(data);
entry.data.clone_from(data);
}
entry.packed = packed;
entry.near = near;
entry.glyphs = glyphs;
@@ -2373,6 +2493,7 @@ impl BlockEntityRenderer {
&mut vertices,
);
vacant.insert(SignTextEntry {
animated: sign_text_animated(data),
state,
data: data.clone(),
packed,
@@ -3612,6 +3733,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// Compare the cached world path with the original model renderer,
@@ -3774,6 +3896,105 @@ mod tests {
// Straight down: ambient only.
assert!((diffuse(-Vec3::Y, lights) - 0.4).abs() < 1e-6);
}
/// A face's colour carries its tint alone and its normal carries the
/// shading: the entity shader applies `minecraft_mix_light` from it, as a
/// shader pack's lighting does, so a baked shade would be applied twice.
#[test]
fn a_face_keeps_its_tint_and_normal_for_the_shader_to_shade() {
let quad = |y: f32, z0: f32, z1: f32| {
[
[0.0, y, z0, 0.0, 0.0],
[0.0, y, z1, 0.0, 1.0],
[16.0, y, z1, 1.0, 1.0],
[16.0, y, z0, 1.0, 0.0],
]
};
// Opposite windings: the first faces up, the second down.
let part = ModelPart {
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
polygons: vec![quad(0.0, 0.0, 16.0), quad(16.0, 16.0, 0.0)],
children: vec![],
};
let mut out = Vec::new();
draw_part(
&part,
"root",
Mat4::IDENTITY,
&|_, _| {},
Paint {
sprite: Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 16,
},
tint: 0x123456,
light: 0,
lights: diffuse_lights(false),
glow: None,
},
&mut out,
);
assert_eq!(out.len(), 8);
// The same tint on both faces: no directional shade baked in (`pack_color`
// stores the tint in the vertex colour's BGR byte order).
let tint = pack_color(0x12_3456, 255, 1.0);
assert!(out.iter().all(|v| v.color == tint));
let (up, down) = (&out[0], &out[4]);
assert_eq!(up.normal, [0, 127, 0, 127]);
assert_eq!(down.normal, [0, -127, 0, 127]);
// The shader still gets the difference the old baked colours held.
let lights = diffuse_lights(false);
let up_normal = Vec3::new(0.0, f32::from(up.normal[1]) / 127.0, 0.0);
let down_normal = Vec3::new(0.0, f32::from(down.normal[1]) / 127.0, 0.0);
assert!(diffuse(up_normal, lights) > diffuse(down_normal, lights));
}
/// Lights the entity shader does not know (an item preview's
/// `Lighting.Entry`) stay baked into the colour, and `normal.w` 0 tells
/// the shader to keep it: `game/item_draw` passes those for block-entity
/// items in inventories and hands.
#[test]
fn lights_other_than_the_levels_stay_baked() {
let part = ModelPart {
pose: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
polygons: vec![[
[0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 16.0, 0.0, 1.0],
[16.0, 0.0, 16.0, 1.0, 1.0],
[16.0, 0.0, 0.0, 1.0, 0.0],
]],
children: vec![],
};
let lights = (Vec3::X, Vec3::Z);
let mut out = Vec::new();
draw_part(
&part,
"root",
Mat4::IDENTITY,
&|_, _| {},
Paint {
sprite: Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 16,
},
tint: 0x12_3456,
light: 0,
lights,
glow: None,
},
&mut out,
);
let baked = pack_color(0x12_3456, 255, diffuse(Vec3::Y, lights));
assert!(out.iter().all(|v| v.color == baked));
assert!(out.iter().all(|v| v.normal == [0; 4]));
assert_ne!(baked, pack_color(0x12_3456, 255, 1.0));
}
#[test]
fn part_poses_compose_like_model_part() {
let part = ModelPart {
@@ -3907,6 +4128,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let mut call = TextCall {
@@ -3914,9 +4136,11 @@ mod tests {
x: 0.0,
y: 0.0,
text: "Hi".into(),
style: Style::default(),
color: 0xFFFF_FF00,
outline: 0xFF33_3300,
light: 240 | 240 << 8,
tier: 0,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
@@ -3980,6 +4204,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let call = TextCall {
@@ -3987,9 +4212,11 @@ mod tests {
x: 10.0,
y: 2.0,
text: "ab".into(),
style: Style::default(),
color: 0xFF00_0000,
outline: 0,
light: 240 | 240 << 8,
tier: 0,
};
let mut out = Vec::new();
draw_text(&scene, &call, &mut out);
@@ -4009,6 +4236,83 @@ mod tests {
assert_eq!(boxes, [[10.0, 14.0, 2.0, 10.0], [15.0, 19.0, 2.0, 10.0]]);
}
#[test]
fn styled_sign_quads_follow_vanilla_glyph_and_effect_bounds() {
let white = Rect {
u0: 0.0,
v0: 0.0,
u1: 0.1,
v1: 0.1,
w: 1,
h: 1,
};
let glyph = |c| Glyph {
rect: (c != ' ').then_some(white),
left: 0.0,
right: 4.0,
top: 0.0,
bottom: 8.0,
advance: 5.0,
offset: 0.5,
};
let mut call = TextCall {
matrix: Mat4::IDENTITY,
x: 0.0,
y: 0.0,
text: "A ".into(),
color: 0xff123456,
outline: 0,
light: 240,
tier: crate::mesher::LOW,
style: Style {
bold: Some(true),
italic: Some(true),
underlined: Some(true),
strikethrough: Some(true),
..Default::default()
},
};
let mut out = Vec::new();
text_vertices(&glyph, Some(white), &call, &mut out);
// BakedSheetGlyph: bold expands by 0.1, italic shears by 1 - y/4,
// the second bold glyph is 0.5 along and 0.001 in front (Unihex).
assert_eq!(out.len(), 24);
for (got, expected) in out[0].pos.iter().zip([-1.1, 8.1, 0.0]) {
assert!((got - expected).abs() < 1e-6);
}
assert!((out[4].pos[0] - out[0].pos[0] - 0.5).abs() < 1e-6);
assert_eq!(out[4].pos[2], 0.001);
assert_eq!(out[8].pos, [-1.0, 4.5, 0.01]);
assert_eq!(out[12].pos, [-1.0, 9.0, 0.01]);
assert_eq!(out[21].pos, [11.0, 9.0, 0.01]); // underline crosses the space
assert!(out.iter().all(|v| v.color == 0xff563412 && v.light == 240));
call.outline = 0xff010203;
out.clear();
text_vertices(&glyph, Some(white), &call, &mut out);
// Eight outlines contain glyphs only; effects appear once, in front.
assert_eq!(out.len(), 8 * 8 + 24);
assert!((out[0].pos[0] - -1.6).abs() < 1e-6);
assert!(out[64..].iter().all(|v| v.pos[2] >= TEXT_OVER_OUTLINE));
// The glow tier reaches bold copies, outlines and decoration quads.
assert!(out.iter().all(|v| v.layer >> 16 == 1));
call.tier = 0;
out.clear();
text_vertices(&glyph, Some(white), &call, &mut out);
assert!(out.iter().all(|v| v.layer >> 16 == 0));
}
#[test]
fn styled_sign_clipping_counts_bold_width_and_preserves_resets() {
let line = Text::legacy("§lAB§r東Z");
let advance = |_: char, s: &Style| 5.0 + if s.is_bold() { 0.5 } else { 0.0 };
let shown = first_line(&line, 16.0, &advance);
assert_eq!(shown.plain(), "AB東");
assert_eq!(
shown.chars().map(|(_, s)| s.is_bold()).collect::<Vec<_>>(),
[true, true, false]
);
}
/// The front text calls of a sign whose every glyph advances 6.
fn front_calls(data: &SignData, near: bool) -> Vec<(String, f32, f32, u32, u32)> {
let world = World::new(BlockStateId(0), 1);
@@ -4037,6 +4341,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
sign_text_calls(&scene, data, &data.front, Mat4::IDENTITY, 0, near)
@@ -4176,7 +4481,9 @@ mod tests {
..changed.clone()
};
let wide = std::cell::Cell::new(6.0);
let glyph_reads = std::cell::Cell::new(0);
let glyph = |c: char| {
glyph_reads.set(glyph_reads.get() + 1);
let u = f32::from(c as u8 % 16) / 16.0;
let rect = Rect {
u0: u,
@@ -4219,6 +4526,7 @@ mod tests {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
@@ -4262,6 +4570,23 @@ mod tests {
wide.set(5.0);
scene.glyphs = 1;
check(&scene, pos, 1);
// With the same game time and data, plain text reuses its vertices,
// but obfuscated text must ask the font for fresh glyphs each frame.
let mut vertices = Vec::new();
let reads = glyph_reads.get();
renderer.render(&scene, [(pos, 1)].iter(), &mut vertices);
assert_eq!(glyph_reads.get(), reads);
let animated = SignData {
front: text("§kSecret", false),
..Default::default()
};
let animated_sign = |_| Some(&animated);
scene.sign = &animated_sign;
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
let reads = glyph_reads.get();
renderer.render(&scene, [(pos, 0)].iter(), &mut vertices);
assert!(glyph_reads.get() > reads);
scene.sign = &first_sign;
// Signs out of view for a while are dropped.
scene.game_time = 2 * SIGN_TEXT_KEEP;
check(&scene, pos, 0);
@@ -4269,11 +4594,23 @@ mod tests {
}
#[test]
fn first_line_wraps_on_bytes_without_splitting_a_char() {
let advance = |c: char| if c == ' ' { 4.0 } else { 6.0 };
assert_eq!(first_line("ab cd ef", 13.0, &advance), "ab");
assert_eq!(first_line("abcdef", 13.0, &advance), "ab");
assert_eq!(first_line("\u{6771}", 3.0, &advance), "\u{6771}");
assert_eq!(first_line("ab\ncd", 100.0, &advance), "ab");
let advance = |c: char, _: &Style| if c == ' ' { 4.0 } else { 6.0 };
assert_eq!(
first_line(&Text::literal("ab cd ef"), 13.0, &advance).plain(),
"ab"
);
assert_eq!(
first_line(&Text::literal("abcdef"), 13.0, &advance).plain(),
"ab"
);
assert_eq!(
first_line(&Text::literal("\u{6771}"), 3.0, &advance).plain(),
"\u{6771}"
);
assert_eq!(
first_line(&Text::literal("ab\ncd"), 100.0, &advance).plain(),
"ab"
);
}
/// `rendertype_end_portal` culls back faces, and `AbstractEndPortalRenderer`
/// submits the unit cube's two Y faces. Both are wound outward, so of each
@@ -4377,6 +4714,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let mut verts = Vec::new();
@@ -4463,6 +4801,7 @@ mod tests {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
@@ -358,6 +358,7 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
// Reverse local-position order to check that cache indexing does not
@@ -447,6 +448,7 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -502,6 +504,7 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([100, 70, -32], 0)];
@@ -689,6 +692,7 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
skull: &|_| None,
glyph: &glyph,
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -743,6 +747,7 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -845,6 +850,7 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -900,6 +906,7 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -962,6 +969,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -993,7 +1001,9 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
scene.nether = true;
let nether_closed = render_cached(&scene);
assert_same_vertices(&nether_closed, &render_direct(&renderer, &scene, &blocks));
assert_vertices_differ(&closed_again, &nether_closed);
// The nether is the entity shader's flag now (`entity_light`'s `l1` and
// `globals.time.y`), not a baked colour, so the cached geometry serves both.
assert_same_vertices(&closed_again, &nether_closed);
}
#[test]
@@ -1058,6 +1068,7 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pos = [0, 64, 0];
@@ -1147,6 +1158,7 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
@@ -1223,6 +1235,7 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -1302,6 +1315,7 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let blocks = [([0, 64, 0], 0)];
@@ -1369,6 +1383,7 @@ fn cached_wall_banner_poles_use_the_wall_model() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let pos = [0, 64, 0];
@@ -1417,6 +1432,7 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
@@ -1523,6 +1539,7 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
skull: &|_| None,
glyph: &|_| Glyph::default(),
glyphs: 0,
font: None,
dynamic_visible: None,
};
let options = CacheOptions {
+7 -7
View File
@@ -114,12 +114,8 @@ mod tests {
ambient_occlusion: true,
particle: None,
cube: None,
faces: vec![BakedFace {
layer: 0,
opaque: true,
translucent: false,
fusion: None,
quad: ModelQuad {
faces: vec![BakedFace::new(
ModelQuad {
positions: [
[0.0, 0.5, 0.0],
[0.0, 0.5, 1.0],
@@ -142,7 +138,11 @@ mod tests {
render_type: None,
shade: true,
},
}],
0,
true,
false,
None,
)],
}
}
#[test]
+123 -1
View File
@@ -37,6 +37,37 @@ pub fn potion_name_key(item: &str, potion: Option<&str>) -> String {
let suffix = potion.and_then(self::potion).map_or("empty", |p| p.name);
format!("item.minecraft.{prefix}.effect.{suffix}")
}
/// `PotionItem.getName` and `TippedArrowItem.getName`: a potion or a tipped
/// arrow is named by its contents (`PotionContents.getName`: their own
/// `custom_name`, else the potion's name, else `empty`), whatever its
/// `item_name`. None for other items, and for a stack whose contents were
/// taken off (`!minecraft:potion_contents`), which takes the item's name.
pub fn contents_name_key(stack: &ItemStack) -> Option<String> {
use horizon_world::nbt::Tag;
let item = &*stack.item;
let named = matches!(
item,
"minecraft:potion"
| "minecraft:splash_potion"
| "minecraft:lingering_potion"
| "minecraft:tipped_arrow"
);
let data = stack.data();
if !named || data.is_some_and(|d| d.other("!minecraft:potion_contents").is_some()) {
return None;
}
let prefix = item.trim_start_matches("minecraft:");
let own = data
.and_then(|d| d.potion_extra.iter().find(|(k, _)| k == "custom_name"))
.and_then(|(_, name)| match name {
Tag::String(s) => Some(s.to_string_lossy()),
_ => None,
});
Some(match own {
Some(suffix) => format!("item.minecraft.{prefix}.effect.{suffix}"),
None => potion_name_key(item, stack.potion()),
})
}
/// `PotionContents.getAllEffects`: the potion's effects, then the stack's
/// `custom_effects` (id, duration, amplifier). Unknown effect ids are dropped.
pub fn contents_effects(stack: &ItemStack) -> Vec<(&'static str, i32, i32)> {
@@ -387,8 +418,21 @@ impl BrewingStand {
out.brewed = true;
}
}
/// The `minecraft:brewing_fuel` providers of an item: (uses, speed_multiplier).
/// `ItemStack.get(BREWING_FUEL)`: the stack's providers (uses, speed_multiplier),
/// respecting component patches before falling back to the item's defaults.
pub fn brewing_fuel(items: &Items, stack: &ItemStack) -> Option<(Value, Value)> {
if let Some(data) = stack.data() {
if data.other("!minecraft:brewing_fuel").is_some() {
return None;
}
if let Some(own) = data.other("minecraft:brewing_fuel") {
let component = horizon_client_core::scoreboard::tag_to_json(own);
return Some((
component["uses"].clone(),
component["speed_multiplier"].clone(),
));
}
}
items.get(&stack.item).and_then(|i| i.brewing_fuel.clone())
}
@@ -396,6 +440,50 @@ pub fn brewing_fuel(items: &Items, stack: &ItemStack) -> Option<(Value, Value)>
mod tests {
use super::*;
#[test]
fn stack_fuel_controls_brew_duration_and_consumption() {
let items = Items::for_tests(Default::default());
let brewing = Brewing {
reagents: ["minecraft:nether_wart".to_owned()].into_iter().collect(),
inputs: HashSet::new(),
recipes: vec![BrewingRecipe {
id: "awkward".into(),
input: PotionIngredient {
items: ["minecraft:potion".to_owned()].into_iter().collect(),
potions: Some(["minecraft:water".to_owned()].into_iter().collect()),
},
reagent: PotionIngredient {
items: ["minecraft:nether_wart".to_owned()].into_iter().collect(),
potions: None,
},
output: ItemStack::new("minecraft:potion", 1).with_potion("minecraft:awkward"),
}],
};
let mut fuel = ItemStack::new("minecraft:stone", 2);
fuel.data_mut().set_loot_component(
"minecraft:brewing_fuel",
&serde_json::json!({"uses": 7, "speed_multiplier": 2.5}),
);
let mut stand = BrewingStand::new();
stand.items[0] = Some(ItemStack::new("minecraft:potion", 1).with_potion("minecraft:water"));
stand.items[3] = Some(ItemStack::new("minecraft:nether_wart", 1));
stand.items[4] = Some(fuel);
let providers = Providers::empty();
stand.tick(&brewing, &items, &providers);
assert_eq!((stand.brew_time, stand.fuel, stand.total_fuel), (160, 6, 7));
assert_eq!(stand.items[4].as_ref().unwrap().count, 1);
for _ in 1..160 {
assert!(!stand.tick(&brewing, &items, &providers).brewed);
}
assert!(stand.tick(&brewing, &items, &providers).brewed);
assert_eq!(
stand.items[0].as_ref().unwrap().potion(),
Some("minecraft:awkward")
);
assert_eq!(stand.fuel, 6);
assert_eq!(stand.items[4].as_ref().unwrap().count, 1);
}
#[test]
fn potion_colours_and_names() {
// Swiftness: speed colour 3402751 = 0x33EBFF.
@@ -412,6 +500,40 @@ mod tests {
potion_name_key("minecraft:splash_potion", Some("minecraft:long_swiftness")),
"item.minecraft.splash_potion.effect.swiftness"
);
// A potion or a tipped arrow is named by its contents, over its
// item_name; one whose contents were taken off is not.
use horizon_world::nbt::Tag;
let mut swift =
ItemStack::new("minecraft:potion", 1).with_potion("minecraft:long_swiftness");
swift
.data_mut()
.set_other("minecraft:item_name", Some(Tag::String("Bottle".into())));
assert_eq!(
contents_name_key(&swift).as_deref(),
Some("item.minecraft.potion.effect.swiftness")
);
assert_eq!(
contents_name_key(&ItemStack::new("minecraft:splash_potion", 1)).as_deref(),
Some("item.minecraft.splash_potion.effect.empty")
);
let mut own = ItemStack::new("minecraft:tipped_arrow", 1).with_potion("minecraft:poison");
own.data_mut()
.potion_extra
.push(("custom_name".into(), Tag::String("luck".into())));
assert_eq!(
contents_name_key(&own).as_deref(),
Some("item.minecraft.tipped_arrow.effect.luck")
);
let mut taken = ItemStack::new("minecraft:potion", 1);
taken.data_mut().set_other(
"!minecraft:potion_contents",
Some(Tag::compound(Vec::<(&str, Tag)>::new())),
);
assert_eq!(contents_name_key(&taken), None);
assert_eq!(
contents_name_key(&ItemStack::new("minecraft:stone", 1)),
None
);
assert_eq!(format_duration(9600), "08:00");
assert_eq!(format_duration(450), "00:22");
assert_eq!(format_duration(140), "00:07");
+54
View File
@@ -1188,6 +1188,18 @@ impl SuggestLayout {
};
}
/// A suggestion accepted by Tab or a click (`SuggestionsList.useSuggestion`).
#[derive(Debug, PartialEq, Eq)]
pub struct SuggestionCompletion {
/// The suggestion list's original input, also when cycling with Tab.
pub original: String,
/// The replaced range in `original`, as Java UTF-16 start/end offsets.
pub range: (usize, usize),
pub text: String,
/// The resulting input, after the text field's length limit.
pub input: String,
}
/// The open chat screen: vanilla's `ChatScreen` with its `EditBox` and
/// `CommandSuggestions` (argument colouring, usage lines, parse errors,
/// the suggestion list, Tab completion, history).
@@ -1211,6 +1223,7 @@ pub struct ChatScreen {
/// the command up to the cursor.
request: Option<(i32, String)>,
suggestions: Option<SuggestionList>,
completion: Option<SuggestionCompletion>,
usage: Vec<Text>,
usage_position: i32,
usage_width: i32,
@@ -1333,6 +1346,7 @@ impl ChatScreen {
awaiting: None,
request: None,
suggestions: None,
completion: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
@@ -1370,6 +1384,7 @@ impl ChatScreen {
awaiting: None,
request: None,
suggestions: None,
completion: None,
usage: Vec::new(),
usage_position: 0,
usage_width: 0,
@@ -1385,6 +1400,11 @@ impl ChatScreen {
pub fn value(&self) -> &str {
&self.input.value
}
/// Take the most recently accepted suggestion, at most once. Call after
/// each key or click to observe every acceptance, including Tab cycling.
pub fn take_suggestion_completion(&mut self) -> Option<SuggestionCompletion> {
self.completion.take()
}
/// Replace the input for a suggest-command link and refresh completions.
pub fn suggest_command(&mut self, m: &Measure, c: &Commands, command: &str) {
self.input.set_value(m, command);
@@ -1434,6 +1454,24 @@ impl ChatScreen {
self.edited(m, c);
}
}
/// Edit the existing input and refresh its command responder when it changes.
pub fn edit_input(&mut self, m: &Measure, c: &Commands, f: impl FnOnce(&mut TextField)) {
let before = (
self.input.value.clone(),
self.input.cursor,
self.input.highlight,
);
f(&mut self.input);
if before
!= (
self.input.value.clone(),
self.input.cursor,
self.input.highlight,
)
{
self.edited(m, c);
}
}
/// Typed text (`charTyped` per character).
pub fn type_text(&mut self, m: &Measure, c: &Commands, text: &str) {
for ch in text.chars() {
@@ -1733,6 +1771,17 @@ impl ChatScreen {
self.select(l, l.current as isize);
self.keep_suggestions = false;
l.tab_cycles = true;
let units = |byte: usize| {
l.original[..byte.min(l.original.len())]
.encode_utf16()
.count()
};
self.completion = Some(SuggestionCompletion {
original: l.original.clone(),
range: (units(s.range.0), units(s.range.1)),
text: s.text,
input: self.input.value.clone(),
});
}
/// `ChatScreen.keyPressed`.
pub fn key(
@@ -2022,6 +2071,11 @@ impl ChatScreen {
for i in 0..limit {
let y = ry + 12 * i as i32;
d::fill(out, rx, y, rx + rw, y + 12, self.layout.fill);
// A row for the pad: nothing else can pick a suggestion without Tab.
crate::widgets::target_as(
[rx as f32, y as f32, rw as f32, 12.0],
crate::widgets::Kind::Row,
);
if mouse.0 > rx && mouse.0 < rx + rw && mouse.1 > y && mouse.1 < y + 12 {
if moved {
self.select(l, (i + l.offset) as isize);
@@ -7,6 +7,85 @@ fn full() -> ChatAbilities {
ChatAbilities::new(ChatVisibility::Full)
}
#[test]
fn accepted_suggestions_report_utf16_ranges_and_tab_cycles() {
use winit::keyboard::KeyCode as K;
let font = font();
let m = Measure(&font);
let registries = Registries {
players: vec!["Steve".into(), "Steven".into()],
..Registries::default()
};
let c = Commands {
dispatcher: crate::command::vanilla::game_dispatcher(),
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.type_text(&m, &c, "😀 hi st");
assert!(screen.take_suggestion_completion().is_none());
screen.key(&m, &c, &[], K::Tab, false, false);
assert!(
screen.take_suggestion_completion().is_none(),
"opening is not accepting"
);
for (shift, text) in [(false, "Steve"), (false, "Steven"), (true, "Steve")] {
screen.key(&m, &c, &[], K::Tab, shift, false);
assert_eq!(screen.value(), format!("😀 hi {text}"));
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "😀 hi st".into(),
range: (6, 8),
text: text.into(),
input: format!("😀 hi {text}"),
})
);
assert!(
screen.take_suggestion_completion().is_none(),
"reported once"
);
}
screen.key(&m, &c, &[], K::ArrowDown, false, false);
assert!(
screen.take_suggestion_completion().is_none(),
"selection is not accepting"
);
screen.key(&m, &c, &[], K::Escape, false, false);
assert!(screen.take_suggestion_completion().is_none());
}
#[test]
fn clicked_suggestion_reports_the_actual_length_limited_input() {
use winit::keyboard::KeyCode as K;
let font = font();
let m = Measure(&font);
let registries = Registries {
players: vec!["Steve".into()],
..Registries::default()
};
let c = Commands {
dispatcher: crate::command::vanilla::game_dispatcher(),
registries: &registries,
};
let mut screen = ChatScreen::new(&m, &c, "", false, 0, 640, 360, full());
screen.input.max_length = 6;
screen.type_text(&m, &c, "hi st");
screen.key(&m, &c, &[], K::Tab, false, false);
let [x, y, _, _] = screen.suggestions.as_ref().unwrap().rect;
assert!(screen.click(&m, &c, f64::from(x + 1), f64::from(y + 1)));
assert_eq!(screen.value(), "hi Ste");
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "hi st".into(),
range: (3, 5),
text: "Steve".into(),
input: "hi Ste".into(),
})
);
assert!(screen.take_suggestion_completion().is_none());
}
fn font() -> Font {
// The missing-glyph metrics let these tests run without game assets.
Font::load(
@@ -483,8 +562,18 @@ fn ask_server_nodes_wait_for_the_servers_completions() {
tab(&mut screen);
assert_eq!(screen.value(), "/bossbar get pt");
screen.server_suggestions(&m, &c, request, (13, 2), reply("pt6:check"));
assert!(screen.take_suggestion_completion().is_none());
tab(&mut screen);
assert_eq!(screen.value(), "/bossbar get pt6:check");
assert_eq!(
screen.take_suggestion_completion(),
Some(SuggestionCompletion {
original: "/bossbar get pt".into(),
range: (13, 15),
text: "pt6:check".into(),
input: "/bossbar get pt6:check".into(),
})
);
// Without the server's provider the local list is done at once.
let local = crate::command::vanilla::game_dispatcher();
let c = Commands {
+34 -13
View File
@@ -357,6 +357,24 @@ impl Light {
}
/// The smallest form of packed light per block.
pub fn packed(light: Box<[u8; 4096]>) -> Self {
match Self::classify(&light) {
(false, false, first) => Self::Uniform(first),
(true, false, first) => Self::Sky(Self::pack_layer(&light, 4), first & 15),
(false, true, first) => Self::Block(Self::pack_layer(&light, 0), first >> 4),
(true, true, _) => Self::Full(light),
}
}
/// The smallest form of packed light per block, allocating a 4 KiB box only
/// when both sky and block light vary within the section.
pub fn from_slice(light: &[u8; 4096]) -> Self {
match Self::classify(light) {
(false, false, first) => Self::Uniform(first),
(true, false, first) => Self::Sky(Self::pack_layer(light, 4), first & 15),
(false, true, first) => Self::Block(Self::pack_layer(light, 0), first >> 4),
(true, true, _) => Self::Full(Box::new(*light)),
}
}
fn classify(light: &[u8; 4096]) -> (bool, bool, u8) {
let first = light[0];
let (mut sky_varies, mut block_varies) = (false, false);
for &v in light.iter() {
@@ -364,19 +382,14 @@ impl Light {
sky_varies |= d & 0xF0 != 0;
block_varies |= d & 15 != 0;
}
let layer = |shift: u32| {
let mut layer = Box::new([0u8; 2048]);
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
}
layer
};
match (sky_varies, block_varies) {
(false, false) => Self::Uniform(first),
(true, false) => Self::Sky(layer(4), first & 15),
(false, true) => Self::Block(layer(0), first >> 4),
(true, true) => Self::Full(light),
(sky_varies, block_varies, first)
}
fn pack_layer(light: &[u8; 4096], shift: u32) -> Box<[u8; 2048]> {
let mut layer = Box::new([0u8; 2048]);
for (out, pair) in layer.iter_mut().zip(light.as_chunks::<2>().0) {
*out = (pair[0] >> shift & 15) | (pair[1] >> shift & 15) << 4;
}
layer
}
/// The packed light of blocks `at..at + out.len()`, into `out`.
#[inline]
@@ -576,7 +589,7 @@ pub struct Chunk {
/// Block entities read from a save, moved into the world on arrival.
pub block_entities: Vec<((i32, i32, i32), block_entity::BlockEntity)>,
/// `CustomName` of the container block entities (shown as the screen title).
pub block_names: Vec<((i32, i32, i32), String)>,
pub block_names: Vec<((i32, i32, i32), Tag)>,
/// Generation postprocessing marks (ProtoChunk.postProcessing) as world
/// positions, applied once by LevelChunk.postProcessGeneration.
pub postprocess: Vec<[i32; 3]>,
@@ -620,6 +633,14 @@ impl Chunk {
entities: Vec::new(),
}
}
/// The original `structures.starts`, including starts with unknown pieces.
pub fn saved_structure_starts(&self) -> Option<&horizon_world::nbt::Compound> {
let structures = self.saved.as_ref()?.root.get(&"structures".into())?;
match structures.get("starts")? {
horizon_world::nbt::Tag::Compound(starts) => Some(starts),
_ => None,
}
}
/// The minimum block Y of the column.
#[inline]
pub fn min_y(&self) -> i32 {
@@ -11,7 +11,7 @@ use horizon_world::{
use std::{
collections::HashMap,
sync::{
Arc,
Arc, Mutex, OnceLock, PoisonError,
atomic::{AtomicU64, Ordering},
},
};
@@ -59,6 +59,9 @@ pub enum BlockEntity {
CreakingHeart(Box<CreakingHeart>),
/// `BannerBlockEntity`: the pattern layers.
Banner(Banner),
/// `CopperGolemStatueBlockEntity`: its own components, which hold the
/// custom name of the golem it was (`createStatue`).
CopperGolemStatue(Compound),
/// `CrafterBlockEntity`.
Crafter(Crafter),
/// `SpawnerBlockEntity` (`crate::mobs::spawner`).
@@ -80,6 +83,8 @@ pub enum BlockEntity {
/// `DaylightDetectorBlockEntity`: no data, only the ticker
/// (`DaylightDetectorBlock.tickEntity`).
DaylightDetector,
/// `BellBlockEntity`: saved identity only; ringing state is kept separately.
Bell,
/// Suspicious sand and gravel: saved loot and the transient brushed face.
Brushable(Box<Brushable>),
/// A shelf holds three displayed item stacks.
@@ -108,6 +113,9 @@ pub enum BlockEntity {
pub struct ModBlockEntity {
fields: ModBlockEntityFields,
revision: ModRevision,
// A client update can consume transient effects without changing saved
// fields. Most entities never use this; do not allocate their queue.
client_update_fields: OnceLock<Mutex<Compound>>,
}
/// What a `ModBlockEntity` holds: its type id (`mymod:machine`), its saved
/// compound without `id`, `x`, `y`, `z` and `Items`, its item slots
@@ -138,11 +146,55 @@ impl ModBlockEntity {
born: MOD_BLOCK_ENTITIES_MADE.fetch_add(1, Ordering::Relaxed),
changes: 0,
},
client_update_fields: OnceLock::new(),
}
}
pub fn revision(&self) -> ModRevision {
self.revision
}
/// Replace the owner's pending, one-shot client update fields. These
/// fields are not saved, cloned, compared or retained for drawing, and
/// changing them does not change this entity's revision. The owner must
/// explicitly take and merge them into its update tag. Calling this does
/// not schedule a block update.
///
/// An empty batch clears a pending batch without allocating an idle queue.
pub fn set_client_update_fields(&mut self, fields: Compound) {
if fields.is_empty() {
self.clear_client_update_fields();
return;
}
*self
.client_update_fields
.get_or_init(|| Mutex::new(Compound::new()))
.lock()
.unwrap_or_else(PoisonError::into_inner) = fields;
}
/// Discard the owner's pending client update fields, without changing
/// saved data, revision or a queue that has never been used.
pub fn clear_client_update_fields(&mut self) {
if let Some(fields) = self.client_update_fields.get() {
fields
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
}
/// Atomically take the owner's pending fields at update-tag construction.
/// A loaded chunk and a block change use this same boundary. Construct a
/// tag once, then send that tag to every recipient; another construction
/// sees an empty batch. A save or a clone does not consume this batch.
///
/// This is safe for an update-tag hook receiving only a shared reference,
/// including a saved chunk encoded by a worker thread. If a previous lock
/// holder panicked, preserve its pending data instead of dropping it.
pub fn take_client_update_fields(&self) -> Compound {
self.client_update_fields
.get()
.map_or_else(Compound::new, |fields| {
std::mem::take(&mut *fields.lock().unwrap_or_else(PoisonError::into_inner))
})
}
/// A client's copy loads the tag the server sent (`loadWithComponents`
/// in `ClientPacketListener.handleBlockEntityData`): `tag`'s fields
/// replace its own of the same names. One it already has as sent is
@@ -402,6 +454,9 @@ pub struct Jukebox {
pub items: [Option<ItemStack>; 1],
/// `ticks_since_song_started`, while a song plays.
pub ticks: i64,
/// `JukeboxBlockEntity.saveAdditional` writes the clock only when its song
/// reference exists. Keep this separate from legacy playback adoption.
pub song_clock_present: bool,
pub playing: bool,
}
impl Jukebox {
@@ -460,6 +515,11 @@ pub struct Info {
pub pos: [f64; 3],
pub source: Option<Who>,
pub owner: Option<Who>,
/// `uuid`: the source's saved identity (`source`), taken when the event
/// was posted. `source` is the live entity and comes back `None` on load.
pub source_uuid: Option<[i32; 4]>,
/// `projectileOwnerUuid` (`projectile_owner`), likewise beside `owner`.
pub owner_uuid: Option<[i32; 4]>,
}
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -706,6 +766,85 @@ pub struct CommandBlock {
mod tests {
use super::*;
#[test]
fn transient_client_update_fields_are_lazy_replaceable_and_drained_once() {
let mut machine = ModBlockEntity::new(ModBlockEntityFields {
id: "test:machine".into(),
nbt: Compound::from([("progress".into(), Tag::Int(7))]),
..Default::default()
});
let revision = machine.revision();
let saved = machine.fields.clone();
let debug = format!("{machine:?}");
machine.clear_client_update_fields();
machine.set_client_update_fields(Compound::new());
assert!(machine.take_client_update_fields().is_empty());
assert!(machine.client_update_fields.get().is_none());
machine.set_client_update_fields(Compound::from([("discarded".into(), Tag::Int(1))]));
let expected = Compound::from([("effects".into(), Tag::List(vec![Tag::Int(2)]))]);
machine.set_client_update_fields(expected.clone());
assert_eq!(machine.take_client_update_fields(), expected);
assert!(machine.take_client_update_fields().is_empty());
machine.set_client_update_fields(expected.clone());
let copy = machine.clone();
assert_eq!(machine, copy, "transient fields are not retained contents");
assert_ne!(machine.revision(), copy.revision());
assert!(copy.client_update_fields.get().is_none());
assert!(copy.take_client_update_fields().is_empty());
assert_eq!(machine.take_client_update_fields(), expected);
machine.set_client_update_fields(expected);
machine.clear_client_update_fields();
assert!(machine.take_client_update_fields().is_empty());
assert_eq!(machine.fields, saved);
assert_eq!(machine.revision(), revision);
assert_eq!(format!("{machine:?}"), debug);
}
#[test]
fn concurrent_client_update_constructions_consume_one_batch() {
let mut machine = ModBlockEntity::default();
let expected = Compound::from([("effects".into(), Tag::Int(23))]);
machine.set_client_update_fields(expected.clone());
let received = std::thread::scope(|scope| {
let handles = (0..16)
.map(|_| scope.spawn(|| machine.take_client_update_fields()))
.collect::<Vec<_>>();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect::<Vec<_>>()
});
let nonempty = received
.into_iter()
.filter(|c| !c.is_empty())
.collect::<Vec<_>>();
assert_eq!(nonempty, [expected]);
}
#[test]
fn a_poisoned_client_update_lock_preserves_its_pending_batch() {
let mut machine = ModBlockEntity::default();
let expected = Compound::from([("effects".into(), Tag::Int(23))]);
machine.set_client_update_fields(expected.clone());
std::thread::scope(|scope| {
assert!(
scope
.spawn(|| {
let _held = machine.client_update_fields.get().unwrap().lock().unwrap();
panic!("interrupt before consuming the queued batch");
})
.join()
.is_err()
);
});
assert_eq!(machine.take_client_update_fields(), expected);
assert!(machine.take_client_update_fields().is_empty());
machine.set_client_update_fields(expected.clone());
assert_eq!(machine.take_client_update_fields(), expected);
}
#[test]
fn a_mod_block_entitys_revision_moves_with_each_change_and_only_then() {
let mut machine = ModBlockEntity::new(ModBlockEntityFields {
+55 -2
View File
@@ -208,7 +208,7 @@ where
Ok(resolve_world_movement(
self,
MovementRequest {
entity: EntityIdentity(0),
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
movement,
@@ -279,7 +279,7 @@ impl SceneCollisions<'_, '_> {
Ok(resolve_world_movement(
self,
MovementRequest {
entity: EntityIdentity(0),
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
movement,
@@ -300,6 +300,7 @@ impl SceneCollisions<'_, '_> {
CollisionContext::for_entity(
CollisionEntity {
identity: EntityIdentity(0),
kind: horizon_world::collision_context::EntityContextKind::Living,
y,
descending: false,
main_hand: Some(CollisionItem::EMPTY),
@@ -335,6 +336,58 @@ where
{
no_collision_in(world, bounds, true)
}
/// `CollisionGetter.noCollision(entity, bounds)`: retain the real entity snapshot.
pub fn no_collision_with_context<W, E>(
world: &mut W,
bounds: Aabb,
context: CollisionContext<'_>,
) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
{
no_world_collision(
world,
CollisionCheckRequest {
entity: context.entity().unwrap_or(EntityIdentity(0)),
context,
bounds,
candidate_budget: 1_000_000,
},
)
.unwrap_or(false)
}
/// `Level.loadedAndEntityCanStandOn(pos, projectile)` with the projectile's
/// feet at `entity_y` (`loadedAndEntityCanStandOnFace` with `UP`):
/// `Block.isFaceFull(state.getCollisionShape(level, pos,
/// CollisionContext.of(projectile)), UP)`. A projectile is not living and never
/// falls (it moves by `setPos`), so powder snow and liquids hold nothing up
/// and only scaffolding reads the height. An unloaded or out-of-world block
/// reads as air.
pub fn projectile_can_stand_on(
world: &World,
binding: &BlockCollisionRegistry<'_>,
pos: BlockPos,
entity_y: f64,
) -> bool {
use horizon_core::geometry::{Direction, ShapeOp};
let mut scene = SceneCollisions {
world,
binding,
subject: None,
};
let state = world.block(pos.x, pos.y, pos.z);
let context = CollisionContext::position(entity_y);
binding
.collision_shape_with_context(&mut scene, state, pos, &context)
.is_ok_and(|shape| {
shape.is_block()
|| shape.face_shape(Direction::Up).is_ok_and(|face| {
VoxelShape::block()
.join_is_not_empty(&face, ShapeOp::XOR)
.is_ok_and(|differs| !differs)
})
})
}
fn no_collision_in<W, E>(world: &mut W, bounds: Aabb, fluids: bool) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
+78 -14
View File
@@ -19,26 +19,75 @@ pub struct ModMenuType {
pub slots: usize,
pub data_slots: usize,
pub results: Vec<usize>,
/// its crafting grid, if it has one (`crafting`).
pub crafting: Option<ModCrafting>,
/// its linked slots, if it has any (`links`).
pub links: Option<ModLinks>,
}
/// a mod menu type's linked slots: `count` container slots from `from` on,
/// which show the slots of the containers a native mod links them to
/// (`native_menu_links`) instead of the block entity's.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ModLinks {
pub from: usize,
pub count: usize,
}
impl ModLinks {
/// The linked container slots.
pub fn slots(&self) -> std::ops::Range<usize> {
self.from..self.from + self.count
}
}
/// a mod menu type's crafting grid: `size`x`size` container slots from
/// `grid` on, row by row, and the slot showing what they craft. The block
/// entity holds the grid; the result is the menu's.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ModCrafting {
pub grid: usize,
pub size: usize,
pub result: usize,
}
impl ModCrafting {
/// The grid's container slots.
pub fn grid_slots(&self) -> std::ops::Range<usize> {
self.grid..self.grid + self.size * self.size
}
}
static MOD_MENU_TYPES: std::sync::OnceLock<Vec<ModMenuType>> = std::sync::OnceLock::new();
/// registers the content mods' menu types, `(id, slots, data slots,
/// results)`, a list set once per process (false when it already was).
pub fn register_mod_menu_types(
menus: impl IntoIterator<Item = (String, usize, usize, Vec<usize>)>,
) -> bool {
/// One menu type as [`register_mod_menu_types`] takes it: `(id, slots, data
/// slots, results, crafting, links)`.
pub type ModMenuTypeEntry = (
String,
usize,
usize,
Vec<usize>,
Option<ModCrafting>,
Option<ModLinks>,
);
/// registers the content mods' menu types, a list set once per process
/// (false when it already was).
pub fn register_mod_menu_types(menus: impl IntoIterator<Item = ModMenuTypeEntry>) -> bool {
if MOD_MENU_TYPES.get().is_some() {
return false;
}
let menus = menus
.into_iter()
.map(|(id, slots, data_slots, results)| ModMenuType {
id: Box::leak(id.into_boxed_str()),
slots,
data_slots,
results,
});
let menus =
menus.into_iter().map(
|(id, slots, data_slots, results, crafting, links)| ModMenuType {
id: Box::leak(id.into_boxed_str()),
slots,
data_slots,
results,
crafting,
links,
},
);
MOD_MENU_TYPES.set(menus.collect()).is_ok()
}
@@ -392,9 +441,15 @@ impl BlockEntity {
if name == "minecraft:end_gateway" {
return Some(Self::EndGateway(EndGateway::default()));
}
if name == "minecraft:bell" {
return Some(Self::Bell);
}
if name == "minecraft:daylight_detector" {
return Some(Self::DaylightDetector);
}
if name.ends_with("copper_golem_statue") {
return Some(Self::CopperGolemStatue(Default::default()));
}
if name == "minecraft:beacon" {
return Some(Self::Beacon(Box::new(Beacon {
last_check_y: i32::MIN / 2,
@@ -460,6 +515,7 @@ impl BlockEntity {
Self::CommandBlock(_) => crate::command_block::Mode::of_block(name).is_some(),
Self::Skull(_) => is_skull(name),
Self::Banner(_) => name.ends_with("_banner"),
Self::CopperGolemStatue(_) => name.ends_with("copper_golem_statue"),
Self::Shelf(_) => name == "minecraft:shelf" || name.ends_with("_shelf"),
Self::Mod(_) => !name.starts_with("minecraft:"),
Self::Beehive(_) => matches!(name, "minecraft:beehive" | "minecraft:bee_nest"),
@@ -483,6 +539,7 @@ impl BlockEntity {
Self::Crafter(_) => name == "minecraft:crafter",
Self::EndGateway(_) => name == "minecraft:end_gateway",
Self::DaylightDetector => name == "minecraft:daylight_detector",
Self::Bell => name == "minecraft:bell",
Self::StructureBlock(_) => name == "minecraft:structure_block",
Self::TestBlock(_) => name == "minecraft:test_block",
Self::TestInstanceBlock(_) => name == "minecraft:test_instance_block",
@@ -510,6 +567,7 @@ impl BlockEntity {
Self::Lectern(_) => "minecraft:lectern",
Self::Sculk(s) => s.id(),
Self::Banner(_) => "minecraft:banner",
Self::CopperGolemStatue(_) => "minecraft:copper_golem_statue",
Self::Crafter(_) => "minecraft:crafter",
Self::CreakingHeart(_) => "minecraft:creaking_heart",
Self::Spawner(_) => "minecraft:mob_spawner",
@@ -520,6 +578,7 @@ impl BlockEntity {
Self::Skull(_) => "minecraft:skull",
Self::EndGateway(_) => "minecraft:end_gateway",
Self::DaylightDetector => "minecraft:daylight_detector",
Self::Bell => "minecraft:bell",
Self::Brushable(_) => "minecraft:brushable_block",
Self::Campfire(_) => "minecraft:campfire",
Self::ChiseledBookshelf(_) => "minecraft:chiseled_bookshelf",
@@ -554,11 +613,12 @@ impl BlockEntity {
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Bell
| Self::Brushable(_) => Vec::new(),
Self::Mod(m) => m.items.iter().flatten().cloned().collect(),
Self::Lectern(l) => l.book.iter().cloned().collect(),
Self::Sculk(_) => Vec::new(),
Self::Banner(_) => Vec::new(),
Self::Banner(_) | Self::CopperGolemStatue(_) => Vec::new(),
Self::Crafter(c) => c.items.iter().flatten().cloned().collect(),
Self::Campfire(c) => c.items.iter().flatten().cloned().collect(),
Self::ChiseledBookshelf(b) => b.items.iter().flatten().cloned().collect(),
@@ -575,6 +635,7 @@ impl BlockEntity {
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::CopperGolemStatue(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::PotentSulfur(_)
@@ -583,6 +644,7 @@ impl BlockEntity {
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Bell
| Self::Brushable(_) => &[],
Self::Jukebox(j) => &j.items,
Self::Crafter(c) => &c.items,
@@ -609,6 +671,7 @@ impl BlockEntity {
| Self::Lectern(_)
| Self::Sculk(_)
| Self::Banner(_)
| Self::CopperGolemStatue(_)
| Self::EndGateway(_)
| Self::Skull(_)
| Self::PotentSulfur(_)
@@ -617,6 +680,7 @@ impl BlockEntity {
| Self::TestBlock(_)
| Self::TestInstanceBlock(_)
| Self::DaylightDetector
| Self::Bell
| Self::Brushable(_) => &mut [],
Self::Jukebox(j) => &mut j.items,
Self::Crafter(c) => &mut c.items,
+169 -10
View File
@@ -65,6 +65,51 @@ pub struct Setting {
pub item: Item,
pub text: &'static str,
}
/// What the chosen value does, for a setting whose values each do something different.
fn choice_description(opt: Opt, pad: &PadSettings) -> Option<&'static str> {
Some(match opt {
Opt::PadGameplayGuide => match pad.guide_detail {
gamepad::GuideDetail::Full => {
"Shows what your buttons do right now, in play and in inventories."
}
gamepad::GuideDetail::Basic => {
"Shows only what you can do with what you're looking at, and the main inventory buttons."
}
gamepad::GuideDetail::Off => "Doesn't show what your buttons do.",
},
Opt::PadGlyphs => match pad.glyphs {
GlyphStyle::Auto => "Shows the buttons of the controller you're using.",
GlyphStyle::Xbox => "Always shows Xbox buttons.",
GlyphStyle::PlayStation => "Always shows PlayStation buttons.",
GlyphStyle::Nintendo => "Always shows Nintendo buttons.",
},
Opt::PadNintendoButtons => {
if pad.nintendo_letters {
"A, on the right, confirms and jumps and B goes back, as on a Switch."
} else {
"B, at the bottom, confirms and jumps and A goes back, as on other controllers."
}
}
Opt::PadKeyboard => match pad.keyboard {
gamepad::TextKeyboard::Auto => {
"Opens Steam's keyboard on a Steam Deck under Steam, and Horizon's otherwise."
}
gamepad::TextKeyboard::Horizon => "Always opens Horizon's keyboard.",
gamepad::TextKeyboard::Steam => "Always opens Steam's keyboard.",
},
Opt::PadLightbar => match pad.lightbar {
gamepad::Lightbar::Health => {
"A DualSense's light shows your health, from green through amber to red."
}
gamepad::Lightbar::Dimension => {
"A DualSense's light changes with the dimension you're in."
}
gamepad::Lightbar::Off => "A DualSense keeps its own light.",
},
_ => return None,
})
}
fn option(opt: Opt, text: &'static str) -> Setting {
Setting {
item: Item::Option(opt),
@@ -97,10 +142,16 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
vec![
(
"Controller",
vec![option(
Opt::PadEnabled,
"Off, the game ignores every controller for this player.",
)],
vec![
option(
Opt::PadEnabled,
"Off, the game ignores every controller for this player.",
),
option(
Opt::PadPlayInBackground,
"The controller keeps playing while this window isn't focused, and losing focus won't pause.",
),
],
),
(
"Look",
@@ -135,10 +186,20 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
),
(
"Play",
vec![option(
Opt::PadTriggerThreshold,
"How far a trigger is pulled before it counts as pressed.",
)],
vec![
option(
Opt::PadGameplayGuide,
"Show the buttons for what you can do, while you play with a controller.",
),
option(
Opt::PadCraftOnServers,
"Lets LT + A craft on servers other than your own worlds. Some servers count it as a macro.",
),
option(
Opt::PadTriggerThreshold,
"How far a trigger is pulled before it counts as pressed.",
),
],
),
(
"Menus",
@@ -211,6 +272,10 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
Opt::PadGlyphs,
"Follow the controller, or always show one family's buttons.",
),
option(
Opt::PadNintendoButtons,
"Whether a Nintendo controller's A and B work by where they sit or by their letters.",
),
info(Info::Using, "The buttons menus show right now."),
],
),
@@ -279,9 +344,11 @@ impl Item {
match self {
Item::Option(Opt::PadEnabled) => Some(c.enabled),
Item::Option(Opt::PadVibration) => Some(c.vibration),
Item::Option(Opt::PadPlayInBackground) => Some(c.play_in_background),
Item::Option(Opt::PadAdaptiveTriggers) => Some(c.adaptive_triggers),
Item::Option(Opt::PadInvertX) => Some(c.invert_x),
Item::Option(Opt::PadInvertY) => Some(c.invert_y),
Item::Option(Opt::PadCraftOnServers) => Some(c.craft_on_servers),
_ => None,
}
}
@@ -604,6 +671,7 @@ impl ControllerUi {
hover,
focused,
held: self.dragging == Some(item),
key: None,
};
shell::row(batch, atlas, &cols, y, &view);
}
@@ -818,8 +886,12 @@ impl ControllerUi {
/// What a row does. A binding names the controller's own buttons, and what else its button
/// does when it shares it.
fn description(&self, setting: Setting, s: &Settings) -> String {
let Item::Bind(action) = setting.item else {
return lang::get(setting.text);
let action = match setting.item {
Item::Bind(action) => action,
Item::Option(opt) => {
return lang::get(choice_description(opt, &s.controller).unwrap_or(setting.text));
}
_ => return lang::get(setting.text),
};
let pad = &s.controller;
let style = self.context.style;
@@ -1265,6 +1337,7 @@ mod tests {
for opt in [
Opt::PadEnabled,
Opt::PadVibration,
Opt::PadPlayInBackground,
Opt::PadAdaptiveTriggers,
Opt::PadLightbar,
Opt::PadLookSensitivity,
@@ -1275,6 +1348,7 @@ mod tests {
Opt::PadLookDeadzone,
Opt::PadTriggerThreshold,
Opt::PadGlyphs,
Opt::PadNintendoButtons,
Opt::PadKeyboard,
Opt::PadPointerSpeed,
Opt::PadScrollSpeed,
@@ -1372,6 +1446,78 @@ mod tests {
assert!((s.controller.move_deadzone - before - 0.01).abs() < 0.001);
}
/// The Device page's Nintendo Buttons row sits with the glyphs, steps between its two
/// values rather than ticking a box, and what it sets is what the controller file keeps.
#[test]
fn the_nintendo_buttons_row_flips_letters_and_the_file_keeps_it() {
let (page, group) = rows()
.into_iter()
.flat_map(|(page, groups)| groups.into_iter().map(move |g| (page, g)))
.find(|(_, (_, rows))| {
rows.iter()
.any(|r| r.item == Item::Option(Opt::PadNintendoButtons))
})
.unwrap();
assert_eq!((page, group.0), (Page::Device, "Glyphs"));
let mut s = Settings::default();
let item = Item::Option(Opt::PadNintendoButtons);
assert!(item.steps(&s));
s.controller.nintendo_letters = false;
let mut ui = ControllerUi {
focus: Some(Target::Item(item)),
..ControllerUi::default()
};
// Vanilla's caption and value template; Horizon's own words come bundled.
struct Vanilla;
impl lang::ResourceSource for Vanilla {
fn read(&self, path: &str) -> Option<Vec<u8>> {
(path == "assets/minecraft/lang/en_us.json")
.then(|| br#"{"options.generic_value": "%s: %s"}"#.to_vec())
}
}
let value = |ui: &ControllerUi, s: &Settings| {
lang::scoped(lang::Language::load(&[&Vanilla], "en_us"), || {
ui.parts(item, s)
})
.0
};
assert_eq!(value(&ui, &s).0, "Nintendo Buttons");
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Position"));
let actions = key(&mut ui, &mut s, K::Enter);
assert!(actions.contains(&Action::Changed), "{actions:?}");
assert!(s.controller.nintendo_letters);
assert_eq!(value(&ui, &s).1.as_deref(), Some("By Letter"));
assert!(PadSettings::parse(&s.controller.serialize()).nintendo_letters);
key(&mut ui, &mut s, K::Enter);
assert!(!s.controller.nintendo_letters);
assert!(!PadSettings::parse(&s.controller.serialize()).nintendo_letters);
}
/// What the screen draws at the bottom of the page changes with the choice, in every one of
/// these settings.
#[test]
fn each_choice_describes_itself() {
let ui = ControllerUi::default();
let mut s = Settings::default();
for (opt, choices) in [
(Opt::PadGameplayGuide, 3),
(Opt::PadGlyphs, 4),
(Opt::PadNintendoButtons, 2),
(Opt::PadKeyboard, 3),
(Opt::PadLightbar, 3),
] {
let setting = option(opt, "the fixed sentence");
let mut seen = Vec::new();
for _ in 0..choices {
let text = ui.description(setting, &s);
assert_ne!(text, "the fixed sentence", "{opt:?}");
assert!(!seen.contains(&text), "{opt:?} repeats {text}");
seen.push(text);
opt.cycle(&mut s, false, FB);
}
}
}
#[test]
fn every_text_is_in_the_language_table() {
let (_, missed) = lang::scoped(lang::Language::load(&[], "de_de"), || {
@@ -1385,6 +1531,19 @@ mod tests {
}
}
}
let mut settings = Settings::default();
for opt in [
Opt::PadGameplayGuide,
Opt::PadGlyphs,
Opt::PadNintendoButtons,
Opt::PadKeyboard,
Opt::PadLightbar,
] {
for _ in 0..4 {
lang::get(choice_description(opt, &settings.controller).unwrap());
opt.cycle(&mut settings, false, FB);
}
}
for text in [
"Controller",
"Player profile",
@@ -0,0 +1,243 @@
//! OptiFine custom skies (`horizon_optifine::sky`) as the game draws them: a pack's layers with
//! their decoded textures, read once with the pack, and each frame's choice of layers, their
//! brightness and rotation ([`CustomSky::draws`]), which `gfx::custom_sky` draws over the
//! vanilla sky. A pack without `optifine/sky` gives an empty [`CustomSky`] and the sky stays
//! vanilla.
use std::path::Path;
use glam::{Mat4, Vec3};
use horizon_optifine::sky::{Blend, Conditions, Skies, SkyLayer, Transition};
/// A layer's texture, RGBA8.
pub struct Image {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
/// The pack's layers, by world, with one texture per layer (`images[i]` for the `i`th layer
/// over all worlds, in `worlds` order).
#[derive(Default)]
pub struct CustomSky {
worlds: Vec<(i32, Vec<SkyLayer>)>,
pub images: Vec<Image>,
}
/// One layer of a frame: which texture, with which blend, how bright and turned how.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Draw {
pub image: u16,
pub blend: Blend,
pub brightness: f32,
/// Camera-relative world from the skybox's space.
pub rotation: Mat4,
}
/// The eased brightness of each layer, kept by the game between frames.
#[derive(Default)]
pub struct State {
world: Option<i32>,
eased: Vec<Transition>,
}
/// Textures past this many texels in all are left out (a skybox is a few megapixels at most).
const TEXEL_BUDGET: u64 = 64 << 20;
impl CustomSky {
/// The custom skies of the overlay at `root`. A layer whose texture does not decode is
/// dropped with a warning.
pub fn load(root: &Path) -> Self {
let skies = Skies::scan(root);
let mut out = Self::default();
let mut texels = 0u64;
for (world, sky) in skies.worlds {
let mut layers = Vec::new();
for layer in sky.layers {
match crate::hud::decode(&root.join(&layer.source)) {
Ok((width, height, rgba))
if texels + u64::from(width) * u64::from(height) <= TEXEL_BUDGET =>
{
texels += u64::from(width) * u64::from(height);
out.images.push(Image {
width,
height,
rgba,
});
layers.push(layer);
}
Ok(_) => log::warn!("{}: past the custom sky's texture budget", layer.source),
Err(e) => log::warn!("{}: {e:#}", layer.source),
}
}
if !layers.is_empty() {
out.worlds.push((world, layers));
}
}
out
}
pub fn is_empty(&self) -> bool {
self.worlds.is_empty()
}
/// Whether `world` has layers (`0` the Overworld, `1` the End).
pub fn has(&self, world: i32) -> bool {
self.worlds.iter().any(|(w, _)| *w == world)
}
/// This frame's layers of `world` into `out` (cleared first), in drawing order: those whose
/// conditions hold, eased over their `transition` by `seconds` since the last frame, at the
/// brightness of their fade times. `sun_turns` is the sun's angle over 360° (`0` at noon).
pub fn draws(
&self,
world: i32,
conditions: &Conditions,
sun_turns: f32,
seconds: f32,
state: &mut State,
out: &mut Vec<Draw>,
) {
out.clear();
let mut first = 0;
let mut found = None;
for (w, layers) in &self.worlds {
if *w == world {
found = Some(layers);
break;
}
first += layers.len();
}
let Some(layers) = found else {
state.world = None;
return;
};
// A new world (or pack) starts each layer where its conditions put it: no fade in.
let snap = state.world != Some(world) || state.eased.len() != layers.len();
if snap {
state.world = Some(world);
state.eased.clear();
state.eased.resize(layers.len(), Transition::default());
}
for (i, (layer, eased)) in layers.iter().zip(&mut state.eased).enumerate() {
let target = if layer.shows(conditions) { 1.0 } else { 0.0 };
let shown = if snap {
eased.value = target;
target
} else {
eased.step(target, seconds, layer.transition)
};
let brightness = shown * layer.brightness(conditions.day_time);
if brightness <= 0.0 {
continue;
}
let turns = layer.turns(conditions.day_time, sun_turns);
let rotation = if turns == 0.0 {
Mat4::IDENTITY
} else {
Mat4::from_axis_angle(Vec3::from_array(layer.axis), turns * std::f32::consts::TAU)
};
out.push(Draw {
image: (first + i) as u16,
blend: layer.blend,
brightness,
rotation,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use horizon_optifine::sky::Now;
fn pack(files: &[(&str, &str)]) -> (std::path::PathBuf, CustomSky) {
let root = std::env::temp_dir().join(format!(
"horizon-custom-sky-{}-{}",
std::process::id(),
files.len()
));
let _ = std::fs::remove_dir_all(&root);
let dir = root.join("assets/minecraft/optifine/sky/world0");
std::fs::create_dir_all(&dir).unwrap();
let png = {
let mut bytes = Vec::new();
let mut encoder = png::Encoder::new(&mut bytes, 6, 4);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer
.write_image_data(&[255, 0, 0, 255].repeat(24))
.unwrap();
writer.finish().unwrap();
bytes
};
for (name, text) in files {
std::fs::write(dir.join(format!("{name}.properties")), text).unwrap();
std::fs::write(dir.join(format!("{name}.png")), &png).unwrap();
}
let sky = CustomSky::load(&root);
(root, sky)
}
fn noon(biome: &str) -> Conditions<'_> {
Conditions {
day_time: 6000,
weather: Now::Clear,
biome,
y: 64,
}
}
#[test]
fn layers_ease_in_at_a_biome_border_but_not_on_arrival() {
let (root, sky) = pack(&[
("sky1", "biomes=desert\ntransition=2"),
("sky2", "blend=alpha\nrotate=false"),
]);
let _ = std::fs::remove_dir_all(root);
assert!(sky.has(0) && !sky.has(1));
assert_eq!(sky.images[0].width, 6);
let (mut state, mut out) = (State::default(), Vec::new());
sky.draws(
0,
&noon("minecraft:desert"),
0.0,
0.05,
&mut state,
&mut out,
);
assert_eq!(
out.iter().map(|d| d.brightness).collect::<Vec<_>>(),
[1.0, 1.0]
);
// Out of the desert: the first layer fades over its two seconds.
sky.draws(0, &noon("minecraft:plains"), 0.0, 0.5, &mut state, &mut out);
assert_eq!(out[0].brightness, 0.75);
assert_eq!(out[1].blend, Blend::Alpha);
sky.draws(0, &noon("minecraft:plains"), 0.0, 2.0, &mut state, &mut out);
assert_eq!(out.len(), 1, "a dark layer is not drawn");
assert_eq!(out[0].image, 1);
// Into the End, which has no layers, and back: no fade.
sky.draws(1, &noon("minecraft:desert"), 0.0, 0.0, &mut state, &mut out);
assert!(out.is_empty());
sky.draws(0, &noon("minecraft:desert"), 0.0, 0.0, &mut state, &mut out);
assert_eq!(out.len(), 2);
}
#[test]
fn a_layer_turns_west_like_the_sun() {
let (root, sky) = pack(&[("sky1", "")]);
let _ = std::fs::remove_dir_all(root);
let (mut state, mut out) = (State::default(), Vec::new());
// A quarter turn after noon (sunset): the skybox's zenith has gone down to the west.
let c = Conditions {
day_time: 12000,
..noon("minecraft:plains")
};
sky.draws(0, &c, 0.25, 0.0, &mut state, &mut out);
let up = out[0].rotation.transform_vector3(Vec3::Y);
assert!((up - Vec3::NEG_X).length() < 1e-5, "{up}");
}
}
+245 -15
View File
@@ -141,7 +141,8 @@ impl DataPack {
pub fn discover(world: &Path) -> Vec<DataPack> {
discover_folder(&world.join("datapacks"))
}
/// Packs in Create World's temporary pack folder, before a world exists.
/// `FolderRepositorySource.loadPacks`: packs in Create World's temporary
/// folder or a world's data-pack directory, with required folder metadata.
pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
@@ -150,7 +151,7 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
for e in entries.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let path = e.path();
if path.is_dir() && path.join("pack.mcmeta").exists() {
if path.is_dir() && folder_metadata_present(&path) {
packs.push(DataPack {
id: format!("file/{name}"),
source: Source::Dir(path),
@@ -168,6 +169,22 @@ pub fn discover_folder(dir: &Path) -> Vec<DataPack> {
packs.sort_by(|a, b| a.id.cmp(&b.id));
packs
}
/// `Pack.readPackMetadata`: even its fallback codec requires a pack section
/// and description. Keep this folder gate separate from format compatibility
/// and the existing interpretation of the description component.
fn folder_metadata_present(path: &Path) -> bool {
let Some(meta) = std::fs::read(path.join("pack.mcmeta"))
.ok()
.and_then(|bytes| crate::packs::parse_meta(&bytes))
else {
return false;
};
meta.as_object()
.and_then(|root| root.get("pack"))
.and_then(serde_json::Value::as_object)
.and_then(|pack| pack.get("description"))
.is_some_and(|description| !description.is_null())
}
/// The resources root's built-in feature packs (`ServerPacksSource`
/// scanning the game's `data/minecraft/datapacks`): every folder with a
/// `pack.mcmeta`, a feature pack (`PackSource.FEATURE`). Off until a world
@@ -261,6 +278,43 @@ pub fn screen_packs(dir: &Path, resources: &Path) -> Vec<crate::pack_screen::Pac
}
infos
}
/// The folder of a game folder whose packs every new world there starts with: a modpack's profile
/// keeps the packs of its data-only jars here (`horizon-port-pack`). Each world gets its own copy,
/// as Create World's packs do, so a world keeps working when the folder changes. Vanilla has no
/// such folder, and without one nothing here happens.
pub const NEW_WORLD_PACKS: &str = "new-world-datapacks";
/// Create World's temporary pack folder holding `game_dir`'s [`NEW_WORLD_PACKS`], and their ids,
/// to show them selected in the Data Packs screen. `None` when there are none.
pub fn seed_create_world(game_dir: &Path) -> Option<(PathBuf, Vec<String>)> {
let from = game_dir.join(NEW_WORLD_PACKS);
let ids: Vec<String> = discover_folder(&from).into_iter().map(|p| p.id).collect();
if ids.is_empty() {
return None;
}
let seeded = temp_folder()
.map_err(|e| e.to_string())
.and_then(|dir| copy(&from, &dir).map(|()| dir));
match seeded {
Ok(dir) => Some((dir, ids)),
Err(e) => {
log::warn!("Horizon: data packs for new worlds: {e}");
None
}
}
}
/// A world about to be created without Create World (`--world`, the control socket) gets
/// `game_dir`'s [`NEW_WORLD_PACKS`]. The game turns on every pack it finds in a world.
pub fn install_new_world_packs(game_dir: &Path, world: &Path) -> Result<(), String> {
let from = game_dir.join(NEW_WORLD_PACKS);
let exists = |name: &str| world.join(name).exists();
if !from.is_dir() || exists("level.dat") || exists("level.dat_old") {
return Ok(());
}
copy(&from, &world.join("datapacks"))
}
/// A new empty folder for Create World's packs
/// (`Files.createTempDirectory("mcworld-")`).
pub fn temp_folder() -> std::io::Result<PathBuf> {
@@ -608,7 +662,7 @@ pub struct DataStore {
/// `item_modifier` files, for `/item modify`.
pub item_modifiers: BTreeMap<String, serde_json::Value>,
/// The content tables' raw files from the packs after the built-in one:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/` and `tags/…/` files, by
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` and `tags/…/` files, by
/// `<kind-dir>/<ns>:<path>`, each id's files in pack order. The tables
/// merge them over what they read from the resource root.
pub files: BTreeMap<String, Vec<Vec<u8>>>,
@@ -617,7 +671,7 @@ pub struct DataStore {
}
/// `<kind-dir>/<ns>:<path>` for a pack file the content tables read:
/// `recipe/`, `recipe_filter/`, `loot_table/`, `advancement/`, `trade_set/`,
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/` or `tags/<sub>/`, where the id keeps the sub
/// `villager_trade/`, `dialog/`, `damage_type/`, `data_maps/`, `enchantment/` or `tags/<sub>/`, where the id keeps the sub
/// path so a nested tag kind stays addressable.
fn content_id(file: &str) -> Option<String> {
let rest = file.strip_prefix("data/")?;
@@ -625,7 +679,7 @@ fn content_id(file: &str) -> Option<String> {
let (kind, rest) = rest.split_once('/')?;
let kind = match kind {
"recipe" | "recipe_filter" | "loot_table" | "advancement" | "trade_set"
| "villager_trade" | "dialog" | "damage_type" | "data_maps" => kind,
| "villager_trade" | "dialog" | "damage_type" | "data_maps" | "enchantment" => kind,
// A tag id is `{ns}:{path-under-tags}` (`tags/worldgen/biome/
// test:sub/mytag`), the whole path after the namespace, so nested
// registries keep their directory (`TagLoader`'s id shape).
@@ -659,6 +713,7 @@ impl DataStore {
"damage_type",
"data_maps",
"dialog",
"enchantment",
"function",
"item_modifier",
"loot_table",
@@ -714,15 +769,55 @@ impl DataStore {
}
s
}
/// `FunctionManager.getTag`: the functions a tag expands to (nested tags
/// included, each function once).
/// `ServerFunctionLibrary.getTag`: only valid function tags expand
/// (nested tags included, each function once).
pub fn tag_functions(&self, id: &str) -> Vec<String> {
let mut out = Vec::new();
let mut seen_fns = std::collections::HashSet::new();
let mut seen_tags = std::collections::HashSet::new();
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags);
let invalid = self.invalid_function_tags(id);
self.expand(id, &mut out, &mut seen_fns, &mut seen_tags, &invalid);
out
}
/// `TagLoader.tryBuildTag`: missing required members invalidate their tag and
/// required parents. Optional invalid children contribute nothing.
/// Resolve against the current accepted function map, without caching a
/// candidate store or changing the existing expansion's cycle handling.
fn invalid_function_tags<'a>(&'a self, id: &'a str) -> std::collections::HashSet<&'a str> {
let mut invalid = std::collections::HashSet::new();
let mut parents: std::collections::HashMap<&str, Vec<&str>> =
std::collections::HashMap::new();
let mut seen = std::collections::HashSet::new();
let mut pending = vec![id];
while let Some(tag) = pending.pop() {
if !seen.insert(tag) {
continue;
}
let Some(entries) = self.function_tags.get(tag) else {
invalid.insert(tag);
continue;
};
for (entry, required) in entries {
if let Some(child) = entry.strip_prefix('#') {
pending.push(child);
if *required {
parents.entry(child).or_default().push(tag);
}
} else if *required && !self.functions.contains_key(entry) {
invalid.insert(tag);
}
}
}
pending.extend(invalid.iter().copied());
while let Some(child) = pending.pop() {
for &parent in parents.get(child).into_iter().flatten() {
if invalid.insert(parent) {
pending.push(parent);
}
}
}
invalid
}
/// Depth-first walk keeping first-seen order. Each tag is expanded at most
/// once, so reference cycles end and nesting depth is not capped.
fn expand<'a>(
@@ -731,18 +826,19 @@ impl DataStore {
out: &mut Vec<String>,
seen_fns: &mut std::collections::HashSet<&'a str>,
seen_tags: &mut std::collections::HashSet<&'a str>,
invalid: &std::collections::HashSet<&'a str>,
) {
if !seen_tags.insert(id) {
if invalid.contains(id) || !seen_tags.insert(id) {
return;
}
let Some(entries) = self.function_tags.get(id) else {
return;
};
for (e, required) in entries {
for (e, _) in entries {
match e.strip_prefix('#') {
Some(inner) => self.expand(inner, out, seen_fns, seen_tags),
Some(inner) => self.expand(inner, out, seen_fns, seen_tags, invalid),
None => {
if (self.functions.contains_key(e) || *required) && seen_fns.insert(e) {
if self.functions.contains_key(e) && seen_fns.insert(e) {
out.push(e.clone());
}
}
@@ -924,6 +1020,97 @@ pub fn worldgen_overrides(
mod tests {
use super::*;
#[test]
fn folder_metadata_is_required_before_contents_are_discovered() {
let root =
std::env::temp_dir().join(format!("horizon-folder-metadata-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
for (name, metadata) in [
("broken", Some("{")),
("root_empty", Some("{}")),
("root_array", Some("[]")),
("pack_empty", Some(r#"{"pack":{}}"#)),
("pack_string", Some(r#"{"pack":"bad"}"#)),
("missing", None),
(
"full",
Some(
r#"{"pack":{"description":"full","pack_format":121,"min_format":[121,0],"max_format":[121,0]}}"#,
),
),
(
"description",
Some(r#"{"pack":{"description":"description only"}}"#),
),
("numeric", Some(r#"{"pack":{"description":42}}"#)),
("null", Some(r#"{"pack":{"description":null}}"#)),
("bom", Some("\u{feff}{\"pack\":{\"description\":\"BOM\"}}")),
(
"comment",
Some("{\"pack\":{\"description\":\"comment\"}}\n// trailing"),
),
] {
let dir = root.join(name);
std::fs::create_dir_all(dir.join("data/h146/function")).unwrap();
std::fs::write(
dir.join(format!("data/h146/function/{name}.mcfunction")),
"scoreboard players set admitted h146 1\n",
)
.unwrap();
if let Some(metadata) = metadata {
std::fs::write(dir.join("pack.mcmeta"), metadata).unwrap();
}
}
let packs = discover_folder(&root);
assert_eq!(
packs.iter().map(|p| p.id.as_str()).collect::<Vec<_>>(),
[
"file/bom",
"file/comment",
"file/description",
"file/full",
"file/numeric"
]
);
let store = DataStore::load(&packs.iter().collect::<Vec<_>>());
assert_eq!(
store
.functions
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
[
"h146:bom",
"h146:comment",
"h146:description",
"h146:full",
"h146:numeric"
]
);
let numeric = packs.iter().find(|p| p.id == "file/numeric").unwrap();
let raw: serde_json::Value =
serde_json::from_slice(&numeric.read("pack.mcmeta").unwrap()).unwrap();
assert_eq!(raw["pack"]["description"], 42);
// Builtin and feature construction still have their independent paths.
let resources = root.join("resources");
let feature = resources.join("data/minecraft/datapacks/feature");
std::fs::create_dir_all(&feature).unwrap();
std::fs::write(
feature.join("pack.mcmeta"),
r#"{"pack":{"description":"feature"}}"#,
)
.unwrap();
assert_eq!(
feature_packs(&resources)
.iter()
.map(|p| p.id.as_str())
.collect::<Vec<_>>(),
["feature"]
);
assert_eq!(DataPack::builtin(&resources).id, "vanilla");
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn data_map_files_are_content_files_keyed_by_map_namespace_and_registry() {
assert_eq!(
@@ -951,6 +1138,35 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_game_folder_s_new_world_packs_go_into_new_worlds_only() {
let root = std::env::temp_dir().join(format!("hz-new-world-packs-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let (game, world, old) = (root.join("game"), root.join("new"), root.join("old"));
assert!(seed_create_world(&game).is_none());
assert_eq!(install_new_world_packs(&game, &world), Ok(()));
assert!(!world.join("datapacks").exists());
for pack in ["void", "islands"] {
let dir = game.join(NEW_WORLD_PACKS).join(pack);
std::fs::create_dir_all(dir.join("data/v/function")).unwrap();
std::fs::write(dir.join("pack.mcmeta"), r#"{"pack":{"description":""}}"#).unwrap();
std::fs::write(dir.join("data/v/function/a.mcfunction"), "say a").unwrap();
}
let (temp, ids) = seed_create_world(&game).unwrap();
assert_eq!(ids, ["file/islands", "file/void"]);
assert!(temp.join("void/data/v/function/a.mcfunction").is_file());
let _ = std::fs::remove_dir_all(&temp);
install_new_world_packs(&game, &world).unwrap();
let ids: Vec<String> = discover(&world).into_iter().map(|p| p.id).collect();
assert_eq!(ids, ["file/islands", "file/void"]);
// A world that exists already keeps the packs it has.
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("level.dat"), "").unwrap();
install_new_world_packs(&game, &old).unwrap();
assert!(discover(&old).is_empty());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn enabled_packs_override_worldgen_in_order() {
let dir = std::env::temp_dir().join(format!("hz-datapack-worldgen-{}", std::process::id()));
@@ -961,7 +1177,7 @@ mod tests {
std::fs::write(path, t).unwrap();
};
for pack in ["a", "b", "off"] {
write(pack, "pack.mcmeta", "{}");
write(pack, "pack.mcmeta", r#"{"pack":{"description":"fixture"}}"#);
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
write(pack, "data/p/structure/room/a.nbt", pack);
write(
@@ -1132,6 +1348,20 @@ mod tests {
#[test]
fn tag_resolution_dedupes_fast_ends_cycles_and_nests_deeply() {
let mut store = DataStore::default();
// Every referenced function exists; missing required ones invalidate a tag.
for id in (0..25_000)
.map(|i| format!("t:f{i}"))
.chain(["t:x", "t:y", "t:deep"].map(str::to_owned))
{
store.functions.insert(
id.clone(),
Function {
id,
lines: Vec::new(),
parameters: Vec::new(),
},
);
}
let big: Vec<(String, bool)> = (0..50_000)
.map(|i| (format!("t:f{}", i % 25_000), true))
.collect();
@@ -1216,13 +1446,13 @@ mod tests {
write(
"fns",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
);
write("fns", "data/x/function/f.mcfunction", "say hi");
write(
"off",
"pack.mcmeta",
r#"{"pack":{"min_format":121,"max_format":121}}"#,
r#"{"pack":{"description":"fixture","min_format":121,"max_format":121}}"#,
);
std::fs::create_dir_all(&dir).unwrap();
let mut zip = zip::ZipWriter::new(std::fs::File::create(dir.join("old.zip")).unwrap());
+124 -26
View File
@@ -14,6 +14,8 @@ pub struct EffectInstance {
pub amplifier: i32,
pub ambient: bool,
pub visible: bool,
/// `MobEffectInstance.showIcon`: whether the HUD and inventory list it.
pub show_icon: bool,
pub hidden: Option<Box<EffectInstance>>,
}
impl EffectInstance {
@@ -24,9 +26,18 @@ impl EffectInstance {
amplifier: amplifier.clamp(0, 255),
ambient: false,
visible: true,
show_icon: true,
hidden: None,
}
}
/// `new MobEffectInstance(effect, duration, amplifier, ambient, visible)`:
/// the icon shows exactly when the particles do.
pub fn with_flags(mut self, ambient: bool, visible: bool) -> Self {
self.ambient = ambient;
self.visible = visible;
self.show_icon = visible;
self
}
fn infinite(&self) -> bool {
self.duration == -1
}
@@ -71,6 +82,10 @@ impl EffectInstance {
self.visible = take.visible;
changed = true;
}
if take.show_icon != self.show_icon {
self.show_icon = take.show_icon;
changed = true;
}
changed
}
/// `MobEffectInstance.tickClient`: a client counts its effects down
@@ -98,6 +113,7 @@ impl EffectInstance {
self.amplifier = h.amplifier;
self.ambient = h.ambient;
self.visible = h.visible;
self.show_icon = h.show_icon;
self.hidden = h.hidden;
}
}
@@ -171,6 +187,57 @@ pub enum OmenTick {
/// It added this effect, then returned false.
Add(EffectInstance),
}
/// A saved player's effect: `MobEffectInstance.Details` under its `id`.
#[derive(Clone, Debug, PartialEq)]
pub struct SavedEffect {
pub id: String,
pub duration: i32,
pub amplifier: i32,
pub ambient: bool,
/// `show_particles`.
pub particles: bool,
/// `show_icon`.
pub icon: bool,
/// `hidden_effect`: the weaker, longer instance of the same effect that
/// comes back when this one ends.
pub hidden: Option<Box<SavedEffect>>,
}
impl SavedEffect {
/// Ambient off, particles and icon shown, nothing hidden: the codec's
/// defaults.
pub fn new(id: impl Into<String>, duration: i32, amplifier: i32) -> Self {
Self {
id: id.into(),
duration,
amplifier,
ambient: false,
particles: true,
icon: true,
hidden: None,
}
}
fn of(e: &EffectInstance) -> Self {
Self {
id: potions::namespaced(e.id).to_owned(),
duration: e.duration,
amplifier: e.amplifier,
ambient: e.ambient,
particles: e.visible,
icon: e.show_icon,
hidden: e.hidden.as_deref().map(|h| Box::new(Self::of(h))),
}
}
/// `new MobEffectInstance(effect, details)`: the hidden chain comes back
/// with it.
fn instance(&self, id: &'static str) -> EffectInstance {
let mut e = EffectInstance::new(id, self.duration, self.amplifier);
e.ambient = self.ambient;
e.visible = self.particles;
e.show_icon = self.icon;
e.hidden = self.hidden.as_deref().map(|h| Box::new(h.instance(id)));
e
}
}
#[derive(Clone, Debug, Default)]
pub struct PlayerEffects {
pub list: Vec<EffectInstance>,
@@ -203,39 +270,25 @@ impl PlayerEffects {
pub fn clear(&mut self) {
self.list.clear();
}
/// The effects of a saved player (`Player.active_effects`: id,
/// duration, amplifier, ambient, particles visible).
pub fn from_saved(saved: &[(String, i32, i32, bool, bool)]) -> Self {
/// The effects of a saved player (`Player.active_effects`), each with
/// the instances it hides.
pub fn from_saved(saved: &[SavedEffect]) -> Self {
let mut effects = Self::default();
for (id, duration, amplifier, ambient, particles) in saved {
if let Some(def) = potions::effect(id) {
let mut e = EffectInstance::new(def.id, *duration, *amplifier);
e.ambient = *ambient;
e.visible = *particles;
effects.add(e);
for s in saved {
if let Some(def) = potions::effect(&s.id) {
effects.add(s.instance(def.id));
}
}
effects
}
/// `Player.active_effects` for the save.
pub fn saved(&self) -> Vec<(String, i32, i32, bool, bool)> {
self.list
.iter()
.map(|e| {
(
potions::namespaced(e.id).to_owned(),
e.duration,
e.amplifier,
e.ambient,
e.visible,
)
})
.collect()
pub fn saved(&self) -> Vec<SavedEffect> {
self.list.iter().map(SavedEffect::of).collect()
}
/// ClientboundUpdateMobEffectPacket's flags without blending: ambient,
/// particles visible and the icon shown (always, for these effects).
/// particles visible and the icon shown.
pub fn flags(e: &EffectInstance) -> i8 {
i8::from(e.ambient) | i8::from(e.visible) << 1 | 4
i8::from(e.ambient) | i8::from(e.visible) << 1 | i8::from(e.show_icon) << 2
}
/// `LivingEntity.tickEffects` (server part) with the omens' effect ticks:
/// `omen` answers `BadOmenMobEffect` (every tick), `RaidOmenMobEffect`
@@ -536,6 +589,36 @@ mod tests {
);
}
/// Warden darkness (`new MobEffectInstance(DARKNESS, 260, 0, false,
/// false)`) shows neither particles nor its icon; a renewal that shows the
/// icon turns it back on (`MobEffectInstance.update`).
#[test]
fn an_effect_without_particles_hides_its_icon() {
let mut fx = PlayerEffects::default();
fx.add(EffectInstance::new("darkness", 260, 0).with_flags(false, false));
let darkness = fx.get("darkness").unwrap();
// ClientboundUpdateMobEffectPacket: no ambient, particles or icon bit.
assert_eq!(PlayerEffects::flags(darkness), 0);
assert!(!crate::hud_overlays::EffectIcon::from(darkness).show_icon);
assert!(fx.add(EffectInstance::new("darkness", 100, 0)));
assert_eq!(PlayerEffects::flags(fx.get("darkness").unwrap()), 2 | 4);
// Details keeps show_icon apart from show_particles.
let mut speed = EffectInstance::new("speed", 100, 0);
speed.show_icon = false;
let mut fx = PlayerEffects::default();
fx.add(speed);
let saved = fx.saved();
assert_eq!(
saved,
[SavedEffect {
icon: false,
..SavedEffect::new("minecraft:speed", 100, 0)
}]
);
let loaded = PlayerEffects::from_saved(&saved);
assert_eq!(PlayerEffects::flags(loaded.get("speed").unwrap()), 2);
}
#[test]
fn adding_counts_a_change_only_when_the_effect_changed() {
let mut fx = PlayerEffects::default();
@@ -572,6 +655,21 @@ mod tests {
assert_eq!(fx.get("speed").unwrap().duration, 49);
assert_eq!(fx.get("raid_omen").unwrap().amplifier, 1);
}
#[test]
fn a_reloaded_effect_falls_back_to_the_one_it_hid() {
// /effect give poison 6 s level II over poison 30 s level I, saved
// and loaded: when level II runs out, level I resumes with what is
// left of its 30 s.
let mut fx = PlayerEffects::default();
fx.add(EffectInstance::new("poison", 600, 0));
fx.add(EffectInstance::new("poison", 120, 1));
let mut loaded = PlayerEffects::from_saved(&fx.saved());
for _ in 0..120 {
loaded.tick();
}
let poison = loaded.get("poison").expect("level I resumed");
assert_eq!((poison.amplifier, poison.duration), (0, 480));
}
/// The only test here that registers mod effects (once per process).
#[test]
fn a_mod_effect_saves_under_its_namespace() {
@@ -587,11 +685,11 @@ mod tests {
1,
));
let saved = fx.saved();
assert_eq!(saved, [("ruby:glow".to_owned(), 600, 1, false, true)]);
assert_eq!(saved, [SavedEffect::new("ruby:glow", 600, 1)]);
assert_eq!(PlayerEffects::from_saved(&saved).list, fx.list);
// Without its mod the saved effect is dropped, as an unknown id is.
assert!(
PlayerEffects::from_saved(&[("gone:glow".into(), 600, 1, false, true)])
PlayerEffects::from_saved(&[SavedEffect::new("gone:glow", 600, 1)])
.list
.is_empty()
);
+37 -4
View File
@@ -33,7 +33,10 @@ struct Globals {
hdr_windows: array<vec4<f32>, 2>,
// HDR lighting: x how strongly the sky lights the world now (`Sky::sky_light_factor`),
// y what the sky and fog get: the same, but none under water.
// w: apply the no-pack HDR gain; HDR_WRITE_LIGHT independently keeps float alpha as coverage.
hdr_light: vec4<f32>,
hdr_settings: vec4<f32>,
hdr_shares: vec4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var atlas: texture_2d<f32>;
@@ -276,6 +279,19 @@ fn crumbling_uv(uv: vec2<f32>, packed_origin: vec2<f32>, sprite_size: vec2<f32>,
}
// CRUMBLING_UV_END
@fragment fn fs_entity(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
return entity_written_color(in, entity_color(in, front));
}
// Takes entity_color's result rather than calling it: FXC (DX12) fails with "argument pulled into
// unrelated predicate" when entity_color's discards sit a function call below the entry point.
fn entity_written_color(in: Out, color: vec4<f32>) -> vec4<f32> {
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 && in.overlay != 11u && in.overlay != 12u {
var mark = hdr_entity_mark(in);
if in.overlay == 4u { mark = hdr_scene_alpha(globals.hdr_light.y); }
return vec4(hdr_written(color.rgb, mark, globals.hdr_settings, globals.hdr_shares), color.a);
}
return color;
}
@fragment fn fs_entity_unmarked(in: Out, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
return entity_color(in, front);
}
fn entity_color(in: Out, front: bool) -> vec4<f32> {
@@ -358,15 +374,23 @@ fn shadow_color(in: Out) -> vec4<f32> {
if in.overlay == 6u { uv.y = in.wrap.x + fract(in.uv.y) * in.wrap.y; }
let texel = textureSample(atlas, atlas_sampler, uv);
if texel.a < 0.1 { discard; }
return vec4(0.0, 0.0, 0.0, hdr_entity_mark(in));
}
fn hdr_entity_mark(in: Out) -> f32 {
var uv = in.uv;
if in.overlay == 6u { uv.y = in.wrap.x + fract(in.uv.y) * in.wrap.y; }
let texel = textureSample(atlas, atlas_sampler, uv);
// Additive and procedural colour draws can survive where the legacy mark pass skips.
if texel.a < 0.1 { return 1.0; }
// Fog hides a light source and the light on a surface as it hides everything else.
let fogged = max(linear_fog(in.fog_distance.x, globals.fog.x, globals.fog.y),
linear_fog(in.fog_distance.y, globals.fog.z, globals.fog.w)) * globals.fog_color.a;
if in.tier == 0u {
return vec4(0.0, 0.0, 0.0, hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged)));
return hdr_scene_alpha(mix(in.scene, globals.hdr_light.y, fogged));
}
// A layer that fades (the warden's) shines by what's left of it.
let y = brightness(texel.rgb * in.color.rgb) * in.color.a * (1.0 - fogged);
return vec4(0.0, 0.0, 0.0, hdr_mark(in.tier, y));
return hdr_mark(in.tier, y);
}
fn brightness(c: vec3<f32>) -> f32 {
let l = pow(max(c, vec3(0.0)), vec3(2.2));
@@ -407,7 +431,11 @@ fn celestial_color(in: Out) -> vec4<f32> {
return textureSample(atlas, atlas_sampler, in.uv) * in.color * in.light_color;
}
@fragment fn fs_celestial(in: Out) -> @location(0) vec4<f32> {
return celestial_color(in);
let color = celestial_color(in);
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
return vec4(hdr_written(color.rgb, hdr_entity_mark(in), globals.hdr_settings, globals.hdr_shares), color.a);
}
return color;
}
// The sky's celestial pass (sun, moon, the End sky): fs_celestial's light,
@@ -424,7 +452,12 @@ fn celestial_color(in: Out) -> vec4<f32> {
let y = brightness(added);
// Only what's past the tier's window takes the mark: a soft edge keeps the sky's light,
// or it would come out darker than the sky around it.
if y < globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u] { return vec4(added, 0.0); }
let full = y >= globals.hdr_windows[1][clamp(tier, 1u, 4u) - 1u];
if HDR_WRITE_LIGHT && globals.hdr_light.w != 0.0 {
return vec4(hdr_written(added, select(1.0, hdr_mark(tier, y), full), globals.hdr_settings, globals.hdr_shares), color.a);
}
if HDR_WRITE_LIGHT { return vec4(added, color.a); }
if !full { return vec4(added, 0.0); }
return vec4(added, 1.0 - hdr_mark(tier, y));
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,297 @@
//! The Horizon Extras page, on the settings shell (`settings_shell`): which rows it has and what
//! pressing them does. Each one's value and what it does stay in `extras::Item`.
use crate::{
extras::{self, Item},
lang,
options_ui::{Event, Page},
settings::Settings,
settings_shell::{self as shell, Group, Output, Row, Screen, Sheet, Value},
};
const TITLE: &str = "horizon.pages.extras";
const ABOUT: &str = "horizon.pages.extras.tooltip";
/// The group of the mod that replaced an extra.
const MODS: &str = "horizon.pages.mods";
const ZOOM: &str = "horizon.extras.zoom";
const ZOOM_INSTALL: &str = "horizon.extras.zoom_mod_install";
const MOD_SETTINGS: &str = "horizon.mods.settings";
/// Its descriptions run long, as tooltips they were: most need more than two lines.
const NOTE_LINES: usize = 5;
/// The official native mod that took over from the Zoom extra (#8713).
const ZOOM_ID: &str = "zoom";
/// A row of the page.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExtraRow {
Item(Item),
/// The Zoom mod, at this place among the installed ones, on or off.
Zoom(usize),
/// Its own settings page.
ZoomSettings(usize),
/// It isn't installed: where to get it. A note, not a button: Options > Mods already has the
/// one way into the Mods page (`every_setting_and_page_is_reached_from_one_place`).
ZoomInstall,
}
fn item_row(item: Item, s: &Settings) -> Row<ExtraRow> {
let e = &s.extras;
let value = match (item.key_name(), item.switch(e)) {
(Some(key), _) => Value::Key(key, item.value(e)),
(None, Some(on)) => Value::Switch(on),
(None, None) => Value::Choice(item.value(e)),
};
// A setting of a switch that is off does nothing, so it is greyed out.
let live = item
.parent()
.is_none_or(|parent| parent.switch(e) != Some(false));
Row::new(ExtraRow::Item(item), item.caption_text(), value)
.about(item.description_text())
.live(live)
}
/// Zoom, the FPS counter, Keystrokes and the Minimap are official mods now; Extras keeps a way to
/// the one players look for here.
fn zoom_rows(s: &Settings) -> Vec<Row<ExtraRow>> {
let installed = &s.mods.installed;
let found = installed
.iter()
.position(|m| m.id == ZOOM_ID && m.native.is_some());
let Some(i) = found else {
let note = Row::new(ExtraRow::ZoomInstall, lang::get(ZOOM_INSTALL), Value::Note);
return vec![note.live(false)];
};
let about = installed[i].problem.as_ref();
let about = about.unwrap_or(&installed[i].description).clone();
let on = Value::Switch(s.mods.is_on(i));
let mut rows = vec![Row::new(ExtraRow::Zoom(i), lang::get(ZOOM), on).about(about)];
let page = installed[i].native.as_ref();
if page.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty()) {
let name = lang::get(MOD_SETTINGS);
rows.push(Row::new(ExtraRow::ZoomSettings(i), name, Value::Press));
}
rows
}
/// The page itself holds nothing: everything it shows is in the settings.
#[derive(Default)]
pub(crate) struct ExtrasScreen;
impl Screen for ExtrasScreen {
type Page = ();
type Row = ExtraRow;
fn title(&self) -> &'static str {
TITLE
}
fn note_lines(&self) -> usize {
NOTE_LINES
}
/// The rows read the extras, the mods and whether cosmetics are offered.
fn stamp(&self, s: &Settings) -> u64 {
shell::stamp_of(&(&s.extras, &s.mods, horizon_cosmetics::enabled()))
}
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
let mut groups = vec![Group {
name: MODS,
rows: zoom_rows(s),
}];
for row in extras::rows() {
match row {
Err(name) => groups.push(Group {
name,
rows: Vec::new(),
}),
Ok(item) => {
if let Some(group) = groups.last_mut() {
group.rows.push(item_row(item, s));
}
}
}
}
vec![Sheet {
id: (),
name: lang::get(TITLE),
about: lang::get(ABOUT),
groups,
}]
}
fn press(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Vec<Output> {
match row {
ExtraRow::Item(item) => match item.key_name() {
Some(key) => vec![Output::BindKey(key)],
None => {
item.cycle(&mut s.extras, back);
vec![Output::Event(Event::Changed)]
}
},
ExtraRow::Zoom(i) => {
s.mods.toggle(i);
vec![Output::Event(Event::Changed)]
}
ExtraRow::ZoomSettings(i) => vec![
Output::Event(Event::ButtonClick),
Output::Push(Page::NativeMod(i)),
Output::Event(Event::NativeSettings(i)),
],
ExtraRow::ZoomInstall => Vec::new(),
}
}
/// With the wheel extra on, the wheel over an item changes it, without a click; the mod's rows
/// let the list scroll.
fn wheel_step(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Option<Vec<Output>> {
let ExtraRow::Item(item) = row else {
return None;
};
item.cycle(&mut s.extras, back);
Some(vec![Output::Event(Event::Changed)])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings_shell::{Hosted, Shell};
const SIZE: (f32, f32) = (854.0, 480.0);
fn rows_of(s: &Settings) -> Vec<Row<ExtraRow>> {
let sheets = ExtrasScreen.sheets(s);
let groups = sheets.into_iter().flat_map(|sheet| sheet.groups);
groups.flat_map(|group| group.rows).collect()
}
/// Greyed exactly while its own switch is off, so it takes no press then.
#[test]
fn a_setting_of_a_switch_that_is_off_is_greyed_and_takes_no_press() {
let mut s = Settings::default();
let live = |s: &Settings| {
let wheel = ExtraRow::Item(Item::MouseTweaksWheel);
rows_of(s).iter().find(|r| r.id == wheel).unwrap().live
};
s.extras.mouse_tweaks = false;
assert!(!live(&s));
let mut shell = Shell::<ExtrasScreen>::default();
let row = ExtraRow::Item(Item::MouseTweaksWheel);
let at = shell.point_at(&s, SIZE, row);
let before = s.extras.mouse_tweaks_wheel;
assert!(
shell
.click(&mut s, at, SIZE, false, false, false)
.is_empty()
);
assert_eq!(s.extras.mouse_tweaks_wheel, before);
s.extras.mouse_tweaks = true;
assert!(live(&s));
assert!(
!shell
.click(&mut s, at, SIZE, false, false, false)
.is_empty()
);
assert_ne!(s.extras.mouse_tweaks_wheel, before);
}
#[test]
fn the_three_way_cosmetics_row_turns_both_ways() {
use horizon_cosmetics::prefs::Others;
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::CosmeticsOthers);
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
assert_eq!(s.extras.effective_others(), Others::Hidden);
screen.press(&mut s, row, false);
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
screen.press(&mut s, row, true);
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
}
/// The sort key binds instead of cycling; the wheel leaves it be.
#[test]
fn the_key_row_waits_for_a_key() {
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::SortKey);
let key = s.extras.sort_key.clone();
assert_eq!(
screen.press(&mut s, row, false),
vec![Output::BindKey(extras::SORT_KEY)]
);
screen.wheel_step(&mut s, row, true);
assert_eq!(s.extras.sort_key, key);
}
#[test]
fn the_wheel_flips_a_switch_and_leaves_the_mod_to_scroll() {
let mut s = Settings::default();
let mut screen = ExtrasScreen;
let row = ExtraRow::Item(Item::Sort);
assert_eq!(
screen.wheel_step(&mut s, row, true),
Some(vec![Output::Event(Event::Changed)])
);
assert!(s.extras.sort);
assert_eq!(screen.wheel_step(&mut s, ExtraRow::Zoom(0), true), None);
}
/// Extras has no Undo: a change leaves none to press or reach.
#[test]
fn a_change_on_extras_leaves_no_undo() {
use crate::settings_shell::{Columns, Target};
use winit::keyboard::KeyCode as K;
let mut s = Settings::default();
let mut shell = Shell::<ExtrasScreen>::default();
let at = shell.point_at(&s, SIZE, ExtraRow::Item(Item::Sort));
assert!(
!shell
.click(&mut s, at, SIZE, false, false, false)
.is_empty()
);
assert!(s.extras.sort && !shell.changed());
let cols = Columns::of(SIZE.0, SIZE.1).with_note_lines(NOTE_LINES);
let [x, y, ..] = cols.button(1);
assert!(
shell
.click(&mut s, (x + 2.0, y + 2.0), SIZE, false, false, false)
.is_empty()
);
assert!(s.extras.sort);
let reached = (0..100).any(|_| {
shell.key(&mut s, K::Tab, None, false, SIZE);
shell.focused() == Some(Target::Undo)
});
assert!(!reached);
}
/// A page with no page list has Done under the search field: Down goes to the first row
/// instead, from the keys and, as the frame declares it, from a controller's focus.
#[test]
fn down_from_the_search_field_lands_on_the_first_row() {
use crate::{hud::HudBatch, settings_shell::Columns, ui};
use winit::keyboard::KeyCode as K;
let atlas = crate::hud::HudAtlas::build(std::path::Path::new("/nonexistent")).unwrap();
let mut s = Settings::default();
let mut shell = Shell::<ExtrasScreen>::default();
let mut batch = HudBatch::new([1708, 960]);
shell.draw(&s, &mut batch, &atlas, (0.0, 0.0), false, None);
crate::widgets::end_frame(1);
let size = (batch.width, batch.height);
let cols = Columns::of(size.0, size.1).with_note_lines(NOTE_LINES);
// The Mods group's note can't take the focus: the first item can.
let first = ExtraRow::Item(Item::OwnNameTag);
let at = shell.point_at(&s, size, first);
let [x, y, w, h] = crate::widgets::linked_step(cols.search(), true).unwrap();
assert!(ui::hovered(at, x, y, w, h));
assert_eq!(crate::widgets::linked_step(cols.search(), false), None);
let [x, y, w, h] = cols.search();
shell.click(
&mut s,
(x + w / 2.0, y + h / 2.0),
size,
false,
false,
false,
);
shell.key(&mut s, K::ArrowDown, None, false, size);
assert_eq!(shell.focused_row(), Some(first));
}
}
@@ -0,0 +1,210 @@
// Still-fluid tiling follows Catnip/Ponder 1.0.82 FluidRenderHelper.renderTiledFace.
// Copyright (c) 2022 The Create Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use horizon_core::geometry::Direction;
/// Controls a still-fluid box. The tint is stack ARGB, independent of biome tint.
#[derive(Clone, Copy, Debug)]
pub struct NativeFluidRenderOptions {
pub argb: Option<u32>,
pub render_bottom: bool,
/// TextureAtlasSprite.uvShrinkRatio: 4 / the largest stitched atlas dimension.
/// Explicit because Horizon's block sprites occupy arrays rather than a stitched atlas.
pub uv_shrink_ratio: f32,
}
/// A corner before the caller's pose, with UVs within the still sprite, and its outward normal.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FluidVertex {
pub pos: [f32; 3],
pub uv: [f32; 2],
pub normal: [i8; 3],
}
/// Preserve sky light and increase block light to the fluid's luminosity (native light packing).
pub fn fluid_light(light: u32, luminosity: u8) -> u32 {
(light & 0xf0) | (((light >> 8 & 0xf0).max(u32::from(luminosity.min(15)) * 16)) << 8)
}
/// Emit whole tiled quads, or refuse invalid/unreasonably large geometry before emitting anything.
/// Negative coordinates and exact integer boundaries retain the source's local UV fractions.
pub fn fluid_box(
bounds: [f32; 6],
options: NativeFluidRenderOptions,
mut emit: impl FnMut(FluidVertex),
) -> bool {
if !options.uv_shrink_ratio.is_finite()
|| !(0.0..=1.0).contains(&options.uv_shrink_ratio)
|| bounds
.iter()
.any(|x| !x.is_finite() || x.abs() > 1_048_576.0)
|| (0..3).any(|i| bounds[i] > bounds[i + 3])
{
return false;
}
let tiles =
std::array::from_fn::<_, 3, _>(|i| (bounds[i + 3].ceil() - bounds[i].floor()) as u64);
let quads = tiles[0] * tiles[1] * 2
+ tiles[1] * tiles[2] * 2
+ tiles[0] * tiles[2] * if options.render_bottom { 2 } else { 1 };
if quads > 65_536 {
return false;
}
let [x0, y0, z0, x1, y1, z1] = bounds;
for (side, face) in [
(Direction::Down, [x0, z0, x1, z1, y0]),
(Direction::Up, [x0, z0, x1, z1, y1]),
(Direction::North, [x0, y0, x1, y1, z0]),
(Direction::South, [x0, y0, x1, y1, z1]),
(Direction::West, [z0, y0, z1, y1, x0]),
(Direction::East, [z0, y0, z1, y1, x1]),
] {
if side != Direction::Down || options.render_bottom {
tiled_face(&mut emit, side, face, options.uv_shrink_ratio);
}
}
true
}
fn tiled_face(out: &mut impl FnMut(FluidVertex), side: Direction, face: [f32; 5], ratio: f32) {
let [a0, b0, a1, b1, plane] = face;
let positive = matches!(side, Direction::Up | Direction::South | Direction::East);
let shrink = ratio * 0.25;
let lerp = |v| v + shrink * (0.5 - v);
let mut a = a0;
while a < a1 {
let end_a = (a.floor() + 1.0).min(a1);
let (u0, u1) = if matches!(side, Direction::North | Direction::East) {
(end_a.ceil() - end_a, end_a.ceil() - a)
} else {
(a - a.floor(), end_a - a.floor())
};
let (u0, u1) = (lerp(u0), lerp(u1));
let mut b = b0;
while b < b1 {
let end_b = (b.floor() + 1.0).min(b1);
let (v0, v1) = if side == Direction::Up {
(b - b.floor(), end_b - b.floor())
} else {
(end_b.ceil() - end_b, end_b.ceil() - b)
};
let (v0, v1) = (lerp(v0), lerp(v1));
let positions = match side {
Direction::West | Direction::East => {
let (near, far) = if positive { (end_a, a) } else { (a, end_a) };
[
[plane, end_b, near],
[plane, b, near],
[plane, b, far],
[plane, end_b, far],
]
}
Direction::North | Direction::South => {
let (near, far) = if positive { (a, end_a) } else { (end_a, a) };
[
[near, end_b, plane],
[near, b, plane],
[far, b, plane],
[far, end_b, plane],
]
}
Direction::Down | Direction::Up => {
let (near, far) = if positive { (b, end_b) } else { (end_b, b) };
[
[a, plane, near],
[a, plane, far],
[end_a, plane, far],
[end_a, plane, near],
]
}
};
let normal = match side {
Direction::Down => [0, -1, 0],
Direction::Up => [0, 1, 0],
Direction::North => [0, 0, -1],
Direction::South => [0, 0, 1],
Direction::West => [-1, 0, 0],
Direction::East => [1, 0, 0],
};
for (pos, uv) in positions
.into_iter()
.zip([[u0, v0], [u0, v1], [u1, v1], [u1, v0]])
{
out(FluidVertex { pos, uv, normal });
}
b = end_b;
}
a = end_a;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_tiling_is_outward_and_does_not_inset_geometry() {
let bounds = [-1.25, 0.125, -0.25, 1.5, 1.125, 0.75];
let mut result = Vec::new();
assert!(fluid_box(
bounds,
NativeFluidRenderOptions {
argb: None,
render_bottom: false,
uv_shrink_ratio: 4.0 / 2048.0,
},
|v| result.push(v),
));
assert_eq!(result.len(), (4 * 2 * 2 + 2 * 2 * 2 + 4 * 2) * 4);
for quad in result.as_chunks::<4>().0 {
let [a, b, c, _] = quad.map(|v| glam::Vec3::from_array(v.pos));
assert!(
(b - a)
.cross(c - a)
.dot(glam::Vec3::from_array(quad[0].normal.map(f32::from)))
> 0.0
);
assert!(
quad.iter()
.all(|v| (0..3).all(|i| v.pos[i] >= bounds[i] && v.pos[i] <= bounds[i + 3]))
);
}
assert!(
result
.iter()
.any(|v| v.pos == [bounds[0], bounds[4], bounds[2]])
);
assert!(!fluid_box(
[0.0, 0.0, 0.0, 1_000.0, 1_000.0, 1_000.0],
NativeFluidRenderOptions {
argb: None,
render_bottom: true,
uv_shrink_ratio: 0.0,
},
|_| panic!("invalid geometry must fail before emitting"),
));
}
#[test]
fn luminosity_changes_only_block_light() {
assert_eq!(fluid_light(0x20a0, 15), 0xf0a0);
assert_eq!(fluid_light(0x25a5, 15), 0xf0a0);
assert_eq!(fluid_light(0xe030, 3), 0xe030);
}
}
+55 -16
View File
@@ -9,6 +9,7 @@ use crate::{
environment::Sky,
world::World,
};
use horizon_core::random::{LegacyRandom, RandomSource};
use std::path::Path;
/// `FogType` of the camera position.
@@ -64,7 +65,7 @@ impl FogTables {
}
/// Fog state that evolves over time.
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Debug)]
pub struct FogState {
/// `LocalPlayer.waterVisionTime` (0..600).
pub water_vision_time: i32,
@@ -72,24 +73,28 @@ pub struct FogState {
pub rain_fog_multiplier: f32,
/// `LightmapRenderStateExtractor.blockLightFlicker`.
pub block_light_flicker: f32,
flicker_seed: u64,
/// `LightmapRenderStateExtractor.random`: `RandomSource.create()`.
flicker_random: LegacyRandom,
}
impl Default for FogState {
fn default() -> Self {
Self {
water_vision_time: 0,
rain_fog_multiplier: 0.0,
block_light_flicker: 0.0,
flicker_random: crate::random_source::create(),
}
}
}
impl FogState {
fn next_float(&mut self) -> f32 {
// splitmix64: the vanilla source is unseeded `RandomSource.create()`.
self.flicker_seed = self.flicker_seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.flicker_seed;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
((z ^ (z >> 31)) >> 40) as f32 / (1u32 << 24) as f32
}
/// `LightmapRenderStateExtractor.tick`: the torch light flickers.
pub fn tick_flicker(&mut self) {
let r = &mut self.flicker_random;
let (a, b, c, d) = (
self.next_float(),
self.next_float(),
self.next_float(),
self.next_float(),
r.next_float(),
r.next_float(),
r.next_float(),
r.next_float(),
);
self.block_light_flicker += (a - b) * c * d * 0.1;
self.block_light_flicker *= 0.9;
@@ -413,6 +418,12 @@ pub fn clear_distance_fog(fog: &mut FogData, input: &FogInputs) {
(fog.environmental_start, fog.environmental_end) = (1e9, 2e9);
}
}
/// What `AtmosphericFogEnvironment.updateRainFogState` eases the rain fog toward: the rain, where
/// the sky reaches the eye, halved in a biome without precipitation.
pub fn rain_fog_target(rain: f32, sky_light: u8, biome_rains: bool) -> f32 {
let sky = ((f32::from(sky_light) - 8.0) / 7.0).clamp(0.0, 1.0);
rain * sky * if biome_rains { 1.0 } else { 0.5 }
}
/// Whether the frame draws no sky, only the clear to the fog colour: `LevelRenderer.addSkyPass`
/// leaves the sky out in lava and powder snow and while blindness or darkness is on (`Camera`'s
/// `doesMobEffectBlockSky`), and `GameRenderer.renderLevel` gives `LevelRenderer.render`
@@ -428,10 +439,14 @@ pub fn update_rain_fog(
biome_rains: bool,
delta_ticks: f32,
) {
let sky = ((f32::from(sky_light) - 8.0) / 7.0).clamp(0.0, 1.0);
let target = rain * sky * if biome_rains { 1.0 } else { 0.5 };
let target = rain_fog_target(rain, sky_light, biome_rains);
state.rain_fog_multiplier += (target - state.rain_fog_multiplier) * delta_ticks * 0.2;
}
/// The rain fog at its target at once: a frozen world under `HORIZON_STILL`, where the rain sits
/// at its level and easing would wait for ticks that do not come.
pub fn settle_rain_fog(state: &mut FogState, rain: f32, sky_light: u8, biome_rains: bool) {
state.rain_fog_multiplier = rain_fog_target(rain, sky_light, biome_rains);
}
fn rgb(c: u32) -> [f32; 3] {
[
(c >> 16 & 255) as f32 / 255.0,
@@ -726,6 +741,30 @@ mod tests {
assert_eq!(clear.environmental_end, settled.environmental_end);
assert_eq!(clear.color, settled.color);
}
/// Settling puts the rain fog at its target exactly, where a tick of easing moves it a fifth
/// of the way, as before.
#[test]
fn settled_rain_fog_is_its_target_and_easing_is_unchanged() {
assert_eq!(
rain_fog_target(1.0, 15, false),
0.5,
"a dry biome halves it"
);
assert_eq!(rain_fog_target(1.0, 8, true), 0.0, "no sky, no rain fog");
let start = FogState {
rain_fog_multiplier: 0.1,
..FogState::default()
};
let mut settled = start.clone();
settle_rain_fog(&mut settled, 0.5, 15, true);
assert_eq!(settled.rain_fog_multiplier, 0.5);
let mut eased = start;
update_rain_fog(&mut eased, 0.5, 15, true, 1.0);
assert_eq!(
eased.rain_fog_multiplier.to_bits(),
(0.1f32 + (0.5f32 - 0.1) * 1.0 * 0.2).to_bits()
);
}
#[test]
fn rain_fog_pulls_fog_in() {
let sky = Sky {
+135 -32
View File
@@ -126,11 +126,16 @@ struct Cache {
random: horizon_core::random::LegacyRandom,
}
pub struct Font {
/// The atlas rects of the sheets under `textures/font`.
/// The atlas rects of every texture, by resource name: a bitmap provider's
/// `file` may name any texture, not only one under `textures/font`.
sheets: HashMap<String, Rect>,
missing: Glyph,
white: Rect,
root: PathBuf,
/// The game's own resources, below a pack overlay: a font file the overlay
/// lacks (a pack adds providers over vanilla's, or the overlay left a
/// directory out) is read from here, as vanilla's resource stack does.
fallback: Option<PathBuf>,
/// Atlas size and the reserved dynamic region (y, height).
atlas: (u32, u32),
region: (u32, u32),
@@ -199,6 +204,13 @@ fn parse_hex(text: &[u8], out: &mut HashMap<u32, HexGlyph>) {
out.insert(codepoint, HexGlyph { rows, left, right });
}
}
/// A resource file's bytes, from `root` or, when the overlay lacks it, from
/// the game's own resources below (`defaults`).
fn read_resource(root: &Path, defaults: Option<&Path>, path: &str) -> Option<Vec<u8>> {
std::fs::read(root.join(path))
.ok()
.or_else(|| defaults.and_then(|d| std::fs::read(d.join(path)).ok()))
}
fn read_json(root: &Path, path: &str) -> Option<serde_json::Value> {
serde_json::from_slice(&std::fs::read(root.join(path)).ok()?).ok()
}
@@ -281,6 +293,7 @@ fn hex_advance(h: &HexGlyph) -> f32 {
/// pixels, copied into the atlas when first drawn.
fn load_bitmap(
root: &Path,
defaults: Option<&Path>,
p: &serde_json::Value,
sheets: &HashMap<String, Rect>,
pixels: &[u8],
@@ -290,22 +303,36 @@ fn load_bitmap(
let mut out = HashMap::new();
let file = p["file"].as_str().unwrap_or("");
let (ns, name) = file.split_once(':').unwrap_or(("minecraft", file));
let in_atlas = (ns == "minecraft")
.then(|| sheets.get(name.trim_end_matches(".png")).copied())
.flatten();
let name = name.trim_end_matches(".png");
// The atlas names the default namespace's textures by their path and every
// other namespace's as `namespace:path` (HudAtlas::build).
let atlas_name = if ns == "minecraft" {
name.to_owned()
} else {
format!("{ns}:{name}")
};
let in_atlas = sheets.get(&atlas_name).copied();
// The sheet: its size, and its pixels when they are not in the atlas.
let (sw, sh, sheet) = match in_atlas {
Some(rect) => {
let (w, h) = rect.texels(atlas);
(w, h, None)
}
None => match crate::hud::decode(&root.join(resource_path(file, "textures", ""))) {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
None => {
let path = resource_path(file, "textures", "");
let sheet = crate::hud::decode(&root.join(&path)).or_else(|first| {
defaults
.and_then(|d| crate::hud::decode(&d.join(&path)).ok())
.ok_or(first)
});
match sheet {
Ok((w, h, rgba)) => (w, h, Some(Arc::new((w, rgba)))),
Err(_) => {
log::warn!("font sheet {file} could not be read");
return out;
}
}
},
}
};
let rows: Vec<Vec<char>> = p["chars"]
.as_array()
@@ -382,11 +409,13 @@ fn load_bitmap(
}
/// `TrueTypeGlyphProviderDefinition.load`: a TrueType file (not CFF) under
/// `font/`, its Unicode characters but the `skip` ones.
fn load_true_type(root: &Path, p: &serde_json::Value) -> Option<TrueType> {
fn load_true_type(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> Option<TrueType> {
let file = p["file"].as_str()?;
let data = std::fs::read(root.join(resource_path(file, "font", "")))
.map_err(|e| log::warn!("font file {file}: {e}"))
.ok()?;
let path = resource_path(file, "font", "");
let Some(data) = read_resource(root, defaults, &path) else {
log::warn!("font file {file} could not be read");
return None;
};
// FT_Get_Font_Format must say TrueType: CFF outlines ("OTTO") are refused.
if data.starts_with(b"OTTO") {
log::warn!("font file {file} is not in TTF format");
@@ -486,13 +515,16 @@ impl TrueType {
)
}
}
fn load_hex(root: &Path, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
fn load_hex(root: &Path, defaults: Option<&Path>, p: &serde_json::Value) -> HashMap<u32, HexGlyph> {
let mut glyphs = HashMap::new();
let Some(file) = p["hex_file"].as_str() else {
return glyphs;
};
let path = root.join(resource_path(file, "", "").replace("//", "/"));
let Ok(zip) = Zip::open(&path) else {
let path = resource_path(file, "", "").replace("//", "/");
let Some(zip) = Zip::open(&root.join(&path))
.ok()
.or_else(|| defaults.and_then(|d| Zip::open(&d.join(&path)).ok()))
else {
return glyphs;
};
let names: Vec<String> = zip
@@ -603,7 +635,24 @@ impl Font {
region: (u32, u32),
white: Rect,
) -> Self {
Self::load_with(root, sprites, pixels, atlas, region, white, false)
Self::load_with(root, None, sprites, pixels, atlas, region, white, false)
}
/// [`Font::load`] over a pack overlay: a sheet, TrueType file or Unihex
/// file the overlay lacks is read from `defaults`, the game's own
/// resources, as vanilla's resource stack does. (Definitions stay the
/// overlay's: a pack's filter may hide one from a lower pack.)
#[allow(clippy::too_many_arguments)]
pub fn load_over(
root: &Path,
defaults: &Path,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
region: (u32, u32),
white: Rect,
) -> Self {
let fallback = (root != defaults).then_some(defaults);
Self::load_with(root, fallback, sprites, pixels, atlas, region, white, false)
}
/// A server measures text but never draws it. Decode bitmap sheets only
/// to obtain advances, then release their pixels; never rasterize glyphs.
@@ -616,10 +665,21 @@ impl Font {
w: 1,
h: 1,
};
Self::load_with(root, &HashMap::new(), &[], (1, 1), (0, 0), white, true)
Self::load_with(
root,
None,
&HashMap::new(),
&[],
(1, 1),
(0, 0),
white,
true,
)
}
#[allow(clippy::too_many_arguments)]
fn load_with(
root: &Path,
fallback: Option<&Path>,
sprites: &HashMap<String, Rect>,
pixels: &[u8],
atlas: (u32, u32),
@@ -627,11 +687,7 @@ impl Font {
white: Rect,
metrics_only: bool,
) -> Self {
let sheets = sprites
.iter()
.filter(|(k, _)| k.starts_with("font/"))
.map(|(k, v)| (k.clone(), *v))
.collect();
let sheets = sprites.clone();
let font = Self {
sheets,
missing: Glyph {
@@ -645,6 +701,7 @@ impl Font {
},
white,
root: root.to_owned(),
fallback: fallback.map(|p| p.to_owned()),
atlas,
region,
metrics_only,
@@ -656,11 +713,8 @@ impl Font {
pending: Vec::new(),
full: false,
},
random: horizon_core::random::LegacyRandom::new(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as i64),
),
// `Font.random`: `RandomSource.create()`.
random: crate::random_source::create(),
}),
generation: std::cell::Cell::new(next_generation()),
uniform: false,
@@ -698,6 +752,7 @@ impl Font {
Filter::of(p),
load_bitmap(
&self.root,
self.fallback.as_deref(),
p,
&self.sheets,
pixels,
@@ -708,7 +763,7 @@ impl Font {
"unihex" => Some(Provider::Unihex(Filter::of(p), p.clone(), None)),
"ttf" => Some(Provider::TrueType(
Filter::of(p),
Box::new(load_true_type(&self.root, p)?),
Box::new(load_true_type(&self.root, self.fallback.as_deref(), p)?),
)),
_ => None,
})
@@ -868,7 +923,8 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
if let Some(h) = glyphs.get(&u32::from(c)).copied()
&& !(non_fishy && fishy_advance(hex_advance(&h)))
{
@@ -1026,7 +1082,8 @@ impl Font {
}
}
Provider::Unihex(filter, def, glyphs) if filter.passes(uniform, japanese) => {
let glyphs = glyphs.get_or_insert_with(|| load_hex(&self.root, def));
let glyphs = glyphs
.get_or_insert_with(|| load_hex(&self.root, self.fallback.as_deref(), def));
if let Some(h) = glyphs.get(&u32::from(c)) {
return Some(hex_advance(h));
}
@@ -1512,6 +1569,52 @@ mod tests {
assert!(texels.iter().any(|t| t[0] == 255));
}
/// A bitmap provider's `file` can name any texture. When the atlas has that
/// texture (HUD packs keep their glyph sheets outside `textures/font`), the
/// glyphs are read from it instead of being copied into the small run-time
/// region one by one; the region then cannot fill up and turn later glyphs
/// into the missing one, whose advance shifts the rest of a HUD line.
#[test]
fn a_bitmap_sheet_outside_font_is_read_from_the_atlas() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
let (_, _, pixels) = crate::hud::decode_bytes(&sheet).unwrap();
let rect = Rect {
u0: 0.0,
v0: 0.0,
u1: 1.0,
v1: 1.0,
w: 16,
h: 8,
};
for (ns, file, key) in [
("minecraft", "minecraft:hud/sheet.png", "hud/sheet"),
(
"hudengine",
"hudengine:font/sheet.png",
"hudengine:font/sheet",
),
] {
let def = format!(
r#"{{"providers":[{{"type":"bitmap","file":"{file}","ascent":7,"chars":["ab"]}}]}}"#
);
// The image is only in the atlas: the pack's copy is not on disk.
let root = root(
ns,
&[("assets/minecraft/font/default.json", def.as_bytes())],
);
let sprites = HashMap::from([(key.to_string(), rect)]);
let font = Font::load(&root, &sprites, &pixels, (16, 8), (0, 8), white());
let _ = std::fs::remove_dir_all(&root);
assert_eq!(font.glyph('a').advance, 3.0, "{ns}");
assert_eq!(font.glyph('b').advance, 7.0, "{ns}");
assert!(font.glyph('a').rect.is_some(), "{ns}");
assert!(
font.take_patches().is_empty(),
"{ns}: the atlas sheet is not copied into the run-time region"
);
}
}
#[test]
fn styles_pick_their_font_and_unknown_fonts_draw_missing_glyphs() {
let sheet = png(16, 8, |x, _| x < 2 || (8..14).contains(&x));
File diff suppressed because it is too large. Load diff
+12 -1
View File
@@ -879,6 +879,7 @@ mod tests {
solid,
solid_slots: u64::from(solid) * 4,
packed: false,
packed_far: false,
sides: crate::mesher::Sides::unsorted(solid),
translucent,
sort: super::super::translucency::SortState::new(Vec::new()),
@@ -1047,7 +1048,15 @@ mod tests {
hdr: None,
}
};
let mut flat = flat::Flat::new(&device, &terrain_layout, 4 * GRAIN, 0, pipeline, solid);
let mut flat = flat::Flat::new(
&device,
&terrain_layout,
4 * GRAIN,
0,
false,
pipeline,
solid,
);
let stride = std::mem::size_of::<crate::mesher::TerrainVertex>() as u64;
let mut translucent = Arena::translucent(&device, stride, 4 * GRAIN);
translucent.page_limit = 4 * GRAIN;
@@ -1083,6 +1092,7 @@ mod tests {
solid: 64,
solid_slots: 64 * 4,
packed: false,
packed_far: false,
sides: crate::mesher::Sides::unsorted(64),
translucent: 128,
sort: super::super::translucency::SortState::new(Vec::new()),
@@ -1310,6 +1320,7 @@ mod tests {
solid,
solid_slots: u64::from(solid) * 4,
packed: false,
packed_far: false,
sides: crate::mesher::Sides::unsorted(solid),
translucent,
sort: super::super::translucency::SortState::new(Vec::new()),
@@ -67,6 +67,7 @@ fn copy(mesh: &SectionMesh) -> SectionMesh {
sides: mesh.sides,
translucent: mesh.translucent.clone(),
packed: mesh.packed.clone(),
packed_far: mesh.packed_far,
solid_tints: Vec::new(),
visibility: mesh.visibility,
block_entities: mesh.block_entities.clone(),
@@ -199,6 +200,8 @@ pub struct FrameInputs {
pub skybox: bool,
pub end_sky: bool,
pub end_sky_sprite: Option<[f32; 4]>,
#[serde(default)]
pub boss_world_fog: bool,
pub environment_fog: Option<(f32, f32)>,
pub water_fog: [f32; 3],
pub fog: Option<FogData>,
@@ -414,6 +417,7 @@ impl Renderer {
skybox: frame.skybox,
end_sky: frame.end_sky,
end_sky_sprite: frame.end_sky_sprite,
boss_world_fog: frame.boss_world_fog,
environment_fog: frame.environment_fog,
water_fog: frame.water_fog,
fog: frame.fog,
@@ -510,6 +514,7 @@ impl Scene {
skybox: f.skybox,
end_sky: f.end_sky,
end_sky_sprite: f.end_sky_sprite,
boss_world_fog: f.boss_world_fog,
environment_fog: f.environment_fog,
water_fog: f.water_fog,
fog: f.fog,
@@ -0,0 +1,436 @@
//! A resource pack's `core/sky` and `core/stars` shaders: the overworld's sky disc and stars drawn
//! with the pack's GLSL (`SkyRenderer.renderSkyDisc`, `renderStars`), as a pack that adds a haze
//! to the horizon or tints the night sky expects. Without one the sky draws natively (`fs_sky` in
//! terrain.wgsl, stars.rs).
use glam::Mat4;
use horizon_shaders::{CoreProgram, Glue};
use wgpu::util::DeviceExt;
/// `DefaultVertexFormat.POSITION`: the disc's and the stars' vertices.
pub(crate) const GLUE: Glue = Glue {
inputs: &[("Position", "vec3")],
prelude: "layout(location = 0) in vec3 hz_position;",
setup: "Position = hz_position;",
// 26.3's ProjMat already has wgpu's reversed 0 to 1 depth (Projection: zZeroToOne).
finish: "",
vertex_id: "int(gl_VertexIndex)",
};
/// Each uniform block's offset in the one buffer (`core_shader::BLOCKS` order).
const BLOCK_OFFSETS: [u64; 4] = [0, 256, 512, 768];
const BLOCK_SIZES: [u64; 4] = [48, 64, 160, 48];
/// `SkyRenderer.buildSkyDisc(16)`: a fan around the point 16 above the camera, out to 512 every
/// 45 degrees, as a triangle list.
pub(crate) fn disc_triangles() -> Vec<[f32; 3]> {
let rim: Vec<[f32; 3]> = (-180..=180)
.step_by(45)
.map(|i: i32| {
let a = (i as f32).to_radians();
[512.0 * a.cos(), 16.0, 512.0 * a.sin()]
})
.collect();
rim.windows(2)
.flat_map(|edge| [[0.0, 16.0, 0.0], edge[0], edge[1]])
.collect()
}
/// One of the sky's pack pipelines with its vertices and uniforms.
struct Part {
pipeline: wgpu::RenderPipeline,
group: wgpu::BindGroup,
uniform: wgpu::Buffer,
vertices: wgpu::Buffer,
count: u32,
}
impl Part {
fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
program: &CoreProgram,
name: &str,
blend: Option<wgpu::BlendState>,
triangles: &[[f32; 3]],
) -> Option<Self> {
// A pack's shader is untrusted: the backend failing to compile it is an internal error.
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
let module = |stage, module: &naga::Module| {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(&format!("Resource pack {name} ({stage})")),
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module.clone())),
})
};
let vertex = module("vertex", &program.vertex);
let fragment = module("fragment", &program.fragment);
// Vanilla's SKY and STARS pipelines have no samplers: a pack that reads one is not used.
let entries: Vec<wgpu::BindGroupLayoutEntry> = (0..4)
.map(|binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(BLOCK_SIZES[binding as usize]),
},
count: None,
})
.collect();
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some(name),
entries: &entries,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(name),
size: 1024,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let resources: Vec<wgpu::BindGroupEntry> = (0..4)
.map(|i| wgpu::BindGroupEntry {
binding: i as u32,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &uniform,
offset: BLOCK_OFFSETS[i],
size: wgpu::BufferSize::new(BLOCK_SIZES[i]),
}),
})
.collect();
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(name),
layout: &layout,
entries: &resources,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(name),
bind_group_layouts: &[Some(&layout)],
..Default::default()
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(name),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vertex,
entry_point: Some("main"),
compilation_options: Default::default(),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: 12,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
})],
},
primitive: wgpu::PrimitiveState::default(),
// The sky is drawn first, behind everything: no depth test, no depth write.
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &fragment,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
let internal = pollster::block_on(internal.pop());
if let Some(e) = internal.or(pollster::block_on(validation.pop())) {
log::warn!("the resource pack's {name} shader is not used: {e}");
return None;
}
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(name),
contents: bytemuck::cast_slice(triangles),
usage: wgpu::BufferUsages::VERTEX,
});
Some(Self {
pipeline,
group,
uniform,
vertices,
count: triangles.len() as u32,
})
}
fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.group, &[]);
pass.set_vertex_buffer(0, self.vertices.slice(..));
pass.draw(0..self.count, 0..1);
}
}
/// The packs' sky disc and stars pipelines, each present when a pack replaces that shader.
pub(super) struct CoreSky {
disc: Option<Part>,
stars: Option<Part>,
}
impl CoreSky {
/// `None` when neither shader is replaced or neither builds.
pub(super) fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
disc: Option<&CoreProgram>,
stars: Option<&CoreProgram>,
) -> Option<Self> {
let disc = disc.and_then(|program| {
// RenderPipelines.SKY: ColorTargetState.DEFAULT, no blending.
Part::new(device, format, program, "sky", None, &disc_triangles())
});
let stars = stars.and_then(|program| {
// RenderPipelines.STARS: BlendFunction.OVERLAY (src alpha and one; alpha one, zero).
let overlay = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::Zero,
operation: wgpu::BlendOperation::Add,
},
};
let triangles = super::stars::star_triangles();
Part::new(device, format, program, "stars", Some(overlay), &triangles)
});
(disc.is_some() || stars.is_some()).then_some(Self { disc, stars })
}
pub(super) fn draws_disc(&self) -> bool {
self.disc.is_some()
}
pub(super) fn draws_stars(&self) -> bool {
self.stars.is_some()
}
/// Uploads the disc's and the stars' blocks: `sky` and `stars` from [`uniforms`].
pub(super) fn prepare(
&self,
uploads: &mut super::upload::Uploads,
encoder: &mut wgpu::CommandEncoder,
sky: &[u8],
stars: Option<&[u8]>,
) {
if let Some(disc) = &self.disc {
uploads.buffer(encoder, &disc.uniform, 0, sky);
}
if let (Some(part), Some(data)) = (&self.stars, stars) {
uploads.buffer(encoder, &part.uniform, 0, data);
}
}
/// The disc, in place of the native sky colour. The caller sets its own group 0 again after.
pub(super) fn draw_disc(&self, pass: &mut wgpu::RenderPass<'_>) {
if let Some(disc) = &self.disc {
disc.draw(pass);
}
}
/// The stars, in place of the native ones. The caller sets its own group 0 again after.
pub(super) fn draw_stars(&self, pass: &mut wgpu::RenderPass<'_>) {
if let Some(stars) = &self.stars {
stars.draw(pass);
}
}
}
/// The std140 blocks for one sky draw: `Globals`, `Projection` (`projection`, far enough for the
/// disc's 512-block rim), `DynamicTransforms` (`model_view`: the camera's rotation, times the
/// stars' rotation for them; `color`: the sky colour, or the star brightness in every channel)
/// and `Fog` (`fog_color`, `fog`: environmental start and end, render distance start and end,
/// sky end, clouds end).
pub(super) fn uniforms(
globals: [u8; 48],
projection: Mat4,
model_view: Mat4,
color: [f32; 4],
fog_color: [f32; 4],
fog: [f32; 6],
) -> Vec<u8> {
let mut data = vec![0f32; 256];
data[64..80].copy_from_slice(&projection.to_cols_array());
let transforms = 128;
data[transforms..transforms + 16].copy_from_slice(&model_view.to_cols_array());
data[transforms + 16..transforms + 32].copy_from_slice(&Mat4::IDENTITY.to_cols_array());
data[transforms + 32..transforms + 36].copy_from_slice(&color);
data[192..196].copy_from_slice(&fog_color);
data[196..202].copy_from_slice(&fog);
let mut bytes: Vec<u8> = bytemuck::cast_slice(&data).to_vec();
bytes[..48].copy_from_slice(&globals);
bytes
}
#[cfg(test)]
mod tests {
use super::*;
/// `buildSkyDisc(16)`: eight triangles from the point above the camera to the rim, which
/// starts and ends at x -512 (cos 180°) and passes through z -512, x 512 and z 512.
#[test]
fn the_disc_is_vanillas_fan_as_triangles() {
let triangles = disc_triangles();
assert_eq!(triangles.len(), 24);
assert!(triangles.chunks(3).all(|t| t[0] == [0.0, 16.0, 0.0]));
let rim: Vec<[f32; 3]> = triangles.chunks(3).map(|t| t[1]).collect();
let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-3);
assert!(near(rim[0], [-512.0, 16.0, 0.0]));
assert!(near(rim[2], [0.0, 16.0, -512.0]));
assert!(near(rim[4], [512.0, 16.0, 0.0]));
assert!(near(rim[6], [0.0, 16.0, 512.0]));
assert!(near(triangles[23], [-512.0, 16.0, 0.0]));
}
/// The prepared resources (`HORIZON_RESOURCES`, or `.local/horizon-client/<version>`), where
/// vanilla's sky shader and its includes are.
fn resources() -> std::path::PathBuf {
std::env::var_os("HORIZON_RESOURCES")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION)
})
}
/// Vanilla's `sky.vsh` with a pack's fragment shader that paints `ColorModulator` (the sky
/// colour) where the disc is: looking at the horizon, the top half of the target is the
/// disc, the bottom half stays clear.
#[test]
fn a_pack_sky_shader_paints_the_disc_above_the_horizon() {
let root = resources();
let Ok(vertex) =
std::fs::read_to_string(root.join("assets/minecraft/shaders/core/sky.vsh"))
else {
eprintln!("skipping: no prepared resources at {}", root.display());
return;
};
let include = |id: &str| {
let (namespace, path) = id.split_once(':')?;
std::fs::read_to_string(root.join(format!("assets/{namespace}/{path}"))).ok()
};
let fragment = "#version 330\n#include <minecraft:dynamictransforms.glsl>\n\
layout(location = 0) out vec4 fragColor;\n\
void main() {\n fragColor = ColorModulator;\n}\n";
let program = horizon_shaders::translate_core(&vertex, fragment, &[], &include, &GLUE)
.expect("the sky shaders translate");
let instance = crate::gfx::test_gpu::instance();
let Some((device, queue)) =
pollster::block_on(instance.request_adapter(&Default::default()))
.ok()
.and_then(|adapter| {
pollster::block_on(adapter.request_device(&Default::default())).ok()
})
else {
eprintln!("no GPU: skipping");
return;
};
const SIZE: u32 = 64;
let format = wgpu::TextureFormat::Rgba8Unorm;
let sky = CoreSky::new(&device, format, Some(&program), None).expect("the sky pipeline");
let projection = crate::gfx::world_projection(90.0, 1.0, 2048.0);
let data = uniforms(
[0; 48],
projection,
Mat4::IDENTITY,
[0.25, 0.5, 1.0, 1.0],
[0.0; 4],
[1e6; 6],
);
queue.write_buffer(&sky.disc.as_ref().unwrap().uniform, 0, &data);
let target = |format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | usage,
view_formats: &[],
})
};
let color = target(format, wgpu::TextureUsages::COPY_SRC);
let depth = target(
wgpu::TextureFormat::Depth32Float,
wgpu::TextureUsages::empty(),
);
let read = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: u64::from(SIZE * SIZE * 4),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&Default::default());
{
let color_view = color.create_view(&Default::default());
let depth_view = depth.create_view(&Default::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &color_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
sky.draw_disc(&mut pass);
}
encoder.copy_texture_to_buffer(
color.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &read,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(SIZE * 4),
rows_per_image: Some(SIZE),
},
},
color.size(),
);
queue.submit([encoder.finish()]);
read.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let pixels = read.slice(..).get_mapped_range().unwrap().to_vec();
let at = |x: usize, y: usize| pixels[(y * 64 + x) * 4..(y * 64 + x) * 4 + 4].to_vec();
// Row 0 is the top: the disc, in the sky colour; the bottom row is below the horizon.
let top = at(32, 4);
assert!(
top.iter()
.zip([64u8, 128, 255, 255])
.all(|(&p, w)| p.abs_diff(w) <= 1),
"{top:?}"
);
assert_eq!(at(32, 60), vec![0, 0, 0, 0]);
}
}
+11 -2
View File
@@ -10,8 +10,8 @@
//! second pass, draws the sections whose box was seen; the others' draws have
//! an instance count of 0.
//!
//! The picture stays the one without culling: the boxes (grown by a block,
//! the farthest a block model reaches out of its cell) are drawn front faces
//! The picture stays the one without culling: the boxes around the mesh's
//! geometry (padded by a block) are drawn front faces
//! only, so a fragment is the box's nearest point on its pixel, and the
//! fragment shader runs only where the depth test passes (forced early), so a
//! section is left out only where all of its fragments would fail the depth
@@ -45,6 +45,15 @@ use std::sync::{
const WORDS: u64 = 5 * 4;
/// A tested section's box, around its origin: its cell grown by a block.
pub const SECTION_BOX: [[f32; 3]; 2] = [[-1.0; 3], [17.0; 3]];
/// Mesh bounds already contain `BOUNDS_PAD`; grow them to a block of padding
/// in total, keeping the box's faces off the geometry. Without mesh bounds,
/// keep the section cell grown by a block, as before.
pub fn section_box(bounds: Option<[glam::Vec3; 2]>) -> [[f32; 3]; 2] {
bounds.map_or(SECTION_BOX, |[low, high]| {
let pad = glam::Vec3::splat(1.0 - crate::mesher::SectionMesh::BOUNDS_PAD);
[(low - pad).to_array(), (high + pad).to_array()]
})
}
/// The bit that marks a far tile's slot (`Renderer::tile_cull_slot`): tiles
/// count their slots apart from the sections' resident ones.
pub const TILE: u32 = 1 << 31;
@@ -16,8 +16,8 @@ struct Globals {
// `place`): the first pass's first draw, both passes' draw counts (the
// second's in the high 16 bits), the second pass's first draw. No draws:
// phase 1 drew it, and the test only tells whether it is still in sight.
// Then its box's lowest and highest corner around the origin (a section's:
// -1 and 17, `SECTION_BOX`).
// Then its box's lowest and highest corner around the origin: a section's
// padded mesh bounds, or -1 and 17 (`SECTION_BOX`) without mesh bounds.
struct Test {
data: vec4<u32>,
low: vec4<f32>,
@@ -53,8 +53,8 @@ fn corner(origin: vec3<f32>, low: vec3<f32>, high: vec3<f32>, c: u32) -> vec3<f3
);
}
// Each tested section's or far tile's box, grown by a block (block models
// reach one block out of their cell), drawn after phase 1 against its depth
// Each tested section's or far tile's geometry box, padded by a block,
// drawn after phase 1 against its depth
// with back faces culled: a front face fragment that passes the depth test
// is the box's nearest point on that pixel in front of phase 1's depth, so
// the section may show there. A box failing everywhere would have every
@@ -0,0 +1,749 @@
//! OptiFine custom sky layers on the GPU: one texture per layer, kept with the renderer (its pack),
//! a static skybox mesh of the 3x2 grid's six faces, and one pipeline per blend mode. A frame
//! uploads one small uniform per drawn layer ([`crate::custom_sky::Draw`]) and draws 36 vertices
//! each, after the vanilla sky's disc and sunrise and before the sun, moon and stars.
use glam::Mat4;
use horizon_optifine::sky::{Blend, Face};
use wgpu::util::DeviceExt;
use crate::custom_sky::{CustomSky as Pack, Draw};
/// The blend modes in the shader's `mode` order (custom_sky.wgsl).
const MODES: [Blend; 9] = [
Blend::Add,
Blend::Subtract,
Blend::Multiply,
Blend::Dodge,
Blend::Burn,
Blend::Screen,
Blend::Replace,
Blend::Overlay,
Blend::Alpha,
];
/// Layers drawn in a frame at most; a pack's later ones wait for a frame with fewer.
const MAX_DRAWS: usize = 32;
/// Dynamic uniform offsets are 256-byte aligned on every backend.
const STRIDE: u64 = 256;
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniform {
clip_from_sky: [[f32; 4]; 4],
brightness: f32,
mode: u32,
pad: [u32; 2],
hdr_settings: [f32; 4],
hdr_shares: [f32; 4],
hdr_light: [f32; 4],
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 3],
uv: [f32; 2],
cell: [f32; 4],
}
pub(super) struct CustomSky {
pipelines: Vec<wgpu::RenderPipeline>,
written_gain: Option<f32>,
written_debug: bool,
layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
uniform: wgpu::Buffer,
vertices: wgpu::Buffer,
/// One bind group per layer texture of the pack.
groups: Vec<wgpu::BindGroup>,
/// This frame's layers: bind group and pipeline indices.
frame: Vec<(u16, u8)>,
}
/// The fragment's output for each mode is in custom_sky.wgsl; these factors finish it.
fn blend(mode: Blend) -> wgpu::BlendComponent {
use wgpu::BlendFactor as F;
let (src_factor, dst_factor, operation) = match mode {
Blend::Add => (F::One, F::One, wgpu::BlendOperation::Add),
Blend::Subtract => (F::One, F::One, wgpu::BlendOperation::ReverseSubtract),
Blend::Multiply => (F::Zero, F::Src, wgpu::BlendOperation::Add),
// dst + dst * src: lightens.
Blend::Dodge => (F::Dst, F::One, wgpu::BlendOperation::Add),
// dst * (1 - src): darkens.
Blend::Burn => (F::Zero, F::OneMinusSrc, wgpu::BlendOperation::Add),
// src + dst * (1 - src) = 1 - (1 - src)(1 - dst).
Blend::Screen => (F::One, F::OneMinusSrc, wgpu::BlendOperation::Add),
Blend::Replace => (F::One, F::Zero, wgpu::BlendOperation::Add),
// dst * src + src * dst.
Blend::Overlay => (F::Dst, F::Src, wgpu::BlendOperation::Add),
Blend::Alpha => (F::SrcAlpha, F::OneMinusSrcAlpha, wgpu::BlendOperation::Add),
};
wgpu::BlendComponent {
src_factor,
dst_factor,
operation,
}
}
/// The skybox's 36 vertices: two triangles per face, each corner with its cell's UV.
fn mesh() -> Vec<Vertex> {
let mut out = Vec::with_capacity(36);
for face in Face::ALL {
let cell = face.cell();
let [u0, v0, u1, v1] = cell;
let uv = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
let corners = face.corners();
for i in [0, 1, 2, 0, 2, 3] {
out.push(Vertex {
position: corners[i],
uv: uv[i],
cell,
});
}
}
out
}
impl CustomSky {
pub(super) fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Custom sky bindings"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<Uniform>() as u64
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Custom sky shader"),
source: wgpu::ShaderSource::Wgsl(
(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl"))
.into(),
),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Custom sky layout"),
bind_group_layouts: &[Some(&layout)],
..Default::default()
});
let attributes = wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4];
let pipelines = MODES
.iter()
.copied()
.map(|mode| (mode, false, false))
.chain((format == super::LINEAR_FRAME).then_some((Blend::Screen, true, false)))
.chain((format == super::LINEAR_FRAME).then_some((Blend::Add, false, true)))
.map(|(mode, written_screen, debug_add)| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Custom sky layer"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_custom_sky"),
compilation_options: Default::default(),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attributes,
})],
},
primitive: wgpu::PrimitiveState {
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some(if debug_add {
"fs_custom_sky_screen_light"
} else {
"fs_custom_sky"
}),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
)],
..Default::default()
},
targets: &[Some(wgpu::ColorTargetState {
format,
// The frame's alpha is HDR output's mark (hdr.wgsl): the sky
// under the layers keeps its own.
blend: Some(wgpu::BlendState {
color: if written_screen {
wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Constant,
..blend(mode)
}
} else {
blend(mode)
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Zero,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
})
.collect();
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Custom sky"),
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Custom sky layers"),
size: STRIDE * MAX_DRAWS as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let vertices = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Custom sky box"),
contents: bytemuck::cast_slice(&mesh()),
usage: wgpu::BufferUsages::VERTEX,
});
Self {
pipelines,
written_gain: None,
written_debug: false,
layout,
sampler,
uniform,
vertices,
groups: Vec::new(),
frame: Vec::new(),
}
}
/// The pack's layer textures, replacing any before.
pub(super) fn set_pack(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pack: &Pack) {
let limit = device.limits().max_texture_dimension_2d;
self.groups = pack
.images
.iter()
.map(|image| {
// Too large for this GPU: a 1x1 transparent texel, so the layer draws nothing.
let (width, height, rgba) = if image.width <= limit && image.height <= limit {
(image.width, image.height, &image.rgba[..])
} else {
log::warn!(
"custom sky texture of {}x{} too large",
image.width,
image.height
);
(1, 1, &[0u8; 4][..])
};
let texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("Custom sky layer"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
rgba,
);
let view = texture.create_view(&Default::default());
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Custom sky layer"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.uniform,
offset: 0,
size: wgpu::BufferSize::new(std::mem::size_of::<Uniform>() as u64),
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
})
.collect();
}
/// Upload this frame's layers; `clip` is clip space from camera-relative world.
pub(super) fn prepare(
&mut self,
uploads: &mut super::upload::Uploads,
encoder: &mut wgpu::CommandEncoder,
clip: Mat4,
draws: &[Draw],
globals: &super::Globals,
) {
self.written_gain = (globals.hdr_light[3] != 0.0).then(|| {
let mark =
((1.0 - 0.24 * globals.hdr_light[1]).clamp(0.0, 1.0) * 255.0).round() / 255.0;
if globals.hdr_settings[2] > 0.5 && globals.hdr_settings[0] > 1.0 {
(1.0 + (globals.hdr_settings[1] - 1.0) * (1.0 - mark) / 0.24).powf(1.0 / 2.2)
} else {
1.0
}
});
self.written_debug = self.written_gain.is_some()
&& globals.hdr_settings[3] > 0.5
&& globals.hdr_settings[2] > 0.5;
self.frame.clear();
for draw in draws.iter().take(MAX_DRAWS) {
if usize::from(draw.image) >= self.groups.len() {
continue;
}
let mode = MODES.iter().position(|m| *m == draw.blend).unwrap_or(0);
let uniform = Uniform {
clip_from_sky: (clip * draw.rotation).to_cols_array_2d(),
brightness: draw.brightness.clamp(0.0, 1.0),
mode: mode as u32,
pad: [0; 2],
hdr_settings: globals.hdr_settings,
hdr_shares: globals.hdr_shares,
hdr_light: globals.hdr_light,
};
let offset = STRIDE * self.frame.len() as u64;
uploads.buffer(encoder, &self.uniform, offset, bytemuck::bytes_of(&uniform));
self.frame.push((draw.image, mode as u8));
}
}
pub(super) fn is_drawing(&self) -> bool {
!self.frame.is_empty()
}
/// The layers [`CustomSky::prepare`] uploaded. Leaves the pass's pipeline and bind group 0
/// changed.
pub(super) fn draw(&self, pass: &mut wgpu::RenderPass<'_>) {
pass.set_vertex_buffer(0, self.vertices.slice(..));
for (i, &(image, mode)) in self.frame.iter().enumerate() {
let screen_debug = usize::from(mode) == 5 && self.written_debug;
if screen_debug {
// Greyscale debug is not a uniform RGB gain. Attenuate with the original
// source first, then add its converted light, without changing the factor.
pass.set_pipeline(&self.pipelines[4]);
pass.set_bind_group(
0,
&self.groups[usize::from(image)],
&[(STRIDE * i as u64) as u32],
);
pass.draw(0..36, 0..1);
}
let index = if screen_debug {
10
} else if usize::from(mode) == 5 && self.written_gain.is_some() {
9
} else {
usize::from(mode)
};
if let Some(gain) = self.written_gain {
let gain = f64::from(gain);
pass.set_blend_constant(wgpu::Color {
r: gain,
g: gain,
b: gain,
a: 1.0,
});
}
pass.set_pipeline(&self.pipelines[index]);
pass.set_bind_group(
0,
&self.groups[usize::from(image)],
&[(STRIDE * i as u64) as u32],
);
pass.draw(0..36, 0..1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// What a blend leaves a sky texel of `dst` with when the shader outputs `src`
/// (custom_sky.wgsl) for a layer texel `t` at brightness `b`, per channel.
fn mix(mode: Blend, dst: f32, t: f32, a: f32, b: f32) -> f32 {
let k = a * b;
let src = match mode {
Blend::Multiply => 1.0 + (t - 1.0) * k,
Blend::Overlay => 0.5 + (t - 0.5) * k,
Blend::Replace | Blend::Alpha => t,
_ => t * k,
};
let c = blend(mode);
let f = |factor: wgpu::BlendFactor| match factor {
wgpu::BlendFactor::One => 1.0,
wgpu::BlendFactor::Zero => 0.0,
wgpu::BlendFactor::Src => src,
wgpu::BlendFactor::OneMinusSrc => 1.0 - src,
wgpu::BlendFactor::Dst => dst,
wgpu::BlendFactor::SrcAlpha => k,
wgpu::BlendFactor::OneMinusSrcAlpha => 1.0 - k,
other => panic!("{other:?}"),
};
let (s, d) = (src * f(c.src_factor), dst * f(c.dst_factor));
match c.operation {
wgpu::BlendOperation::Add => s + d,
wgpu::BlendOperation::ReverseSubtract => d - s,
other => panic!("{other:?}"),
}
}
#[test]
fn every_mode_gives_its_documented_result() {
let close = |a: f32, b: f32| assert!((a - b).abs() < 1e-6, "{a} != {b}");
// Full brightness and alpha, a sky texel of 0.4 under a layer texel of 0.8.
let (d, t) = (0.4, 0.8);
close(mix(Blend::Add, d, t, 1.0, 1.0), 1.2);
close(mix(Blend::Subtract, d, t, 1.0, 1.0), -0.4);
close(mix(Blend::Multiply, d, t, 1.0, 1.0), 0.32);
close(mix(Blend::Screen, d, t, 1.0, 1.0), 1.0 - 0.6 * 0.2);
close(mix(Blend::Replace, d, t, 1.0, 1.0), 0.8);
close(mix(Blend::Overlay, d, t, 1.0, 1.0), 0.64);
close(mix(Blend::Overlay, d, 0.2, 1.0, 1.0), 0.16);
close(mix(Blend::Alpha, d, t, 0.25, 1.0), 0.4 * 0.75 + 0.8 * 0.25);
assert!(mix(Blend::Dodge, d, t, 1.0, 1.0) > d);
assert!(mix(Blend::Burn, d, t, 1.0, 1.0) < d);
// Faded out, every mode but replace (drawn only when bright) leaves the sky alone.
for mode in MODES {
if mode != Blend::Replace {
close(mix(mode, d, t, 1.0, 0.0), d);
}
}
// Half faded: add adds half.
close(mix(Blend::Add, d, t, 1.0, 0.5), 0.8);
}
#[test]
fn the_shader_validates() {
let module = naga::front::wgsl::parse_str(
&(include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl")),
)
.unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn the_box_covers_every_face_with_its_cell() {
let mesh = mesh();
assert_eq!(mesh.len(), 36);
for v in &mesh {
let [u0, v0, u1, v1] = v.cell;
assert!((u0..=u1).contains(&v.uv[0]) && (v0..=v1).contains(&v.uv[1]));
assert_eq!(v.position.iter().filter(|c| c.abs() == 1.0).count(), 3);
}
}
#[test]
fn written_custom_sky_modes_keep_factors_and_scale_only_screen_source() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let (device, queue) =
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let source = include_str!("../hdr_light.wgsl").to_owned()
+ include_str!("custom_sky.wgsl")
+ r#"
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> Out {
let p = vec2(f32((i << 1u) & 2u), f32(i & 2u));
return Out(vec4(p * 2.0 - 1.0, 0.0, 1.0), vec2(0.5), vec4(0.0, 0.0, 1.0, 1.0));
}
"#;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(source.into()),
});
let sky = CustomSky::new(&device, wgpu::TextureFormat::Rgba16Float);
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[Some(&sky.layout)],
..Default::default()
});
let image = device.create_texture_with_data(
&queue,
&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&[204, 102, 51, 128],
);
let image = image.create_view(&Default::default());
let sampler = device.create_sampler(&Default::default());
let target = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
// Independent scalar expectation: a sky mark of 206/255 gives this gamma gain.
let hdr_gain = (1.0f32 + 3.0 * (1.0 - 206.0 / 255.0) / 0.24).powf(1.0 / 2.2);
for (mode_index, mode, debug, written) in MODES
.iter()
.copied()
.enumerate()
.map(|(i, m)| (i, m, false, true))
.chain([(5, Blend::Screen, true, true)])
.chain(
MODES
.iter()
.copied()
.enumerate()
.map(|(i, m)| (i, m, false, false)),
)
{
let gain = if written { hdr_gain } else { 1.0 };
let uniform = Uniform {
clip_from_sky: Mat4::IDENTITY.to_cols_array_2d(),
brightness: 0.7,
mode: mode_index as u32,
pad: [0; 2],
hdr_settings: [5.0, 4.0, 1.0, f32::from(u8::from(debug))],
hdr_shares: [0.2, 0.5, 0.6, 1.0],
hdr_light: [0.8, 0.8, 1.0, f32::from(u8::from(written))],
};
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::bytes_of(&uniform),
usage: wgpu::BufferUsages::UNIFORM,
});
let color = if debug {
blend(Blend::Add)
} else if mode == Blend::Screen {
wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::Constant,
..blend(mode)
}
} else {
blend(mode)
};
let pipeline_for = |entry, color| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("test_vertex"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some(entry),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &[("HDR_WRITE_LIGHT", 1.0)],
..Default::default()
},
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba16Float,
blend: Some(wgpu::BlendState {
color,
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
};
let pipeline = pipeline_for(
if debug {
"fs_custom_sky_screen_light"
} else {
"fs_custom_sky"
},
color,
);
let attenuate = debug.then(|| pipeline_for("fs_custom_sky", blend(Blend::Burn)));
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &sky.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&image),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
let mut encoder = device.create_command_encoder(&Default::default());
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 2.0,
g: 2.0,
b: 2.0,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
let g = f64::from(gain);
pass.set_blend_constant(wgpu::Color {
r: g,
g,
b: g,
a: 1.0,
});
pass.set_bind_group(0, &group, &[0]);
if let Some(attenuate) = &attenuate {
pass.set_pipeline(attenuate);
pass.draw(0..3, 0..1);
}
pass.set_pipeline(&pipeline);
pass.draw(0..3, 0..1);
}
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
target.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(256),
rows_per_image: None,
},
},
target.size(),
);
queue.submit([encoder.finish()]);
readback.map_async(wgpu::MapMode::Read, .., |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let data = readback.get_mapped_range(..).unwrap();
let got = half::f16::from_le_bytes([data[0], data[1]]).to_f32();
let (d, t, k) = (2.0, 0.8, 128.0 / 255.0 * 0.7);
let alpha = half::f16::from_le_bytes([data[6], data[7]]).to_f32();
let coverage = if mode == Blend::Alpha { k } else { 1.0 };
assert!(
(alpha - coverage).abs() < 0.001,
"{mode:?}: alpha {alpha}, expected {coverage}"
);
let expected = if debug {
let grey = (0.2126f32 * 0.8f32.powf(2.2)
+ 0.7152 * 0.4f32.powf(2.2)
+ 0.0722 * 0.2f32.powf(2.2))
.powf(1.0 / 2.2);
gain * grey * k + d * (1.0 - t * k)
} else {
match mode {
Blend::Add => d + gain * t * k,
Blend::Subtract => d - gain * t * k,
Blend::Multiply => d * (1.0 + (t - 1.0) * k),
Blend::Dodge => d + d * t * k,
Blend::Burn => d * (1.0 - t * k),
Blend::Screen => gain * t * k + d * (1.0 - t * k),
Blend::Replace => gain * t,
Blend::Overlay => 2.0 * d * (0.5 + (t - 0.5) * k),
Blend::Alpha => gain * t * k + d * (1.0 - k),
}
};
assert!(
(got - expected).abs() < 0.004,
"{mode:?} debug {debug}: {got} expected {expected}"
);
}
eprintln!(
"HDR custom sky GPU ran all nine modes with and without gain, plus debug Screen, checking RGB and coverage"
);
}
}
@@ -0,0 +1,79 @@
// OptiFine custom sky layers (gfx/custom_sky.rs): one skybox per draw, its six faces cut from a
// 3x2 texture, mixed with the sky under it by the pipeline's blend. The fragment's output is
// chosen so that blend gives the layer's documented mode at its brightness, and a layer at
// brightness 0 leaves the sky as it was.
struct Layer {
// Projection * view (camera-relative) * the layer's rotation.
clip_from_sky: mat4x4<f32>,
brightness: f32,
// horizon_optifine::sky::Blend, in gfx/custom_sky.rs's MODES order.
mode: u32,
pad: vec2<u32>,
hdr_settings: vec4<f32>,
hdr_shares: vec4<f32>,
hdr_light: vec4<f32>,
}
@group(0) @binding(0) var<uniform> layer: Layer;
@group(0) @binding(1) var sky_texture: texture_2d<f32>;
@group(0) @binding(2) var sky_sampler: sampler;
struct Out {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
// The face's cell (u0, v0, u1, v1): sampling stays half a texel inside it.
@location(1) @interpolate(flat) cell: vec4<f32>,
}
@vertex fn vs_custom_sky(
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) cell: vec4<f32>,
) -> Out {
var out: Out;
out.clip = layer.clip_from_sky * vec4(position * 100.0, 1.0);
out.uv = uv;
out.cell = cell;
return out;
}
const ADD: u32 = 0u;
const SUBTRACT: u32 = 1u;
const MULTIPLY: u32 = 2u;
const DODGE: u32 = 3u;
const BURN: u32 = 4u;
const SCREEN: u32 = 5u;
const REPLACE: u32 = 6u;
const OVERLAY: u32 = 7u;
const ALPHA: u32 = 8u;
fn custom_sky_color(in: Out) -> vec4<f32> {
let half = 0.5 / vec2<f32>(textureDimensions(sky_texture));
let uv = clamp(in.uv, in.cell.xy + half, in.cell.zw - half);
let t = textureSample(sky_texture, sky_sampler, uv);
let k = t.a * layer.brightness;
var color = t.rgb;
if HDR_WRITE_LIGHT && layer.hdr_light.w != 0.0 &&
(layer.mode == ADD || layer.mode == SUBTRACT || layer.mode == REPLACE || layer.mode == ALPHA) {
color = hdr_written(color, 1.0 - 0.24 * layer.hdr_light.y, layer.hdr_settings, layer.hdr_shares);
}
switch layer.mode {
// dst * src: white leaves the sky alone.
case MULTIPLY: { return vec4(mix(vec3(1.0), t.rgb, k), 1.0); }
// 2 * dst * src: grey leaves it alone.
case OVERLAY: { return vec4(mix(vec3(0.5), t.rgb, k), 1.0); }
case REPLACE: { return vec4(color, 1.0); }
// mix(dst, src, alpha).
case ALPHA: { return vec4(color, k); }
// add, subtract, dodge, burn, screen: the layer premultiplied, black leaves it alone.
default: { return vec4(color * k, 1.0); }
}
}
@fragment fn fs_custom_sky(in: Out) -> @location(0) vec4<f32> {
return custom_sky_color(in);
}
@fragment fn fs_custom_sky_screen_light(in: Out) -> @location(0) vec4<f32> {
let color = custom_sky_color(in);
return vec4(hdr_written(color.rgb, 1.0 - 0.24 * layer.hdr_light.y, layer.hdr_settings, layer.hdr_shares), color.a);
}
@@ -12,7 +12,7 @@ use crate::mesher::TerrainVertex;
/// The solid quads the grid can draw a frame, and the sections they come from.
const MAX_QUADS: u64 = 1 << 16;
const MAX_SECTIONS: usize = 64;
pub(crate) const MAX_SECTIONS: usize = 64;
/// Bytes of one quad's corners.
const QUAD_BYTES: u64 = 4 * std::mem::size_of::<TerrainVertex>() as u64;
/// How far past the flat face the relief may reach, in blocks, for the view test.
@@ -413,6 +413,8 @@ pub struct Displacement {
pub shadow: Option<Picks>,
/// Group 1's layout for the grid's pipelines.
pub draw_layout: wgpu::BindGroupLayout,
/// CPU-side section table reused between displacement preparation passes.
table: Vec<Section>,
/// Sections the pick ran over this frame (0: nothing to draw).
pub drawn: usize,
}
@@ -598,6 +600,7 @@ impl Displacement {
view,
shadow,
draw_layout,
table: Vec::new(),
drawn: 0,
}
}
@@ -610,7 +613,8 @@ impl Displacement {
near: &[Near<'_>],
view_proj: glam::Mat4,
) {
let mut table = Vec::with_capacity(near.len().min(MAX_SECTIONS));
self.table.clear();
self.table.reserve(near.len().min(MAX_SECTIONS));
let mut first = 0u64;
let mut most = 0u32;
for n in near.iter().take(MAX_SECTIONS) {
@@ -626,7 +630,7 @@ impl Displacement {
first * QUAD_BYTES,
quads * QUAD_BYTES,
);
table.push(Section {
self.table.push(Section {
origin: [
n.origin[0],
n.origin[1],
@@ -640,11 +644,11 @@ impl Displacement {
first += quads;
most = most.max(quads as u32);
}
self.drawn = table.len();
if table.is_empty() {
self.drawn = self.table.len();
if self.table.is_empty() {
return;
}
queue.write_buffer(&self.sections, 0, bytemuck::cast_slice(&table));
queue.write_buffer(&self.sections, 0, bytemuck::cast_slice(&self.table));
let lists = std::iter::once((&self.view, 0)).chain(self.shadow.iter().map(|s| (s, 1)));
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Displacement pick"),
@@ -655,7 +659,7 @@ impl Displacement {
let params = Params {
view_proj: view_proj.to_cols_array_2d(),
distance: self.distance,
sections: table.len() as u32,
sections: self.table.len() as u32,
margin: RELIEF_MARGIN,
capacity: MAX_QUADS as u32,
shadow,
@@ -665,7 +669,7 @@ impl Displacement {
let vertices = grid_vertices(picks.cells, picks.steps);
queue.write_buffer(&picks.draw, 0, bytemuck::cast_slice(&[vertices, 0, 0, 0]));
pass.set_bind_group(0, &picks.pick_group, &[]);
pass.dispatch_workgroups(most.div_ceil(64), table.len() as u32, 1);
pass.dispatch_workgroups(most.div_ceil(64), self.table.len() as u32, 1);
}
}
@@ -14,8 +14,24 @@ use super::Vertex;
/// material at the end: the render types vanilla gives an `OitPipelineSet`
/// (translucent, emissive, eyes and breeze wind entity models). Off, the tag is
/// ignored and they stay in the first one, in their order.
/// Two native world-canvas text materials follow it, NORMAL then POLYGON_OFFSET.
const MATERIALS: usize = 12;
/// Sorted still-fluid geometry follows, then the two native world-canvas text materials,
/// NORMAL and POLYGON_OFFSET. Material storage order is independent of their draw stages.
const MATERIALS: usize = 16;
pub const TERRAIN_MODEL_SOLID_OVERLAY: u32 = 42;
pub const TERRAIN_MODEL_CUTOUT_OVERLAY: u32 = 43;
pub const TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY: u32 = 44;
/// Native terrain-array UV/payloads must never enter a shared-atlas shader-pack pass.
pub fn is_native_terrain(layer: u32) -> bool {
matches!(
layer & 0xffff,
FLUID_OVERLAY
| TERRAIN_MODEL_SOLID_OVERLAY
| TERRAIN_MODEL_CUTOUT_OVERLAY
| TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY
)
}
/// Sorted depth-writing still fluid, sampling terrain sprite arrays rather than the shared atlas.
pub const FLUID_OVERLAY: u32 = 41;
/// A bit of `Vertex::layer`, above the overlay (low 16 bits) and the HDR tier
/// (bits 16 to 23): the quad's render type has an `OitPipelineSet`
/// (`RenderTypes.entityTranslucent`, `entityTranslucentEmissive`, `eyes`,
@@ -31,7 +47,9 @@ pub const DISSOLVE_OVERLAY: u32 = 38;
/// (`OIT_ENTITY`): improved transparency draws them in its stages.
pub fn mark_oit(quads: &mut [Vertex]) {
for vertex in quads {
vertex.layer |= OIT_ENTITY;
if !is_native_terrain(vertex.layer) {
vertex.layer |= OIT_ENTITY;
}
}
}
/// The overlay of depth-only quads that hide water (`RenderTypes.waterMask`).
@@ -60,12 +78,21 @@ pub const WORLD_TEXT_POLYGON_OFFSET_OVERLAY: u32 = 40;
/// The material of a quad by its overlay (`MATERIALS` order): the overlay is the layer's low 16
/// bits, as entity.wgsl reads it; HDR output's tier rides in the high ones.
fn material(layer: u32, oit: bool) -> usize {
match layer & 0xffff {
TERRAIN_MODEL_SOLID_OVERLAY => return 13,
TERRAIN_MODEL_CUTOUT_OVERLAY => return 14,
TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY => return 15,
_ => {}
}
if layer & 0xffff == FLUID_OVERLAY {
return 10;
}
if oit && layer & OIT_ENTITY != 0 {
return 9;
}
match layer & 0xffff {
WORLD_TEXT_OVERLAY => 10,
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 11,
WORLD_TEXT_OVERLAY => 11,
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 12,
TEXT_POLYGON_OFFSET_OVERLAY => 1,
11 => 2,
12 => 3,
@@ -148,10 +175,10 @@ impl EntityBatch {
}
}
}
// RenderType.text sorts alpha-blended quads from far to near. Keep
// both world-canvas layers after existing materials (including eyes
// moved out of additive); front text uses polygon offset.
for runs in &self.runs[10..] {
// Fluid and RenderType.text sort their own alpha-blended quads far to
// near. Fluid precedes both world-canvas layers, after existing materials
// (including eyes moved out of additive).
for runs in &self.runs[10..13] {
let start = self.combined.len();
for &(first, count) in runs {
self.combined
@@ -169,6 +196,13 @@ impl EntityBatch {
distance(b).total_cmp(&distance(a))
});
}
// Native solid/cutout models are depth writing and keep their submission order.
for runs in &self.runs[13..] {
for &(first, count) in runs {
self.combined
.extend_from_slice(&entities[first * 4..(first + count) * 4]);
}
}
if self.moved_additive == 0 {
self.combined.extend_from_slice(additive);
} else {
@@ -189,7 +223,57 @@ mod tests {
use super::*;
use std::{hint::black_box, time::Instant};
#[test]
fn world_canvas_sorts_after_oit_eyes_before_frame_tails() {
fn native_models_keep_opaque_order_and_do_not_move_existing_materials_or_tails() {
let quad = |layer, x| {
[Vertex {
layer,
pos: [x, 0.0, 0.0],
..geometry(1, false)[0]
}; 4]
};
let entities: Vec<_> = [
(FLUID_OVERLAY, 10.0),
(WORLD_TEXT_OVERLAY, 11.0),
(TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16, 3.0),
(TERRAIN_MODEL_SOLID_OVERLAY, 1.0),
(TERRAIN_MODEL_CUTOUT_OVERLAY, 4.0),
(TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY, 5.0),
(OIT_ENTITY, 6.0),
(0, 7.0),
]
.into_iter()
.flat_map(|(layer, x)| quad(layer, x))
.collect();
let mut batch = EntityBatch::default();
let counts = batch.update(
&entities,
&quad(0, 8.0),
&quad(0, 9.0),
&quad(0, 12.0),
true,
);
assert_eq!(counts, [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 1, 1]);
let xs: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(
xs,
[7.0, 6.0, 10.0, 11.0, 3.0, 1.0, 4.0, 5.0, 8.0, 9.0, 12.0]
);
assert_eq!(
batch.combined[16].layer,
TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16
);
}
#[test]
fn tagging_oit_does_not_corrupt_a_native_models_sprite_index() {
let mut vertices = [geometry(1, false)[0]; 2];
vertices[0].layer = TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16;
vertices[1].layer = 3;
mark_oit(&mut vertices);
assert_eq!(vertices[0].layer, TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16);
assert_eq!(vertices[1].layer, 3 | OIT_ENTITY);
}
#[test]
fn fluid_and_world_canvas_sort_after_oit_eyes_before_frame_tails() {
let quad = |layer, x| {
[Vertex {
layer,
@@ -204,6 +288,8 @@ mod tests {
(WORLD_TEXT_OVERLAY, 4.0),
(WORLD_TEXT_OVERLAY, 5.0),
(WORLD_TEXT_POLYGON_OFFSET_OVERLAY, 6.0),
(FLUID_OVERLAY, 11.0),
(FLUID_OVERLAY | OIT_ENTITY, 12.0),
]
.into_iter()
.flat_map(|(layer, x)| quad(layer, x))
@@ -215,12 +301,14 @@ mod tests {
let mut batch = EntityBatch::default();
assert_eq!(
batch.update(&entities, &additive, &quad(0, 9.0), &quad(0, 10.0), true),
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1]
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0]
);
let positions: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(
positions,
[1.0, 2.0, 3.0, 7.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0]
[
1.0, 2.0, 3.0, 7.0, 12.0, 11.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0
]
);
}
@@ -320,12 +408,12 @@ mod tests {
let mut batch = EntityBatch::default();
// Off: the tag changes nothing, the quads keep their place.
let counts = batch.update(&entities, &[], &[], &[], false);
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]);
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(xs, [0.0, 1.0, 2.0, 4.0, 3.0]);
// On: the tagged quads come last, in order.
let counts = batch.update(&entities, &[], &[], &[], true);
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0]);
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0]);
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
assert_eq!(xs, [0.0, 2.0, 3.0, 1.0, 4.0]);
}
+17 -2
View File
@@ -106,6 +106,8 @@ pub struct Flat {
phase2_calls: wgpu::Buffer,
phase2_call_capacity: u64,
phase2_call_data: Vec<[u32; 5]>,
/// GPU culling's phase 2 uses indirect draws; flat draws without culling do not.
indirect_execution: bool,
/// The copy of phase 2's draws, and its ranges.
phase2_group: Option<(wgpu::BindGroup, u32)>,
/// The layout of a page's bind group 1 (shader packs' terrain pipelines share it).
@@ -365,6 +367,7 @@ impl Flat {
terrain_layout: &wgpu::BindGroupLayout,
page_vertices: u64,
min_sections: usize,
indirect_execution: bool,
pipeline: impl Fn(
&wgpu::PipelineLayout,
&str,
@@ -489,10 +492,16 @@ impl Flat {
device,
"Flat section draw calls, phase 2",
20,
wgpu::BufferUsages::INDIRECT | storage_usage,
storage_usage
| if indirect_execution {
wgpu::BufferUsages::INDIRECT
} else {
wgpu::BufferUsages::empty()
},
),
phase2_call_capacity: 1,
phase2_call_data: Vec::new(),
indirect_execution,
phase2_group: None,
draw_layout,
blocks_per_page: page_vertices.div_ceil(GRAIN),
@@ -645,6 +654,7 @@ impl Flat {
timing: Option<&mut super::pack_timing::PassTiming>,
) -> bool {
self.phase2_group = None;
let phase2 = phase2.filter(|_| self.indirect_execution);
let phase1_draws = if phase2.is_some() {
phase1_draws
} else {
@@ -753,7 +763,12 @@ impl Flat {
device,
"Flat section draw calls, phase 2",
self.phase2_call_capacity * 20,
wgpu::BufferUsages::INDIRECT | wgpu::BufferUsages::STORAGE,
wgpu::BufferUsages::STORAGE
| if self.indirect_execution {
wgpu::BufferUsages::INDIRECT
} else {
wgpu::BufferUsages::empty()
},
);
}
if phase2_calls > 0 {
@@ -0,0 +1,69 @@
//! Sorted still-fluid material: ordinary back culling, alpha blend and color/depth writes.
pub(super) const SAMPLER: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
label: Some("Still fluid (nearest texels, linear mipmaps)"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
lod_min_clamp: 0.0,
lod_max_clamp: 32.0,
compare: None,
anisotropy_clamp: 1,
border_color: None,
};
pub(super) fn pipeline(
device: &wgpu::Device,
layout: Option<&wgpu::PipelineLayout>,
shader: &wgpu::ShaderModule,
format: wgpu::TextureFormat,
constants: &[(&str, f64)],
) -> wgpu::RenderPipeline {
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Still fluid"),
layout,
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_native_fluid"),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
..Default::default()
},
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<super::Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &attributes,
})],
},
primitive: wgpu::PrimitiveState {
cull_mode: Some(wgpu::Face::Back),
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_native_fluid"),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
..Default::default()
},
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,65 @@
// The light of colour `f` under the world's mark `a` (the alpha the world left, hdr_mark) where
// the world `shows`, else the colour clipped at white; and whether a light source lifted it.
// `eased` for the last pass, which eases it into the peak itself; the layered frame leaves that
// to the tone curve (fs_tonemap).
struct ReferenceLit {
light: vec3<f32>,
lifted: bool,
};
fn reference_lit(f: vec3<f32>, a: f32, shows: bool, eased: bool) -> ReferenceLit {
var lifted = false;
// Light over 1 (a float frame's) shows only where the world does and carries
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
// at white before their lift, as on a unorm frame, so light sources, the sun and
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
let c = select(min(f, vec3(1.0)), f, shows && a == 1.0);
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
var light = pow(c, vec3(2.2));
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
// light is albedo times lightmap, so this is the lightmap going past 1. It eases into the
// peak from 60% of it, so bright textures keep their detail. Unmarked light (alpha 1)
// gets no gain and no knee: the clamp to the peak below is all it needs here.
if shows && a >= 0.75 && a < 1.0 {
let top = max(light.r, max(light.g, light.b));
let gain = 1.0 + (params.scene - 1.0) * (1.0 - a) / 0.24;
let v = top * gain;
let knee = max(0.6 * params.peak, 1.0);
var out = v;
if eased && v > knee && params.peak > knee {
out = knee + (params.peak - knee) * (1.0 - exp(-(v - knee) / (params.peak - knee)));
}
light *= select(gain, out / max(top, 1e-4), eased && v > knee);
}
// The world's alpha is below 0.75 on light sources, the sun and the moon
// (terrain.wgsl, entity.wgsl): its band of 0.175 is the tier, the lower the
// brighter, and where in the band how much (hdr_mark).
if shows && a < 0.75 {
let band = u32(clamp(floor((a + 0.005) / 0.175), 0.0, 3.0));
let tier = 3u - band;
let lift = 1.0 - clamp((a - 0.175 * f32(band)) / 0.165, 0.0, 1.0);
if lift > 0.0 {
let top = max(light.r, max(light.g, light.b));
var gain = 1.0 + (params.peak - 1.0) * params.share[tier] * lift;
if eased {
gain = min(gain, params.peak / max(top, 1e-4));
}
light *= max(gain, 1.0);
lifted = true;
}
}
// HORIZON_HDR_DEBUG=1: what isn't lifted turns grey, what is keeps its
// colour with a magenta tint, so a lift shows on any colour.
if params.debug > 0.5 && params.world > 0.5 {
if lifted {
light *= vec3(1.0, 0.6, 1.0);
} else {
light = vec3(dot(light, vec3(0.2126, 0.7152, 0.0722)));
}
}
if eased {
light = min(light, vec3(params.peak));
}
return ReferenceLit(light, lifted);
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,335 @@
//! A held world (`WorldHold`, the native hook `native_world_hold`): the terrain goes on being
//! drawn as it stands while what changes under it is meshed, and all of it takes its place on
//! one frame. A section already on screen that is meshed again is uploaded to the arena as ever,
//! over as many frames as its mesh takes to come, but waits beside the mesh drawn (`Hold`);
//! letting go only swaps which of the two is drawn, with nothing left to upload. Sections not on
//! screen yet (terrain streaming in) are not held.
//!
//! Both meshes of a waiting section are in memory, so a hold costs up to what the changed land's
//! new meshes come to. It is bounded, and each bound lets go at once rather than losing anything:
//! `MAX_BYTES` of waiting meshes, `MAX_HOLD` of time, and the arena running out
//! (`Renderer::upload`). It never outlives what asked for it: the mods are asked every frame,
//! and the sections' being cleared (the world left, another dimension) drops what waits. A hold
//! that ended by itself (spent) stays ended until no mod has asked for a frame.
use super::SectionKey;
use crate::fast_hash::{FxHashMap, FxHashSet};
use std::time::{Duration, Instant};
/// The longest a hold lasts.
pub(super) const MAX_HOLD: Duration = Duration::from_secs(30);
/// The most bytes of new meshes that wait.
pub(super) const MAX_BYTES: u64 = 1 << 30;
/// The most circles a hold is kept to (`WorldHold::hold_within`).
pub const MAX_AREAS: usize = 64;
/// What the mods ask of the terrain's picture this frame (`native_world_hold`): nothing, all of
/// it held, or only the sections inside some circles. Several mods add up: one holding all of it
/// holds all of it, and circles join.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WorldHold {
all: bool,
within: Vec<[f64; 3]>,
}
impl WorldHold {
/// Hold the whole world's terrain as it is drawn, this frame.
pub fn hold(&mut self) {
self.all = true;
}
/// Hold the sections whose middle lies within `radius` blocks of (`x`, `z`), this frame. A
/// circle that isn't finite or has a negative radius is left out, as are those past
/// `MAX_AREAS`.
pub fn hold_within(&mut self, x: f64, z: f64, radius: f64) {
let good = x.is_finite() && z.is_finite() && radius.is_finite() && radius >= 0.0;
if good && self.within.len() < MAX_AREAS {
self.within.push([x, z, radius]);
}
}
/// Whether anything is held.
pub fn is_held(&self) -> bool {
self.all || !self.within.is_empty()
}
/// Whether section `key` is in what is held: all of the world, or a circle its middle lies in.
pub fn covers(&self, key: SectionKey) -> bool {
if self.all {
return true;
}
let (x, z) = (f64::from(key.0) * 16.0 + 8.0, f64::from(key.2) * 16.0 + 8.0);
self.within
.iter()
.any(|&[cx, cz, r]| (x - cx).powi(2) + (z - cz).powi(2) <= r * r)
}
}
/// What a frame's ask means for the sections waiting.
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Release {
/// They go on waiting.
None,
/// Those the hold no longer covers take their place.
Outside,
/// All of them take their place.
All,
}
/// A section waiting behind the hold and the bytes its new mesh takes.
struct Waiting<S> {
section: S,
bytes: u64,
}
/// The renderer's side of a hold: whether it is on, what it covers, and the sections waiting.
pub(super) struct Hold<S> {
ask: WorldHold,
since: Option<Instant>,
spent: bool,
waiting: FxHashMap<SectionKey, Waiting<S>>,
bytes: u64,
}
impl<S> Default for Hold<S> {
fn default() -> Self {
Self {
ask: WorldHold::default(),
since: None,
spent: false,
waiting: FxHashMap::default(),
bytes: 0,
}
}
}
impl<S> Hold<S> {
/// Whether a new mesh of section `key` waits.
pub fn holds(&self, key: SectionKey) -> bool {
self.since.is_some() && !self.spent && self.ask.covers(key)
}
pub fn is_empty(&self) -> bool {
self.waiting.is_empty()
}
pub fn bytes(&self) -> u64 {
self.bytes
}
/// A frame's ask, at `now`.
pub fn ask(&mut self, ask: &WorldHold, now: Instant) -> Release {
if !ask.is_held() {
let was = self.since.is_some();
(self.since, self.spent) = (None, false);
self.ask = WorldHold::default();
return if was && !self.waiting.is_empty() {
Release::All
} else {
Release::None
};
}
if self.spent {
return Release::None;
}
let since = *self.since.get_or_insert(now);
if now.saturating_duration_since(since) >= MAX_HOLD {
log::warn!(
"Horizon: a mod held the world for {} s: let go",
MAX_HOLD.as_secs()
);
self.spend();
return Release::All;
}
if *ask == self.ask {
return Release::None;
}
// (fewer circles or smaller: some of what waits is let go; more: nothing waits that didn't)
self.ask.clone_from(ask);
if self.waiting.is_empty() {
Release::None
} else {
Release::Outside
}
}
/// Ended by itself: nothing waits from now on, until no mod has asked for a frame.
/// What waited is the caller's to place (`take_all`) or drop.
pub fn spend(&mut self) {
self.spent = self.since.is_some();
}
/// Section `key`'s new mesh waits: the one that waited before for it comes back, to free.
/// The bool is whether what waits is now too much (`MAX_BYTES`).
pub fn wait(&mut self, key: SectionKey, section: S, bytes: u64) -> (Option<S>, bool) {
let old = self.take(key);
self.bytes += bytes;
self.waiting.insert(key, Waiting { section, bytes });
(old, self.bytes > MAX_BYTES)
}
/// Section `key`'s waiting mesh, no longer waiting.
pub fn take(&mut self, key: SectionKey) -> Option<S> {
if self.waiting.is_empty() {
return None;
}
let w = self.waiting.remove(&key)?;
self.bytes -= w.bytes;
Some(w.section)
}
/// Everything waiting, no longer waiting.
pub fn take_all(&mut self) -> Vec<(SectionKey, S)> {
self.bytes = 0;
self.waiting.drain().map(|(k, w)| (k, w.section)).collect()
}
/// What waits that the hold no longer covers.
pub fn take_outside(&mut self) -> Vec<(SectionKey, S)> {
let ask = &self.ask;
let all = self.since.is_none() || self.spent;
let out: Vec<_> = self
.waiting
.extract_if(|&k, _| all || !ask.covers(k))
.collect();
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
out.into_iter().map(|(k, w)| (k, w.section)).collect()
}
/// What waits in these columns, which are going.
pub fn take_columns(&mut self, columns: &FxHashSet<(i32, i32)>) -> Vec<S> {
if self.waiting.is_empty() {
return Vec::new();
}
let out: Vec<_> = self
.waiting
.extract_if(|k, _| columns.contains(&(k.0, k.2)))
.collect();
self.bytes -= out.iter().map(|(_, w)| w.bytes).sum::<u64>();
out.into_iter().map(|(_, w)| w.section).collect()
}
/// The sections are all gone: what waits goes with them and the hold is spent.
pub fn clear(&mut self) {
self.waiting.clear();
self.bytes = 0;
self.spend();
}
pub fn sections(&self) -> impl Iterator<Item = &S> {
self.waiting.values().map(|w| &w.section)
}
pub fn sections_mut(&mut self) -> impl Iterator<Item = &mut S> {
self.waiting.values_mut().map(|w| &mut w.section)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all() -> WorldHold {
let mut h = WorldHold::default();
h.hold();
h
}
fn within(circles: &[[f64; 3]]) -> WorldHold {
let mut h = WorldHold::default();
for &[x, z, r] in circles {
h.hold_within(x, z, r);
}
h
}
#[test]
fn a_hold_covers_sections_by_their_middle() {
assert!(all().covers((900, 0, -900)));
assert!(!WorldHold::default().covers((0, 0, 0)));
assert!(within(&[[8.0, 8.0, 1.0]]).covers((0, 3, 0)));
assert!(!within(&[[0.0, 0.0, 11.0]]).covers((0, 3, 0)));
assert!(within(&[[0.0, 0.0, 12.0]]).covers((0, 3, 0)));
assert!(within(&[[1000.0, 0.0, 1.0], [24.0, 8.0, 0.5]]).covers((1, 0, 0)));
assert!(within(&[[-8.0, -8.0, 0.0]]).covers((-1, 0, -1)));
}
#[test]
fn bad_circles_are_left_out_and_there_are_at_most_max_areas() {
let h = within(&[
[f64::NAN, 0.0, 5.0],
[0.0, f64::INFINITY, 5.0],
[0.0, 0.0, -1.0],
[0.0, 0.0, f64::INFINITY],
]);
assert!(!h.is_held());
let mut many = WorldHold::default();
for i in 0..MAX_AREAS + 10 {
many.hold_within(i as f64 * 100.0, 0.0, 1.0);
}
assert_eq!(many.within.len(), MAX_AREAS);
// (several mods: all of it from one holds all of it)
many.hold();
assert!(many.covers((-5000, 0, 5000)));
}
#[test]
fn a_hold_starts_and_lets_go_when_no_one_asks() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
assert!(!g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t), Release::None);
assert!(g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t + Duration::from_secs(5)), Release::None);
// (nothing waited: nothing to place)
assert_eq!(g.ask(&WorldHold::default(), t), Release::None);
g.ask(&all(), t);
g.wait((0, 0, 0), 7, 100);
assert_eq!(g.ask(&WorldHold::default(), t), Release::All);
assert_eq!(g.take_all(), vec![((0, 0, 0), 7)]);
assert!(!g.holds((0, 0, 0)));
}
#[test]
fn a_hold_ends_by_itself_and_stays_ended_until_let_go() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
assert_eq!(g.ask(&all(), t + MAX_HOLD), Release::All);
assert!(!g.holds((0, 0, 0)));
assert_eq!(g.ask(&all(), t + MAX_HOLD * 2), Release::None);
assert!(!g.holds((0, 0, 0)));
g.ask(&WorldHold::default(), t + MAX_HOLD * 2);
g.ask(&all(), t + MAX_HOLD * 3);
assert!(g.holds((0, 0, 0)));
}
#[test]
fn smaller_circles_let_go_what_they_leave() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&within(&[[0.0, 0.0, 100.0]]), t);
assert!(g.holds((0, 0, 0)) && g.holds((5, 0, 0)) && !g.holds((7, 0, 0)));
g.wait((0, 0, 0), 1, 10);
g.wait((5, 0, 0), 2, 20);
assert_eq!(g.ask(&within(&[[0.0, 0.0, 100.0]]), t), Release::None);
assert_eq!(g.ask(&within(&[[0.0, 0.0, 40.0]]), t), Release::Outside);
assert_eq!(g.take_outside(), vec![((5, 0, 0), 2)]);
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
assert!(!g.holds((5, 0, 0)));
// (to all of it again: nothing more is let go, and all of it waits again)
assert_eq!(g.ask(&all(), t), Release::Outside);
assert!(g.take_outside().is_empty());
assert!(g.holds((5, 0, 0)));
}
#[test]
fn what_waits_is_counted_and_capped() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
assert_eq!(g.wait((0, 0, 0), 1, MAX_BYTES - 10), (None, false));
// (a newer mesh of the same section replaces it, and the old one comes back to free)
assert_eq!(g.wait((0, 0, 0), 2, MAX_BYTES - 20), (Some(1), false));
assert_eq!(g.bytes(), MAX_BYTES - 20);
assert_eq!(g.wait((1, 0, 0), 3, 20), (None, false));
assert_eq!(g.wait((2, 0, 0), 4, 1), (None, true));
assert_eq!(g.take((1, 0, 0)), Some(3));
assert_eq!(g.take((1, 0, 0)), None);
assert_eq!(g.bytes(), MAX_BYTES - 19);
}
#[test]
fn columns_going_and_the_sections_cleared_drop_what_waits() {
let (mut g, t) = (Hold::<u32>::default(), Instant::now());
g.ask(&all(), t);
g.wait((0, 0, 0), 1, 10);
g.wait((0, 5, 0), 2, 10);
g.wait((3, 0, 0), 3, 10);
let gone: FxHashSet<(i32, i32)> = [(0, 0)].into_iter().collect();
let mut dropped = g.take_columns(&gone);
dropped.sort_unstable();
assert_eq!(dropped, vec![1, 2]);
assert_eq!((g.sections().count(), g.bytes()), (1, 10));
g.clear();
assert!(g.is_empty() && g.bytes() == 0 && !g.holds((3, 0, 0)));
// (cleared while held: spent until let go; with no hold, nothing to end)
assert_eq!(g.ask(&all(), t), Release::None);
assert!(!g.holds((3, 0, 0)));
let mut none = Hold::<u32>::default();
none.clear();
none.ask(&all(), t);
assert!(none.holds((3, 0, 0)));
}
}
@@ -0,0 +1,465 @@
//! HUD coverage beside its unchanged colour and alpha, only on the float HDR lane.
pub(crate) const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
pub(crate) fn supported(
attachments: u32,
flags: wgpu::DownlevelFlags,
format: wgpu::TextureFormatFeatures,
) -> bool {
attachments >= 2
&& flags.contains(wgpu::DownlevelFlags::INDEPENDENT_BLEND)
&& format
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING)
&& format
.flags
.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
}
pub(crate) fn attachment(view: &wgpu::TextureView) -> wgpu::RenderPassColorAttachment<'_> {
wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
}
}
pub(crate) fn clear(encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) {
let mut color = attachment(view);
color.ops.load = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("HUD coverage reset"),
color_attachments: &[Some(color)],
..Default::default()
});
}
pub(crate) struct Pipeline {
plain: wgpu::RenderPipeline,
coverage: Option<wgpu::RenderPipeline>,
}
impl Pipeline {
/// `None` when `enabled` and the shader can't be given a coverage output (a resource
/// pack's, which the caller then doesn't use).
pub(crate) fn new(
device: &wgpu::Device,
desc: &wgpu::RenderPipelineDescriptor<'_>,
source: wgpu::ShaderSource<'_>,
enabled: bool,
) -> Option<Self> {
let coverage = if enabled {
Some(pipeline(device, desc, source)?)
} else {
None
};
Some(Self {
plain: device.create_render_pipeline(desc),
coverage,
})
}
pub(crate) fn get(&self, coverage: bool) -> &wgpu::RenderPipeline {
if coverage {
self.coverage.as_ref().expect("HUD coverage pipeline")
} else {
&self.plain
}
}
}
/// Keep the primary output and all its blend factors, including destination alpha. Wrap the
/// final shader result, so pack text's alpha, early returns and discard all apply to coverage.
/// `None` for a fragment shader without a colour output at location 0.
pub(super) fn pipeline(
device: &wgpu::Device,
desc: &wgpu::RenderPipelineDescriptor<'_>,
source: wgpu::ShaderSource<'_>,
) -> Option<wgpu::RenderPipeline> {
let mut desc = desc.clone();
let fragment = desc.fragment.as_mut()?;
let mut module = match source {
wgpu::ShaderSource::Wgsl(s) => naga::front::wgsl::parse_str(&s).ok()?,
wgpu::ShaderSource::Naga(m) => m.into_owned(),
_ => return None,
};
let primary = fragment.targets.first()?.clone()?;
let blend = primary.blend.unwrap_or(wgpu::BlendState::REPLACE).color;
let additive = matches!(
(blend.src_factor, blend.dst_factor),
(
wgpu::BlendFactor::One
| wgpu::BlendFactor::Src
| wgpu::BlendFactor::SrcAlpha
| wgpu::BlendFactor::Zero,
wgpu::BlendFactor::One
)
) && blend.operation == wgpu::BlendOperation::Add;
wrap(&mut module, fragment.entry_point?, blend)?;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: desc.label,
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
});
let over = wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrc,
operation: wgpu::BlendOperation::Add,
};
let targets = [
Some(primary),
Some(wgpu::ColorTargetState {
format: FORMAT,
blend: Some(wgpu::BlendState {
color: over,
alpha: over,
}),
write_mask: if additive {
wgpu::ColorWrites::empty()
} else {
wgpu::ColorWrites::RED
},
}),
];
fragment.module = &shader;
fragment.targets = &targets;
Some(device.create_render_pipeline(&desc))
}
fn wrap(module: &mut naga::Module, name: &str, blend: wgpu::BlendComponent) -> Option<()> {
use naga::{Expression as E, Statement as S, TypeInner as T};
use wgpu::BlendFactor as B;
let span = naga::Span::UNDEFINED;
let entry = module.entry_points.iter_mut().find(|e| e.name == name)?;
// Checked before anything is taken apart: a resource pack's shader may have no colour.
let result = entry.function.result.clone()?;
if let T::Struct { members, .. } = &module.types[result.ty].inner {
members
.iter()
.find(|m| matches!(m.binding, Some(naga::Binding::Location { location: 0, .. })))?;
} else if !matches!(
result.binding,
Some(naga::Binding::Location { location: 0, .. })
) {
return None;
}
let mut original = std::mem::take(&mut entry.function);
entry.function.arguments = original.arguments.clone();
for arg in &mut original.arguments {
arg.binding = None;
}
original.result.as_mut().unwrap().binding = None;
original.name = Some("hud_color".into());
let function = module.functions.append(original, span);
let f = &mut entry.function;
let args = (0..f.arguments.len())
.map(|i| f.expressions.append(E::FunctionArgument(i as u32), span))
.collect();
let call = f.expressions.append(E::CallResult(function), span);
f.body.push(
S::Call {
function,
arguments: args,
result: Some(call),
},
span,
);
let one = f
.expressions
.append(E::Literal(naga::Literal::F32(1.0)), span);
let start = f.expressions.len();
let (mut members, mut values, color, offset) = match &module.types[result.ty].inner {
T::Struct {
members,
span: size,
} => {
// HUD pipelines have one target, so a pack's other colour outputs went nowhere;
// left in, one at location 1 would collide with the coverage.
let (members, values): (Vec<_>, Vec<_>) = members
.iter()
.enumerate()
.filter(|(_, m)| {
!matches!(m.binding, Some(naga::Binding::Location { location, .. }) if location != 0)
})
.map(|(i, m)| {
let value = f.expressions.append(
E::AccessIndex {
base: call,
index: i as u32,
},
span,
);
(m.clone(), value)
})
.unzip();
let color = members
.iter()
.position(|m| {
matches!(m.binding, Some(naga::Binding::Location { location: 0, .. }))
})
.unwrap();
let color = values[color];
(members, values, color, *size)
}
_ => (
vec![naga::StructMember {
name: Some("color".into()),
ty: result.ty,
binding: result.binding,
offset: 0,
}],
vec![call],
call,
16,
),
};
let mut component = |index| {
f.expressions
.append(E::AccessIndex { base: color, index }, span)
};
let [r, g, b, a] = [component(0), component(1), component(2), component(3)];
let mut math = |fun, arg, arg1| {
f.expressions.append(
E::Math {
fun,
arg,
arg1: Some(arg1),
arg2: None,
arg3: None,
},
span,
)
};
let low = math(naga::MathFunction::Min, r, g);
let low = math(naga::MathFunction::Min, low, b);
let high = math(naga::MathFunction::Max, r, g);
let high = math(naga::MathFunction::Max, high, b);
// A scalar cannot reproduce RGB-dependent replacement or inversion: surviving pixels
// cover fully, so the later composite cannot leak world light through them.
let c = if blend.operation != wgpu::BlendOperation::Add {
one
} else {
match (blend.src_factor, blend.dst_factor) {
(B::SrcAlpha | B::One, B::OneMinusSrcAlpha) => a,
(B::Zero, B::OneMinusSrc) => high,
// Multiplying retains src.rgb (or 1 - alpha + src.rgb) of the backdrop.
// Use the greatest channel darkening, as for the credits vignette above.
(B::Zero, B::Src)
| (B::Dst, B::Zero)
| (B::Dst, B::OneMinusSrcAlpha)
| (B::Dst, B::Src) => {
let top = if blend.dst_factor == B::OneMinusSrcAlpha {
a
} else {
one
};
let factor = if blend.dst_factor == B::Src && blend.src_factor == B::Dst {
f.expressions.append(
E::Binary {
op: naga::BinaryOperator::Add,
left: low,
right: low,
},
span,
)
} else {
low
};
f.expressions.append(
E::Binary {
op: naga::BinaryOperator::Subtract,
left: top,
right: factor,
},
span,
)
}
_ => one,
}
};
let c = f.expressions.append(
E::Math {
fun: naga::MathFunction::Saturate,
arg: c,
arg1: None,
arg2: None,
arg3: None,
},
span,
);
let scalar = module.types.insert(
naga::Type {
name: None,
inner: T::Scalar(naga::Scalar::F32),
},
span,
);
members.push(naga::StructMember {
name: Some("coverage".into()),
ty: scalar,
binding: Some(naga::Binding::Location {
location: 1,
interpolation: None,
sampling: None,
blend_src: None,
per_primitive: false,
}),
offset,
});
let ty = module.types.insert(
naga::Type {
name: Some("HudCoverage".into()),
inner: T::Struct {
members,
span: (offset + 4).next_multiple_of(16),
},
},
span,
);
values.push(c);
let value = f.expressions.append(
E::Compose {
ty,
components: values,
},
span,
);
f.result = Some(naga::FunctionResult { ty, binding: None });
f.body.push(S::Emit(f.expressions.range_from(start)), span);
f.body.push(S::Return { value: Some(value) }, span);
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coverage_requires_two_attachments_and_independent_r8_blending() {
let flags = wgpu::DownlevelFlags::INDEPENDENT_BLEND;
let format = wgpu::TextureFormatFeatures {
allowed_usages: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
flags: wgpu::TextureFormatFeatureFlags::BLENDABLE,
};
assert!(supported(2, flags, format));
assert!(!supported(1, flags, format));
assert!(!supported(2, wgpu::DownlevelFlags::empty(), format));
assert!(!supported(
2,
flags,
wgpu::TextureFormatFeatures {
flags: wgpu::TextureFormatFeatureFlags::empty(),
..format
}
));
assert!(!supported(
2,
flags,
wgpu::TextureFormatFeatures {
allowed_usages: wgpu::TextureUsages::TEXTURE_BINDING,
..format
}
));
assert!(!supported(
2,
flags,
wgpu::TextureFormatFeatures {
allowed_usages: wgpu::TextureUsages::RENDER_ATTACHMENT,
..format
}
));
}
#[test]
fn disabled_coverage_builds_only_the_original_pipeline() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, _)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(
"@vertex fn vs() -> @builtin(position) vec4<f32> { return vec4(0.0); }".into(),
),
});
let desc = wgpu::RenderPipelineDescriptor {
label: None,
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
primitive: Default::default(),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: Default::default(),
bias: Default::default(),
}),
multisample: Default::default(),
fragment: None,
multiview_mask: None,
cache: None,
};
// The coverage source must not even be parsed outside the lane.
let pipeline = Pipeline::new(
&device,
&desc,
wgpu::ShaderSource::Wgsl("invalid".into()),
false,
)
.unwrap();
assert!(pipeline.coverage.is_none());
}
#[test]
fn coverage_wraps_the_debug_crosshair_without_changing_world_entry_points() {
let mut module = naga::front::wgsl::parse_str(&crate::gfx::terrain_shader()).unwrap();
wrap(
&mut module,
"fs_debug_line",
wgpu::BlendState::ALPHA_BLENDING.color,
)
.unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
}
#[test]
fn coverage_replaces_a_colour_output_the_hud_has_no_target_for() {
let mut module = naga::front::wgsl::parse_str(
"struct Out { @location(0) color: vec4<f32>, @location(1) unused: vec4<f32> }
@fragment fn fs() -> Out { return Out(vec4(0.5), vec4(1.0)); }",
)
.unwrap();
wrap(&mut module, "fs", wgpu::BlendState::ALPHA_BLENDING.color).unwrap();
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.unwrap();
let result = module.entry_points[0].function.result.as_ref().unwrap();
let naga::TypeInner::Struct { members, .. } = &module.types[result.ty].inner else {
panic!("the wrapped entry returns a struct");
};
let names: Vec<_> = members.iter().map(|m| m.name.as_deref().unwrap()).collect();
assert_eq!(names, ["color", "coverage"]);
}
}
@@ -98,8 +98,24 @@ fn interfaces(source: &str) -> Vec<Interface> {
/// The shaders as the renderer assembles them, by the `.wgsl` files each is made of.
fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
let entity = crate::overlay::wgsl_constants() + include_str!("../entity.wgsl");
let entity = super::entity_shader();
vec![
(
"HDR",
vec!["hdr.wgsl", "hdr_light.wgsl"],
super::hdr::shader(),
),
("terrain", vec!["terrain.wgsl"], super::terrain_shader()),
(
"stars",
vec!["stars.wgsl"],
include_str!("../hdr_light.wgsl").to_owned() + include_str!("stars.wgsl"),
),
(
"custom sky",
vec!["custom_sky.wgsl"],
include_str!("../hdr_light.wgsl").to_owned() + include_str!("custom_sky.wgsl"),
),
("entity", vec!["entity.wgsl"], entity.clone()),
(
"GPU culling occlusion test",
@@ -124,14 +140,14 @@ fn composed() -> Vec<(&'static str, Vec<&'static str>, String)> {
(
"improved transparency world",
vec!["terrain.wgsl", "oit.wgsl", "oit_world.wgsl"],
include_str!("../terrain.wgsl").to_owned()
super::terrain_shader()
+ include_str!("../oit.wgsl")
+ include_str!("../oit_world.wgsl"),
),
(
"visibility buffer",
vec!["terrain.wgsl", "visbuffer.wgsl"],
include_str!("../terrain.wgsl").to_owned() + include_str!("../visbuffer.wgsl"),
super::terrain_shader() + include_str!("../visbuffer.wgsl"),
),
(
"improved transparency world border",
+21 -15
View File
@@ -50,8 +50,10 @@ impl Pending {
}
}
/// Small, its meshes boxed: `draw_frame` walks every tile each frame (thousands at long far
/// distances), and that walk is bound by the memory it reads.
pub(super) struct Tile {
pub mesh: SectionGpu,
pub mesh: Box<SectionGpu>,
bounds: Bounds,
/// When it began to fade in; none while the rest of its job uploads.
shown: Option<Instant>,
@@ -62,7 +64,7 @@ pub(super) struct Tile {
/// Its mesh before near chunks took some of its columns, drawn in its
/// place until they have faded in (from the job's end, the instant), so
/// no hole shows through them.
previous: Option<(SectionGpu, Option<Instant>)>,
previous: Option<Box<(SectionGpu, Option<Instant>)>>,
/// Its slot in occlusion culling's results (`cull::TILE`), kept while
/// meshes of the same tile replace one another.
pub cull_slot: u32,
@@ -155,10 +157,12 @@ pub(super) struct Visible {
impl Tile {
/// The mesh drawn this frame: the previous one while it stands in.
pub fn drawn(&self) -> &SectionGpu {
self.previous.as_ref().map_or(&self.mesh, |(mesh, _)| mesh)
self.previous
.as_deref()
.map_or(&self.mesh, |(mesh, _)| mesh)
}
pub fn drawn_mut(&mut self) -> &mut SectionGpu {
match &mut self.previous {
match self.previous.as_deref_mut() {
Some((mesh, _)) => mesh,
None => &mut self.mesh,
}
@@ -177,7 +181,7 @@ impl Tile {
}
/// Both meshes it holds in the arena: its own and the stand-in, if any.
pub fn meshes_mut(&mut self) -> impl Iterator<Item = &mut SectionGpu> {
std::iter::once(&mut self.mesh).chain(self.previous.as_mut().map(|(mesh, _)| mesh))
std::iter::once(&mut *self.mesh).chain(self.previous.as_deref_mut().map(|(mesh, _)| mesh))
}
pub fn visible(
&self,
@@ -214,12 +218,12 @@ impl Renderer {
let (shown, previous) = match self.lods.remove(&key) {
Some(old) if old.dying.is_none() => match old.previous {
Some(previous) => {
self.release_mesh(old.mesh);
self.release_mesh(*old.mesh);
(old.shown, Some(previous))
}
None if keep => (old.shown, Some((old.mesh, None))),
None if keep => (old.shown, Some(Box::new((*old.mesh, None)))),
None => {
self.release_mesh(old.mesh);
self.release_mesh(*old.mesh);
(old.shown, None)
}
},
@@ -240,7 +244,7 @@ impl Renderer {
self.lods.insert(
key,
Tile {
mesh,
mesh: Box::new(mesh),
bounds,
shown,
dying: None,
@@ -251,7 +255,7 @@ impl Renderer {
);
}
None => {
if let Some((previous, _)) = previous {
if let Some((previous, _)) = previous.map(|p| *p) {
self.release_mesh(previous);
}
self.lod_cull_free.extend(old_slot);
@@ -260,8 +264,8 @@ impl Renderer {
}
fn release_tile(&mut self, tile: Tile) {
self.release_mesh(tile.mesh);
if let Some((previous, _)) = tile.previous {
self.release_mesh(*tile.mesh);
if let Some((previous, _)) = tile.previous.map(|p| *p) {
self.release_mesh(previous);
}
}
@@ -337,8 +341,10 @@ impl Renderer {
let stood_in: Vec<_> = self
.lods
.values_mut()
.filter_map(|tile| match tile.previous {
Some((_, Some(since))) if start - since >= FADE => tile.previous.take(),
.filter_map(|tile| match tile.previous.as_deref() {
Some((_, Some(since))) if start - *since >= FADE => {
tile.previous.take().map(|p| *p)
}
_ => None,
})
.collect();
@@ -440,7 +446,7 @@ impl Renderer {
if tile.shown.is_none() {
tile.shown = Some(if over_replaced(key) { start } else { whole });
}
if let Some((_, since)) = &mut tile.previous {
if let Some((_, since)) = tile.previous.as_deref_mut() {
since.get_or_insert(start);
}
}
@@ -49,9 +49,23 @@ struct HorizonMaterialOut {
@location(14) @interpolate(flat) normal: vec3<f32>,
}
fn horizon_material_vertex(position: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
light: vec2<u32>, origin: vec3<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin, uv, layer, color, light);
return horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
light: vec2<u32>, origin: vec4<f32>) -> HorizonMaterialOut {
let pos = horizon_material_displace(position + origin.xyz, uv, layer, color, light);
var out = horizon_material_out(pos, uv, layer, color, vec2<f32>(light));
// The section's fade-in (`RenderSection.getVisibility`), as how far it is from whole, in the
// high 16 bits of `hdr_code`: no varying of its own, and 0 (whole) where nothing fades. It is
// the same for the whole section, so the flat varying loses nothing.
let instance = horizon_terrain_instance_visibility(origin.w);
out.hdr_code |= u32(round((1.0 - instance) * 65535.0)) << 16u;
return out;
}
// terrain.wgsl's fade-in (terrain.vsh's chunkVisibility): whole within 16 blocks, the section's
// own visibility from 32 out, toward the fog colour, alpha kept. The distance blend runs here on
// the interpolated distance, as vanilla's interpolated `visibility`, not once per triangle.
fn horizon_material_faded(in: HorizonMaterialOut, color: vec4<f32>) -> vec4<f32> {
let instance = 1.0 - f32(in.hdr_code >> 16u) / 65535.0;
let visibility = mix(1.0, instance, clamp((in.spherical - 16.0) / 16.0, 0.0, 1.0));
return vec4(mix(horizon_terrain_globals.fog_color.rgb, color.rgb, visibility), color.a);
}
// `light`: sky, block, 16 per level.
fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f32>,
@@ -94,7 +108,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec2<u32>,
@location(5) origin: vec4<f32>) -> HorizonMaterialOut {
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin.xyz);
color, light, origin);
}
@vertex fn horizon_material_vs_flat(
@builtin(vertex_index) vertex: u32,
@@ -103,7 +117,7 @@ fn horizon_material_out(pos: vec3<f32>, uv: vec2<f32>, layer: u32, color: vec4<f
) -> HorizonMaterialOut {
let origin = horizon_terrain_slot_origins[horizon_terrain_block_slots[vertex >> 8u]];
return horizon_material_vertex(position, horizon_terrain_terrain_uv(uv, origin.w), layer,
color, light, origin.xyz);
color, light, origin);
}
// The prelude's `block_normal_map` and `block_specular_map`: the resource packs' LabPBR maps
// in the pack's frame group (3), one array each, a blank 1x1 without them.
@@ -181,9 +195,11 @@ struct HorizonMaterialSolid {
// What `fs_solid` leaves in the frame's alpha for HDR output, from the sprite before the pack's
// material: the pack decides the colour, Horizon still knows which pixels are light sources.
fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
if in.hdr_code > 0u {
let whole = in.hdr_code > 4u;
let tier = select(in.hdr_code, in.hdr_code - 4u, whole);
if horizon_terrain_HDR_WRITE_LIGHT { return 1.0; }
let code = in.hdr_code & 15u;
if code > 0u {
let whole = code > 4u;
let tier = select(code, code - 4u, whole);
let l = in.layer & 0x7fffu;
let t = textureLoad(horizon_terrain_emission_thresholds,
vec2(i32(l % horizon_terrain_SPRITE_SPLIT), i32(l / horizon_terrain_SPRITE_SPLIT)), 0);
@@ -202,7 +218,7 @@ fn horizon_material_mark(in: HorizonMaterialOut, texel: vec4<f32>) -> f32 {
return horizon_material_solid(in);
}
fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
let color = horizon_material_color(in, false);
let color = horizon_material_faded(in, horizon_material_color(in, false));
let texel = horizon_terrain_sprite(in.uv, in.layer & 0x7fffu);
// Bit 15 of the layer marks opaque leaves (forceOpaque): no alpha cutout.
if color.a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
@@ -216,7 +232,7 @@ fn horizon_material_solid(in: HorizonMaterialOut) -> HorizonMaterialSolid {
return out;
}
@fragment fn horizon_material_fs_translucent(in: HorizonMaterialOut) -> @location(0) vec4<f32> {
let color = horizon_material_color(in, true);
let color = horizon_material_faded(in, horizon_material_color(in, true));
if color.a < 0.01 { discard; }
return horizon_material_fogged(color, in.spherical, in.cylindrical);
}
@@ -294,7 +310,8 @@ pub struct NativeMaterial {
/// every name it declares prefixed.
fn vanilla_terrain() -> String {
prefixed(
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
&(include_str!("../hdr_light.wgsl").to_owned()
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
PREFIX,
)
}
@@ -416,6 +433,10 @@ impl NativeMaterial {
];
constants.extend(sprite_names.into_iter().zip(shape.sprites));
constants.push(("horizon_material_fog", f64::from(u8::from(fog))));
constants.push((
"horizon_terrain_HDR_WRITE_LIGHT",
f64::from(u8::from(shape.format == super::LINEAR_FRAME)),
));
constants.push((
"horizon_displace_distance",
displaced.map_or(0.0, |d| f64::from(d.distance)),
@@ -564,6 +585,39 @@ impl NativeMaterial {
}
}
#[cfg(test)]
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
let manifest = format!(
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
);
let material = "fn material(input: MaterialInput) -> Surface { return Surface(vec4<f32>(1.4, 0.3, 0.1, 0.7)); }\nfn displacement(input: DisplacementInput) -> f32 { return 0.0; }\n";
let files = [(MANIFEST, manifest.as_str()), ("material.wgsl", material)]
.map(|(n, text)| (n.to_owned(), text.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let stage = report
.stages
.iter()
.find(|s| s.kind == StageKind::Material)
.unwrap();
shader_source(&stage.source, &[], &[], (false, true), api)
+ r#"
override TEST_TIER: u32 = 0u;
override TEST_OVERLAY: u32 = 0u;
@vertex fn test_vertex(@builtin(vertex_index) i: u32) -> HorizonMaterialOut {
var o: HorizonMaterialOut;
o.clip = vec4(vec2(f32((i << 1u) & 2u), f32(i & 2u)) * 2.0 - 1.0, 0.0, 1.0);
o.uv = vec2(0.5);
o.tint = vec4(1.0);
o.lightmap = vec3(1.0);
o.hdr_code = TEST_TIER;
o.base = vec3(1.0);
return o;
}
"#
}
#[cfg(test)]
mod tests {
use super::*;
@@ -582,6 +636,25 @@ mod tests {
report.stages.remove(0)
}
/// The fade-in packed above the HDR code: a section a quarter visible (`section_instance`
/// with step 1: mantissa `1 - visibility` = 0.75) keeps its visibility through the code, and
/// a fragment 24 blocks away, half-way into the 16-block blend, is 1 - (1 - 0.25) / 2 = 0.625
/// visible; the code's low nibble survives.
#[test]
fn the_fade_in_rides_the_hdr_code() {
let w = f32::from_bits(1.0f32.to_bits() | (0.75 * 8_388_608.0) as u32);
// `horizon_material_vertex`.
let instance = 1.0 - (w.to_bits() & 0x7fffff) as f32 / 8_388_608.0;
let code = 7u32 | (((1.0 - instance) * 65535.0).round() as u32) << 16;
// `horizon_material_faded` and `horizon_material_mark`.
let unpacked = 1.0 - (code >> 16) as f32 / 65535.0;
assert!((unpacked - 0.25).abs() < 1.0 / 65535.0);
let blend = ((24.0f32 - 16.0) / 16.0).clamp(0.0, 1.0);
let visibility = 1.0 + (unpacked - 1.0) * blend;
assert!((visibility - 0.625).abs() < 1.0 / 65535.0);
assert_eq!(code & 15, 7);
}
#[test]
fn the_terrain_takes_a_prefix_and_validates_with_a_pack() {
let terrain = vanilla_terrain();
@@ -90,6 +90,27 @@ impl NativePack {
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
// A backend may refuse a layout or pipeline the pack asks for (a stage's bindings
// past its sampled-texture limit, say, as OpenGL's 16 was before #10095): the pack
// is refused with that error instead of a validation panic in the renderer.
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let built = Self::build(device, queue, terrain, report, constants, lights);
let refused = pollster::block_on(scope.pop());
match (built, refused) {
(Ok(built), None) => Ok(built),
(Ok(_), Some(e)) => Err(e.to_string()),
(Err(e), _) => Err(e),
}
}
fn build(
device: &wgpu::Device,
queue: &wgpu::Queue,
terrain: &TerrainShape<'_>,
report: Report,
constants: &[(String, f64)],
lights: super::block_light::LightTable,
) -> Result<(Self, Vec<String>), String> {
let (globals, format) = (terrain.globals, terrain.format);
let textures = super::native_post::pack_textures(device, queue, &report);
@@ -278,9 +299,90 @@ impl NativePack {
#[cfg(test)]
mod tests {
use super::chosen_shadow;
use super::{NativePack, TerrainShape, chosen_shadow};
use horizon_shader_api::manifest::{OptionKind, PackOption, Shadow};
/// A device with OpenGL's minimum 16 sampled textures per stage refuses a pack whose
/// sky adds the terrain's 10 to the frame's 9: that is the pack's error to report, not
/// a validation panic (#10095).
#[test]
fn a_pack_the_backend_refuses_is_an_error_not_a_panic() {
use horizon_shader_api::{MANIFEST, Pack, check};
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let mut limits = adapter.limits();
limits.max_sampled_textures_per_shader_stage = 16;
let Ok((device, queue)) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: limits,
..Default::default()
}))
else {
return;
};
// The terrain's group 0: its uniform and the 10 sampled textures (_terrain.wgsl_'s
// 8 sprite arrays, the lightmap and the emission thresholds).
let entry = |binding, ty| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty,
count: None,
};
let mut entries = vec![entry(
0,
wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
)];
entries.extend((1..=10).map(|binding| {
entry(
binding,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
)
}));
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &entries,
});
let empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[],
});
let terrain = TerrainShape {
globals: &globals,
flat: None,
sprites: [1.0, 2048.0],
format: wgpu::TextureFormat::Rgba8Unorm,
sprite_tags: &[],
sprite_materials: &[],
entities: &empty,
weather_groups: [&empty, &empty],
pbr: None,
};
let manifest = "schema = 1\nid = \"t\"\nname = \"T\"\nversion = \"1\"\napi = \"0.1\"\n\
[sky]\nfile = \"sky.wgsl\"\n";
let files = [
(MANIFEST, manifest),
(
"sky.wgsl",
"fn sky(input: SkyInput) -> vec4<f32> { return input.color; }\n",
),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let built = NativePack::new(&device, &queue, &terrain, report, &[], Default::default());
assert!(built.is_err(), "the pack should be refused");
}
fn option(id: &str, kind: OptionKind) -> PackOption {
PackOption {
id: id.into(),
@@ -849,6 +849,15 @@ impl NativeFrame {
inputs
}
/// Forgets the frames before, as a freshly loaded pack: the post stage's next `history` is
/// black, eased values start at their targets, wetness dry, the frame count at 0.
pub(super) fn reset_history(&mut self) {
self.last = None;
self.frames = 0;
self.wetness = (0.0, None);
self.start = std::time::Instant::now();
}
/// Takes the post stage's pending history reset. It stays pending if post rendering is
/// skipped for a frame, so the first frame it does render still sees black history.
pub(super) fn take_history_reset(&mut self) -> bool {
@@ -1119,6 +1128,13 @@ impl NativePost {
.collect()
}
/// Forgets the pack's own targets (`[[post]] output`): made again, blank, on the next frame,
/// as for a pack just made. Its pipelines stay.
pub(super) fn reset_history(&mut self) {
self.named = None;
self.groups = None;
}
fn prepare(
&mut self,
device: &wgpu::Device,
@@ -1737,6 +1753,152 @@ mod tests {
assert!(!native.take_history_reset());
}
/// After a reset, the same frames give the same inputs, to the bit, as a pack just made,
/// however the frames before moved the eased values, wetness and the frame count.
#[test]
fn a_reset_frame_gives_a_fresh_pack_s_inputs() {
use crate::gfx::pack_uniforms::FrameTiming;
let Some((device, _)) = device() else {
return;
};
let hud = crate::hud::HudBatch::new([4, 4]);
let mut frame = crate::gfx::pack_uniforms::initial_history_tests::frame(&hud);
let new = || {
NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_1,
)
};
let (mut used, mut fresh) = (new(), new());
frame.pack.rain = 1.0;
frame.pack.eye_light = [12, 3];
frame.pack.eye_in = 1;
for n in 1..=5 {
frame.pack.frame_timing = Some(FrameTiming::nth(n, 700.0));
used.next_inputs(&frame, [4, 4]);
}
used.reset_history();
frame.pack.eye_light = [2, 15];
frame.pack.eye_in = 0;
for n in 1..=3 {
frame.pack.frame_timing = Some(FrameTiming::nth(n, 50.0));
let inputs = [&mut used, &mut fresh].map(|f| f.next_inputs(&frame, [4, 4]));
assert_eq!(
bytemuck::bytes_of(&inputs[0]),
bytemuck::bytes_of(&inputs[1]),
"frame {n}"
);
assert_eq!(used.take_history_reset(), fresh.take_history_reset());
}
}
/// After a reset a pass reads its pack's own targets blank, as in a pack just made: here
/// `fb`, which a pass reads before the pass that writes it, so it holds last frame's.
#[test]
fn a_reset_post_reads_its_own_targets_blank() {
let Some((device, queue)) = device() else {
return;
};
let manifest = "schema = 1
id = \"t\"
name = \"T\"
version = \"1\"
api = \"0.1\"
[[post]]
file = \"a.wgsl\"
[[post]]
file = \"b.wgsl\"
output = \"fb\"
[[post]]
file = \"c.wgsl\"
";
let a = "fn post(input: PostInput) -> vec4<f32> { return vec4<f32>(textureLoad(fb, vec2<i32>(input.pixel), 0).rgb + 0.25, 1.0); }
";
let pass = "fn post(input: PostInput) -> vec4<f32> { return input.color; }
";
let files = [
(MANIFEST, manifest),
("a.wgsl", a),
("b.wgsl", pass),
("c.wgsl", pass),
]
.map(|(n, t)| (n.to_owned(), t.as_bytes().to_vec()))
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let format = wgpu::TextureFormat::Rgba8Unorm;
let frame = NativeFrame::new(
&device,
None,
Vec::new(),
Default::default(),
None,
Api::V0_1,
);
let mut post = NativePost::new(
&device,
report.stages,
&report.manifest,
&[],
format,
&frame,
None,
)
.unwrap();
let usage = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING;
let color = texture(&device, format, usage).create_view(&Default::default());
let depth = texture(&device, wgpu::TextureFormat::Depth32Float, usage)
.create_view(&Default::default());
let screen = texture(
&device,
format,
wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
);
let view = screen.create_view(&Default::default());
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT * 4),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let red = |post: &mut NativePost| {
let mut encoder = device.create_command_encoder(&Default::default());
let world = (&color, &depth, &frame.surface_view);
post.run(&device, &mut encoder, world, &view, [4, 4], false, None);
encoder.copy_texture_to_buffer(
screen.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT),
rows_per_image: Some(4),
},
},
screen.size(),
);
queue.submit([encoder.finish()]);
buffer
.slice(..)
.map_async(wgpu::MapMode::Read, |r| r.unwrap());
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let red = buffer.slice(..).get_mapped_range().unwrap()[0];
buffer.unmap();
red
};
let near = |got: u8, want: u8| got.abs_diff(want) <= 1;
for want in [64, 128, 191] {
let got = red(&mut post);
assert!(near(got, want), "{got} before the reset, {want} expected");
}
post.reset_history();
let got = red(&mut post);
assert!(near(got, 64), "{got} after the reset: fb kept last frame's");
}
fn device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
@@ -73,10 +73,15 @@ pub struct WorldDrawn<'a> {
pub entities: &'a [Vertex],
pub spans: &'a [EntitySpan],
/// Which entity each run of `entities` is, recorded every frame (`Frame::entity_motion`):
/// the run's first vertex and the entity, `(1 << 32) | id` for a mob or a player and
/// `(2 << 32) | id` for a dropped item (their entity ids), 0 for anything else. A run lasts
/// until the next. Unlike `spans`, whose ids name a type, these follow one entity from
/// frame to frame (API 371).
/// the run's first vertex and the entity, `(1 << 32) | id` for a mob or a player,
/// `(2 << 32) | id` for a dropped item, `(3 << 32) | id` for another entity (boat,
/// minecart, armor stand, item frame, painting, primed TNT, falling block) and
/// `(4 << 32) | id` for an item frame's held item (their entity ids), `(5 << 32) | p` for a
/// block entity (`p`: its position, x and z in 11 bits each, y in 10), `(6 << 32) | p`
/// for a block a piston moves (`p`: the cell it moves to), `7 << 32` for the player's own
/// model, 0 for anything else.
/// A run lasts until the next. Unlike `spans`, whose ids name a type, these follow one
/// entity from frame to frame (API 371).
pub motion: &'a [(u32, u64)],
/// The shared atlas entity UVs point into.
pub atlas: Option<&'a wgpu::Texture>,
@@ -512,7 +512,8 @@ fn shader_source(
format!(
"{head}// Horizon's terrain (terrain.wgsl)\n{}{solid}{}{displace}{ENTRIES}",
prefixed(
section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)"),
&(include_str!("../hdr_light.wgsl").to_owned()
+ section(TERRAIN, "struct Globals {", "// Sky (SkyRenderer)")),
PREFIX
),
super::native_post::line_helpers(api)
@@ -30,6 +30,7 @@ pub struct NativeSky {
/// terrain.wgsl's sky with every name it declares (and `globals`) prefixed.
fn vanilla_sky() -> String {
let text = [
include_str!("../hdr_light.wgsl"),
section(TERRAIN, "struct Globals {", "@group(0) @binding(1)"),
section(TERRAIN, "fn linear_fog(", "fn apply_fog("),
section(TERRAIN, "struct SkyOut {", "// rendertype_lines"),
+22 -5
View File
@@ -192,7 +192,7 @@ impl Oit {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon improved transparency"),
source: wgpu::ShaderSource::Wgsl(
(include_str!("../terrain.wgsl").to_owned()
(super::terrain_shader()
+ include_str!("../oit.wgsl")
+ include_str!("../oit_world.wgsl"))
.into(),
@@ -253,7 +253,14 @@ impl Oit {
..Default::default()
})
};
let constants = [("SPRITE_ARRAYS", sprites[0]), ("SPRITE_SPLIT", sprites[1])];
let constants = [
("SPRITE_ARRAYS", sprites[0]),
("SPRITE_SPLIT", sprites[1]),
(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
),
];
let add = wgpu::BlendOperation::Add;
let depth = |write| {
Some(wgpu::DepthStencilState {
@@ -464,7 +471,10 @@ impl Oit {
fragment: Some(wgpu::FragmentState {
module: &cloud_shader,
entry_point: Some(fragment),
compilation_options: Default::default(),
compilation_options: wgpu::PipelineCompilationOptions {
constants: &constants[2..],
..Default::default()
},
targets: &stage_targets(stage),
}),
multiview_mask: None,
@@ -488,7 +498,14 @@ impl Oit {
Stage::Accumulate,
),
];
let features = Features::new(device, &layouts, features[0], features[1], features[2]);
let features = Features::new(
device,
&layouts,
features[0],
features[1],
features[2],
format,
);
Self {
uniform,
layouts,
@@ -912,7 +929,7 @@ mod tests {
#[test]
fn the_terrain_shader_validates() {
validate(
&(include_str!("../terrain.wgsl").to_owned()
&(crate::gfx::terrain_shader()
+ include_str!("../oit.wgsl")
+ include_str!("../oit_world.wgsl")),
&[
@@ -55,6 +55,7 @@ impl Features {
entity: &wgpu::BindGroupLayout,
weather: &wgpu::BindGroupLayout,
border: &wgpu::BindGroupLayout,
format: wgpu::TextureFormat,
) -> Self {
let entity_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Horizon improved transparency features"),
@@ -113,6 +114,15 @@ impl Features {
bind_group_layouts: &[groups[0], groups[1], Some(&layouts[stage as usize])],
..Default::default()
});
let fragment = if kind.label == "text" && stage == Stage::Accumulate {
"fs_oit_text_accumulate".to_owned()
} else {
format!("fs_oit_{}_{suffix}", kind.name)
};
let constants = [(
"HDR_WRITE_LIGHT",
f64::from(u8::from(format == super::LINEAR_FRAME)),
)];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&format!("OIT {} {suffix}", kind.label)),
layout: Some(&layout),
@@ -136,8 +146,15 @@ impl Features {
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module,
entry_point: Some(&format!("fs_oit_{}_{suffix}", kind.name)),
compilation_options: Default::default(),
entry_point: Some(&fragment),
compilation_options: wgpu::PipelineCompilationOptions {
constants: if kind.name == "border" {
&[]
} else {
&constants
},
..Default::default()
},
targets: &stage_targets(stage),
}),
multiview_mask: None,
@@ -728,6 +728,13 @@ impl PackAtlas {
),
("mc_Entity", entity),
("at_midBlock", mid_block),
// Horizon's own: the face's HDR tier (`mesher::emitting`'s code in the colour's low
// alpha nibble): 0 for none, 1 to 4 for low to very bright, plus 4 when the whole
// face shines (`mesher::WHOLE`).
(
"horizonTier",
"float(15u - (uint(round(hz_v_color.a * 255.0)) & 15u))",
),
(
"mc_midTexCoord",
"vec4((hz_cell + hz_mid) * hz_scale, 0.0, 1.0)",
@@ -750,6 +757,7 @@ impl PackAtlas {
alpha_test: None,
item_id_declared: false,
far_depth: false,
paged_maps: false,
}
}
}
@@ -157,6 +157,7 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
flat,
geometry,
origin,
0x80ffffff,
);
let fade = if requested { expected } else { 1.0 };
let expected = [
@@ -182,6 +183,96 @@ fn terrain_fade_reaches_pack_pixels_through_real_instances() {
assert_eq!(count, 98);
}
/// `horizonTier`: the HDR tier the mesher marks a light source's faces with (`mesher::emitting`),
/// read by a pack's terrain program, flat or instanced.
#[test]
fn terrain_tier_reaches_pack_pixels() {
let instance = crate::gfx::test_gpu::instance();
let adapter = match pollster::block_on(instance.request_adapter(&Default::default())) {
Ok(adapter) => adapter,
Err(error) => {
eprintln!("SKIP terrain tier GPU control: no adapter: {error}");
return;
}
};
let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let array = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 2,
height: 2,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let mut encoder = device.create_command_encoder(&Default::default());
let atlas = PackAtlas::new(&device, &mut encoder, &[array], 1, 1, 2).unwrap();
queue.submit([encoder.finish()]);
let folder = ProbePack(
std::env::temp_dir().join(format!("horizon-terrain-tier-{}", std::process::id())),
);
std::fs::create_dir_all(folder.0.join("shaders")).unwrap();
std::fs::write(
folder.0.join("shaders/gbuffers_terrain.vsh"),
"#version 330\nin float horizonTier;\nout vec4 observation;\n\
void main() {\nobservation = vec4(horizonTier, 0.0, 0.0, 0.0);\n\
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n}",
)
.unwrap();
std::fs::write(
folder.0.join("shaders/gbuffers_terrain.fsh"),
"#version 330\nin vec4 observation;\nlayout(location = 0) out uvec4 pixel;\n\
void main() { pixel = floatBitsToUint(observation); }",
)
.unwrap();
let pack = horizon_shaders::Pack::open(&folder.0).unwrap();
let mut macros = horizon_shaders::Macros::standard();
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
use crate::mesher::{BRIGHT, LOW, VERY_BRIGHT, WHOLE, emitting};
// No light, low, very bright, and bright all over (4 more).
let cases = [
(0, 0.0),
(LOW, 1.0),
(VERY_BRIGHT, 4.0),
(BRIGHT | WHOLE, 7.0),
];
for flat in [false, true] {
let post = horizon_shaders::load_post(
&pack,
"",
&properties,
&macros,
&horizon_shaders::GameInputs {
terrain: Some(atlas.terrain_inputs(flat)),
..Default::default()
},
);
let program = post
.terrain
.as_ref()
.unwrap_or_else(|| panic!("{:?}", post.notes));
for (tier, expected) in cases {
let origin = section_instance([0.0; 3], 0, 1.0);
let pixel = sample(
&device,
&queue,
&atlas,
program.program.as_ref().unwrap(),
flat,
false,
origin,
emitting(0xffff_ffff, tier),
);
assert_eq!(pixel[0], expected, "tier {tier} flat={flat}");
}
}
}
fn sample(
device: &wgpu::Device,
queue: &wgpu::Queue,
@@ -190,6 +281,7 @@ fn sample(
flat: bool,
geometry: bool,
origin: [f32; 4],
color: u32,
) -> [f32; 4] {
let buffer = |bytes: &[u8], usage| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -209,7 +301,7 @@ fn sample(
let slots = buffer(bytemuck::bytes_of(&0u32), wgpu::BufferUsages::STORAGE);
let vertices: Vec<_> = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]]
.into_iter()
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, 0x80ffffff, 0))
.map(|pos| TerrainVertex::new(pos, [0.25, 0.75], 0, color, 0))
.collect();
let data = buffer(
bytemuck::cast_slice(&vertices),
@@ -217,6 +217,41 @@ impl CenterDepth {
})
}
/// Once the GPU has finished the frame (`screenshot-sequence`), the depth it copied, as
/// the next frame reads it: an error unless exactly that one sample was read back, never
/// an older one.
pub fn take_completed(&mut self, device: &wgpu::Device) -> Result<f32, String> {
device
.poll(wgpu::PollType::Poll)
.map_err(|e| e.to_string())?;
let state = |s: &Slot| s.state.load(Ordering::Acquire);
let mut mapped = self.slots.iter().filter(|s| state(s) == MAPPED);
let (Some(slot), None) = (mapped.next(), mapped.next()) else {
return Err("the frame's centre depth wasn't read back alone".into());
};
if self
.slots
.iter()
.any(|s| !matches!(state(s), FREE | MAPPED))
{
return Err("the frame's centre depth wasn't read back alone".into());
}
let depth = slot
.buffer
.slice(..4)
.get_mapped_range()
.map(|bytes| bytemuck::pod_read_unaligned::<f32>(&bytes[..4]))
.map_err(|e| e.to_string());
slot.buffer.unmap();
slot.state.store(FREE, Ordering::Release);
let depth = depth?;
if !depth.is_finite() {
return Err("the centre depth isn't finite".into());
}
self.latest = Some(depth.clamp(0.0, 1.0));
Ok(depth.clamp(0.0, 1.0))
}
/// The newest depth read back so far, `None` before the first arrives.
pub fn collect(&mut self, device: &wgpu::Device) -> Option<f32> {
let _ = device.poll(wgpu::PollType::Poll);
@@ -265,6 +300,76 @@ mod tests {
});
}
/// Inline and on the submit thread: each finished frame's depth, then none left to
/// take; several outstanding samples are refused.
#[test]
#[ignore = "requires a host GPU"]
fn a_finished_frame_s_depth_is_taken_alone_never_an_older_one() {
let (device, raw) = test_device().expect("host GPU required");
let depth = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 3,
height: 3,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = depth.create_view(&Default::default());
for threaded in [false, true] {
let queue = super::super::submit::Queue::new(raw.clone(), threaded);
if threaded {
assert!(queue.threaded(), "deferred submit unavailable");
}
let mut center = CenterDepth::new(&device);
assert!(center.take_completed(&device).is_err());
for value in [0.125, 0.75, 0.25] {
let mut encoder = device.create_command_encoder(&Default::default());
clear(&mut encoder, &view, value);
center.copy(&device, &mut encoder, &depth);
let after = center.submitted().expect("one copied sample");
if threaded {
let job = super::super::submit::Job {
encoder,
surface: None,
after: Some(Box::new(after)),
};
assert!(queue.submit_later(job).is_ok());
queue.wait();
} else {
queue.submit([encoder.finish()]);
after();
}
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(std::time::Duration::from_secs(30)),
})
.unwrap();
assert_eq!(center.take_completed(&device).unwrap(), value);
assert_eq!(center.collect(&device), Some(value));
assert!(
center.take_completed(&device).is_err(),
"no stale latest fallback"
);
}
// Several outstanding copies are ambiguous, even after they all map.
let mut encoder = device.create_command_encoder(&Default::default());
for _ in 0..SLOTS {
center.copy(&device, &mut encoder, &depth);
}
queue.submit([encoder.finish()]);
center.submitted().unwrap()();
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
assert!(center.take_completed(&device).is_err());
}
}
/// Frames that go to the submit thread start their readback there: the centre depth
/// follows the frames instead of keeping the first ones' depth.
#[test]
@@ -30,7 +30,7 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
/// What `gbuffers_entities` and `gbuffers_block` read from an entity vertex: camera-relative
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
/// the vertex has none (block entities) and `at_tangent` along the texture's u, as for terrain.
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
let words = std::mem::size_of::<Vertex>() / 4;
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
@@ -107,6 +107,10 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
"at_velocity",
"hz_velocity(gbufferModelView, gbufferPreviousModelView)",
),
// Horizon's own, as for terrain: the HDR tier of a light-giving item, particle or glowing
// text (`mesher::hdr_tier`, the overlay's high bits), 0 for none, 1 to 4 for low to very
// bright.
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
];
horizon_shaders::GbuffersInputs {
declarations,
@@ -123,6 +127,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
}),
item_id_declared: false,
far_depth: false,
// Entities, items and block entities read the shared atlas: its own maps (paged_maps.rs).
paged_maps: true,
}
}
@@ -186,6 +192,7 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
alpha_test: None,
item_id_declared: false,
far_depth: false,
paged_maps: false,
}
}
@@ -454,9 +461,31 @@ impl PackEntities {
let first = spans.first().map_or(entities.len(), |s| s.start as usize);
extend_without_portals(rest, &entities[..first.min(entities.len())]);
let (mut other, mut portal_faces, mut beam_faces) = (Vec::new(), Vec::new(), Vec::new());
let mut without_fluids = Vec::new();
for (span, range) in runs(entities.len(), spans) {
let run_start = range.start as u32;
let whole = &entities[range];
// A native fluid carries sprite-array UVs, not shared-atlas UVs.
// Keep it in its own material even when a shader pack draws the
// model in the same block-entity run. Do not corrupt either UV space.
let whole: &[Vertex] = if whole
.chunks(4)
.any(|q| super::is_native_terrain(q[0].layer))
{
without_fluids.clear();
for quad in whole.chunks(4) {
if super::is_native_terrain(quad[0].layer) {
if !span.hidden {
rest.extend_from_slice(quad);
}
} else {
without_fluids.extend_from_slice(quad);
}
}
&without_fluids
} else {
whole
};
// A block entity's run can hold End portal faces and beams beside plain parts (an End
// gateway: its cube and its beam): each goes to its own program.
// Beacon beams come in a run of their own, at stage NONE (game.rs: no stage of 5.4
@@ -1141,6 +1170,83 @@ fn skin_runs(skin: &super::entity_skin::SkinFrame) -> Vec<(EntitySpan, Range<u32
mod gpu_span_tests {
use super::*;
use crate::gfx::entity_skin::{SkinFrame, SkinJob};
#[test]
#[ignore = "requires a Vulkan/Metal/DX12 adapter"]
fn native_fluid_stays_native_while_model_uses_pack() {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
let (device, queue) =
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
let layout = super::super::pack_draw::DrawUniforms::new(&device);
let mut geometry = PackEntities::new(&device, &layout.layout);
for overlay in [
super::super::FLUID_OVERLAY,
super::super::TERRAIN_MODEL_SOLID_OVERLAY,
super::super::TERRAIN_MODEL_CUTOUT_OVERLAY,
super::super::TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY,
] {
let mut mixed = vec![
Vertex {
layer: 0,
..bytemuck::Zeroable::zeroed()
};
8
];
for v in &mut mixed[4..] {
v.layer = overlay | 279 << 16;
v.uv = [0.25, 0.75];
v.glint_uv = [17.0, 0.0];
}
let mut rest = Vec::new();
for hidden in [false, true] {
geometry.prepare(
&device,
&queue,
&layout.layout,
&mixed,
&[EntitySpan {
start: 0,
id: 77,
stage: render_stage("BLOCK_ENTITIES"),
hidden,
..Default::default()
}],
[true; PROGRAMS],
([0.; 4], [1; 2]),
Casters {
blocks: true,
..Default::default()
},
&mut rest,
None,
(&[], &[]),
);
assert_eq!(
geometry.scratch.len(),
4,
"the model still uses the shader pack"
);
assert_eq!(geometry.draws.len(), 1);
assert_eq!(
(geometry.draws[0].pack, geometry.draws[0].shadow),
(!hidden, true)
);
if hidden {
assert!(
rest.is_empty(),
"hidden native fluid must not leak into the visible pass"
);
} else {
assert_eq!(
bytemuck::cast_slice::<_, u8>(&rest),
bytemuck::cast_slice::<_, u8>(&mixed[4..]),
"fluid UV/layer payload cannot be sampled from the entity atlas"
);
}
}
}
}
#[test]
fn posed_draws_preserve_subject_ids_overlays_and_hidden_shadow_casters() {
let mut skin = SkinFrame {
@@ -1529,3 +1635,44 @@ mod velocity_translate_tests {
let _ = std::fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod tier_translate_tests {
/// A pack's entity and hand programs may read `horizonTier`, the overlay's HDR tier.
#[test]
fn a_pack_reading_horizon_tier_translates_for_entities_and_the_hand() {
let dir = std::env::temp_dir().join(format!("hz-entity-tier-{}", std::process::id()));
let shaders = dir.join("shaders");
std::fs::create_dir_all(&shaders).unwrap();
std::fs::write(
shaders.join("gbuffers_entities.vsh"),
"#version 120\nattribute float horizonTier;\nvarying float t;\n\
void main() {\n gl_Position = ftransform();\n t = horizonTier;\n}\n",
)
.unwrap();
std::fs::write(
shaders.join("gbuffers_entities.fsh"),
"#version 120\nvarying float t;\nvoid main() {\n gl_FragData[0] = vec4(t);\n}\n",
)
.unwrap();
let pack = horizon_shaders::Pack::open(&dir).unwrap();
let m = horizon_shaders::Macros::standard();
let vsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.vsh", &m);
let fsh = horizon_shaders::preprocess(&pack, "gbuffers_entities.fsh", &m);
for (who, inputs) in [
("entities", super::inputs()),
("hand", super::super::pack_hand::inputs()),
] {
let attribute = inputs.attributes.iter().find(|(n, _)| n == "horizonTier");
assert!(attribute.is_some(), "{who}");
horizon_shaders::translate_gbuffers(
&vsh,
&fsh,
horizon_shaders::Target::Buffers,
&inputs,
)
.unwrap_or_else(|e| panic!("{who}: {e:?}"));
}
let _ = std::fs::remove_dir_all(&dir);
}
}
@@ -23,6 +23,13 @@ pub(super) struct PackImage {
}
impl PackImage {
pub(super) fn reset_history(&mut self) {
self.view = None;
self.copy = None;
self.before = None;
self.made_for = [0; 2];
}
pub(super) fn sample_type(&self, features: wgpu::Features) -> wgpu::TextureSampleType {
self.format
.sample_type(None, Some(features))
@@ -75,6 +82,9 @@ pub(super) fn format(f: Format, packed_float: bool) -> wgpu::TextureFormat {
_ => rgba,
};
match (f.kind, f.bits) {
(Kind::Unorm, 10) => T::Rgb10a2Unorm,
(Kind::Uint, 10) => T::Rgb10a2Uint,
(Kind::Snorm, 16) => pick(T::R16Snorm, T::Rg16Snorm, T::Rgba16Snorm),
(Kind::Int, 8) => pick(T::R8Sint, T::Rg8Sint, T::Rgba8Sint),
(Kind::Int, 16) => pick(T::R16Sint, T::Rg16Sint, T::Rgba16Sint),
(Kind::Int, _) => pick(T::R32Sint, T::Rg32Sint, T::Rgba32Sint),
@@ -316,6 +326,11 @@ impl StandIn {
}
}
pub(super) fn reset_stand_ins(device: &wgpu::Device, images: &mut Vec<StandIn>) {
let wanted = images.iter().map(|i| (i.format, i.dimension)).collect();
*images = stand_ins(device, wanted);
}
/// One stand-in for each format and dimension of `wanted`.
pub(super) fn stand_ins(
device: &wgpu::Device,
@@ -392,6 +407,12 @@ pub(super) struct Buffers {
}
impl Buffers {
pub(super) fn reset_history(&mut self) {
self.group = None;
self.buffers.clear();
self.made_for = [0; 2];
}
/// The layout of `objects`, bound in `stages`; each buffer is at least its entry in
/// `minimums` long.
pub(super) fn new(
@@ -52,6 +52,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
alpha_test: None,
item_id_declared: false,
far_depth: false,
paged_maps: false,
}
}
@@ -149,6 +149,7 @@ void main() {
post,
wgpu::TextureFormat::Rgba8Unorm,
world,
&Default::default(),
)
.expect("the pack runs");
let mut pass = post
@@ -0,0 +1,207 @@
//! Paged maps (`paged_maps.rs`) through the translator's `hz_pagedTexture` and the pass's
//! binding: a pack reads the map's texels at the colour's coordinates.
use super::*;
use wgpu::util::DeviceExt;
/// The atlas the fixture's coordinates are in, and the one mapped texture's place.
const ATLAS: (u32, u32) = (64, 32);
const SPRITE: [u32; 4] = [20, 4, 20, 20];
/// The fixture's normal map texel at texel (x, y) of its texture.
fn texel(x: u32, y: u32) -> [u8; 4] {
[x as u8 * 10, y as u8 * 10, 7, 255]
}
#[test]
#[ignore = "requires a host GPU; missing adapter is a failure"]
fn a_pack_reads_paged_maps_at_the_colours_coordinates() {
let instance = crate::gfx::test_gpu::instance();
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
}))
.unwrap();
let root = std::env::temp_dir().join(format!("horizon-paged-pack-{}", std::process::id()));
std::fs::create_dir_all(root.join("shaders")).unwrap();
std::fs::write(
root.join("shaders/gbuffers_terrain.vsh"),
"#version 120\nvoid main(){gl_Position=gl_Vertex;}",
)
.unwrap();
// Each pixel reads one atlas texel's centre: in the texture (on four pages), beside it,
// on a page without maps, and the specular map, which no texture has.
std::fs::write(
root.join("shaders/gbuffers_terrain.fsh"),
r#"#version 120
uniform sampler2D normals;
uniform sampler2D specular;
vec2 at(float x, float y) { return (vec2(x, y) + 0.5) / vec2(64.0, 32.0); }
void main() {
int x = int(floor(gl_FragCoord.x));
if (x == 0) gl_FragData[0] = texture2D(normals, at(20.0, 4.0));
else if (x == 1) gl_FragData[0] = texture2D(normals, at(39.0, 23.0));
else if (x == 2) gl_FragData[0] = texture2D(normals, at(31.0, 15.0), 2.0);
else if (x == 3) gl_FragData[0] = texture2DLod(normals, at(32.0, 16.0), 3.0);
else if (x == 4) gl_FragData[0] = texture2D(normals, at(19.0, 4.0));
else if (x == 5) gl_FragData[0] = texture2D(normals, at(50.0, 30.0));
else if (x == 6) gl_FragData[0] = texture2D(specular, at(25.0, 10.0));
else gl_FragData[0] = texture2D(normals, at(33.0, 5.0));
}
"#,
)
.unwrap();
let [x0, y0, w, h] = SPRITE;
let normal: Vec<u8> = (0..w * h).flat_map(|i| texel(i % w, i / w)).collect();
let [normals, specular] = crate::paged_maps::build(
ATLAS,
&[crate::paged_maps::SpriteMaps {
rect: SPRITE,
maps: [Some(normal), None],
}],
16384,
);
assert!(specular.is_none());
let normals = normals.unwrap();
let map = device
.create_texture_with_data(
&queue,
&wgpu::TextureDescriptor {
label: Some("Fixture paged normal map"),
size: wgpu::Extent3d {
width: normals.width,
height: normals.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&normals.pixels,
)
.create_view(&Default::default());
let pack = horizon_shaders::Pack::open(&root).unwrap();
let macros = horizon_shaders::Macros::standard();
let properties = horizon_shaders::Properties::parse("alphaTest.gbuffers_terrain=off", &macros);
let inputs = horizon_shaders::GameInputs {
terrain: Some(horizon_shaders::GbuffersInputs {
body: "vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); hz_Vertex=vec4(p*2.0-1.0,0.0,1.0);".into(),
paged_maps: true,
..Default::default()
}),
..Default::default()
};
let post = horizon_shaders::load_post(&pack, "", &properties, &macros, &inputs);
assert!(post.terrain.is_some(), "{:?}", post.notes);
let world = World {
sprite_rects: &[],
atlas: None,
material_maps: None,
lightmap: map.clone(),
terrain: Some(Geometry {
buffers: &[],
table: None,
depth_write: true,
atlas: None,
material_maps: false,
ids: None,
cull: false,
}),
block_ids: &[],
celestial: None,
gui_size: [1, 1],
clouds: None,
weather: false,
hand: false,
};
let mut post = PackPost::new(
&device,
&queue,
&adapter,
post,
wgpu::TextureFormat::Rgba8Unorm,
world,
&Default::default(),
)
.expect("the pack runs");
let mut pass = post
.terrain
.take()
.unwrap_or_else(|| panic!("terrain pipeline must compile: {:?}", post.notes));
// As the entity passes are (`paged`), with the maps the renderer hands over.
pass.paged = true;
post.set_paged_maps([Some(map), None]);
post.copied |= 1;
post.resize(&device, [8, 1]);
let read = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: 256,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&Default::default());
{
let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Initialize fixture depth"),
color_attachments: &[],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &post.targets.as_ref().unwrap().depth,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
}
let group = post.bind(&device, &queue, &mut encoder, &mut pass, [8, 1]);
{
let mut render = post.begin(&mut encoder, &pass);
render.set_pipeline(pass.render());
render.set_bind_group(0, &group, &[]);
if pass.draw {
post.draws.bind(&mut render, "TERRAIN_SOLID");
}
render.draw(0..3, 0..1);
}
let output = post.targets.as_ref().unwrap().color[0].as_ref().unwrap()[0].texture();
encoder.copy_texture_to_buffer(
output.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &read,
layout: wgpu::TexelCopyBufferLayout {
bytes_per_row: Some(256),
..Default::default()
},
},
wgpu::Extent3d {
width: 8,
height: 1,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
let data = read.slice(..).get_mapped_range().unwrap();
let local = |x: u32, y: u32| texel(x - x0, y - y0);
let expected = [
local(20, 4),
local(39, 23),
local(31, 15),
local(32, 16),
crate::sprites::FLAT_NORMAL,
crate::sprites::FLAT_NORMAL,
crate::sprites::NO_SPECULAR,
local(33, 5),
];
for (i, e) in expected.iter().enumerate() {
assert_eq!(&data[i * 4..i * 4 + 4], e, "pixel {i}");
}
std::fs::remove_dir_all(root).unwrap();
}
+532 -92
View File
@@ -31,6 +31,9 @@ const BUFFERS: usize = horizon_shaders::BUFFERS;
mod packed;
#[cfg(test)]
mod history_reset_tests;
#[cfg(test)]
mod world_target_tests;
@@ -58,6 +61,9 @@ mod shadow_faces_tests;
#[cfg(test)]
mod r11f_image_tests;
#[cfg(test)]
mod work_group_fold_tests;
#[cfg(test)]
mod forced_flip_tests;
@@ -73,6 +79,9 @@ mod custom_type_tests;
#[cfg(test)]
mod sky_depth_tests;
#[cfg(test)]
mod end_fan_tests;
#[cfg(test)]
mod runtime_length_tests;
@@ -375,12 +384,13 @@ impl Gpu {
}
}
/// A pass's pipeline: a draw, or a compute dispatch.
/// A pass's pipeline: a draw, or a compute dispatch. Its slot is filled when the pack's
/// pipelines are compiled (`compile`), all of them at once once the pack is laid out.
#[derive(Clone)]
enum Pipeline {
Render(wgpu::RenderPipeline),
Render(Slot<wgpu::RenderPipeline>),
Compute {
pipeline: wgpu::ComputePipeline,
pipeline: Slot<wgpu::ComputePipeline>,
/// `workGroups`, else `workGroupsRender` and the local size.
groups: Option<[u32; 3]>,
render: [f32; 2],
@@ -395,10 +405,12 @@ struct Pass {
pipeline: Pipeline,
/// The same program as vanilla blends the sky's parts (`PostProgram::vanilla_sky_blends`):
/// without blending for the disc and the void, then with `STARS_BLEND` for the stars.
sky_parts: Option<[wgpu::RenderPipeline; 2]>,
sky_parts: Option<[Slot<wgpu::RenderPipeline>; 2]>,
layout: wgpu::BindGroupLayout,
ubo: wgpu::Buffer,
block: Vec<u8>,
/// Shader-declared defaults must survive a reset of the mutable upload block.
initial_block: Vec<u8>,
/// (index into `pack_uniforms::NAMES`, declared type, offset).
uniforms: Vec<(Option<usize>, String, u32)>,
samplers: Vec<Binding>,
@@ -421,6 +433,9 @@ struct Pass {
/// What `texture` reads instead of the block atlas.
atlas: Option<wgpu::TextureView>,
material_maps: bool,
/// Draws the shared atlas's geometry: `normals` and `specular` are its paged maps
/// (`PackPost::set_paged_maps`), as its programs were translated to read them.
paged: bool,
/// Reads `mc_Entity`'s table (`PackPost::block_ids`).
ids: bool,
/// Bind groups by the sides read (bits 0–15), the buffers whose mip chain is current (bits
@@ -452,9 +467,19 @@ struct Pass {
}
impl Pass {
/// Back to the uniform values the pass was made with, and no bind groups.
fn reset_history(&mut self) {
self.block.clone_from(&self.initial_block);
self.groups.clear();
self.portal_groups.clear();
for compute in &mut self.compute {
compute.reset_history();
}
}
fn render(&self) -> &wgpu::RenderPipeline {
match &self.pipeline {
Pipeline::Render(p) => p,
Pipeline::Render(p) => p.get().expect("compiled with its pack"),
Pipeline::Compute { .. } => unreachable!("a compute pass draws nothing"),
}
}
@@ -497,6 +522,7 @@ struct Targets {
}
/// What a pack's gbuffers programs read and draw from the game's world.
#[derive(Clone)]
pub(super) struct World<'a> {
/// The block atlas and its size.
pub atlas: Option<(wgpu::TextureView, [u32; 2])>,
@@ -654,6 +680,8 @@ pub struct PackPost {
custom: horizon_shaders::Custom,
atlas: Option<wgpu::TextureView>,
material_maps: Option<[wgpu::TextureView; 2]>,
/// The shared atlas's normal and specular paged maps (`set_paged_maps`).
paged_maps: [Option<wgpu::TextureView>; 2],
lightmap: wgpu::TextureView,
deferred_pre: u16,
composite_pre: u16,
@@ -1225,6 +1253,7 @@ fn block_ids(
/// The game's geometry a gbuffers program draws: its vertex buffers and, for flat draws
/// (flat.rs), the layout of their bind group 1.
#[derive(Clone)]
pub(super) struct Geometry<'a> {
pub buffers: &'a [Option<wgpu::VertexBufferLayout<'a>>],
pub table: Option<&'a wgpu::BindGroupLayout>,
@@ -1268,6 +1297,177 @@ struct Resources<'a> {
/// Bit n: shadowtex n read through a plain sampler filters linearly (`shadowtexNNearest`
/// unset): the translator's helpers blend a gather of it (`hz_depth_linear`).
shadow_linear: u8,
/// While the pack is laid out for `compile`: the pipelines its programs need. `None`
/// compiles each as its program is built.
jobs: Option<std::cell::RefCell<Vec<Job>>>,
}
impl Resources<'_> {
/// The slot of the render pipeline `job` makes: filled now, or by `compile` when the pack
/// is laid out.
fn render_pipeline(&self, device: &wgpu::Device, job: RenderJob) -> Slot<wgpu::RenderPipeline> {
let slot = Slot::default();
let job = Job::Render(slot.clone(), job);
match &self.jobs {
Some(jobs) => jobs.borrow_mut().push(job),
None => job.create(device),
}
slot
}
/// As `render_pipeline`, for a compute pipeline.
fn compute_pipeline(
&self,
device: &wgpu::Device,
job: ComputeJob,
) -> Slot<wgpu::ComputePipeline> {
let slot = Slot::default();
let job = Job::Compute(slot.clone(), job);
match &self.jobs {
Some(jobs) => jobs.borrow_mut().push(job),
None => job.create(device),
}
slot
}
}
/// A pipeline, made once: `Pipeline`'s, filled by its `Job`.
type Slot<T> = std::sync::Arc<std::sync::OnceLock<T>>;
/// What a render pipeline of a pack's program is made of, kept to make it later.
#[derive(Clone)]
struct RenderJob {
label: String,
layout: wgpu::PipelineLayout,
vs: wgpu::ShaderModule,
fs: wgpu::ShaderModule,
/// The vertex buffers: stride, step mode and attributes.
buffers: Vec<Option<(u64, wgpu::VertexStepMode, Vec<wgpu::VertexAttribute>)>>,
primitive: wgpu::PrimitiveState,
depth_stencil: Option<wgpu::DepthStencilState>,
targets: Vec<Option<wgpu::ColorTargetState>>,
}
/// What a compute pipeline of a pack's program is made of.
struct ComputeJob {
label: String,
layout: wgpu::PipelineLayout,
module: wgpu::ShaderModule,
}
/// A pipeline to make, and the slot it goes in.
enum Job {
Render(Slot<wgpu::RenderPipeline>, RenderJob),
Compute(Slot<wgpu::ComputePipeline>, ComputeJob),
}
impl Job {
/// Makes the pipeline (the driver compiles its shaders here) into its slot.
fn create(&self, device: &wgpu::Device) {
match self {
Job::Render(slot, j) => {
let buffers: Vec<Option<wgpu::VertexBufferLayout>> = j
.buffers
.iter()
.map(|b| {
b.as_ref()
.map(|(stride, step, attributes)| wgpu::VertexBufferLayout {
array_stride: *stride,
step_mode: *step,
attributes,
})
})
.collect();
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&j.label),
layout: Some(&j.layout),
vertex: wgpu::VertexState {
module: &j.vs,
entry_point: Some("main"),
compilation_options: Default::default(),
buffers: &buffers,
},
primitive: j.primitive,
depth_stencil: j.depth_stencil.clone(),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &j.fs,
entry_point: Some("main"),
compilation_options: Default::default(),
targets: &j.targets,
}),
multiview_mask: None,
cache: None,
});
let _ = slot.set(pipeline);
}
Job::Compute(slot, j) => {
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&j.label),
layout: Some(&j.layout),
module: &j.module,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let _ = slot.set(pipeline);
}
}
}
fn label(&self) -> &str {
match self {
Job::Render(_, j) => &j.label,
Job::Compute(_, j) => &j.label,
}
}
}
/// Makes the pipelines of `jobs` on as many threads as the machine has cores (up to 8): the
/// driver compiles a big pack's programs for tens of seconds one at a time, on one core. The
/// first one wgpu or the driver refuses, with why; `None` when all are made.
///
/// A refusal doesn't stop the others: every job is made, so that the serial rebuild `new` then
/// runs finds the driver's shader cache warm instead of compiling the rest cold, one at a time.
/// `cancel` set stops handing out jobs: the caller then drops the pack (`PackPost::new`).
fn compile(
device: &wgpu::Device,
jobs: &[Job],
cancel: &std::sync::atomic::AtomicBool,
) -> Option<(String, String)> {
use std::sync::atomic::{AtomicUsize, Ordering};
let threads = std::thread::available_parallelism()
.map_or(1, |n| n.get())
.clamp(1, 8)
.min(jobs.len().max(1));
let next = AtomicUsize::new(0);
let refused = std::sync::Mutex::new(None);
std::thread::scope(|scope| {
for _ in 0..threads {
scope.spawn(|| {
while !cancel.load(Ordering::Relaxed)
&& let Some(job) = jobs.get(next.fetch_add(1, Ordering::Relaxed))
{
if let Err(e) = checked(device, || job.create(device)) {
let mut refused = refused.lock().unwrap_or_else(|e| e.into_inner());
refused.get_or_insert((job.label().to_owned(), e));
}
}
});
}
});
refused.into_inner().unwrap_or_else(|e| e.into_inner())
}
/// Vertex buffer layouts owned, for a `RenderJob`.
fn owned_buffers(
buffers: &[Option<wgpu::VertexBufferLayout<'_>>],
) -> Vec<Option<(u64, wgpu::VertexStepMode, Vec<wgpu::VertexAttribute>)>> {
buffers
.iter()
.map(|b| {
b.as_ref()
.map(|b| (b.array_stride, b.step_mode, b.attributes.to_vec()))
})
.collect()
}
/// A program's bind group layout (its uniform block, samplers and images) and how each sampler
@@ -1876,60 +2076,47 @@ fn build_pass(
packed::quantise_outputs(&mut fragment, &packed_outputs);
packed::bitcast_outputs(&mut fragment, &int_outputs);
let (vs, fs) = (module(program.vertex), module(fragment));
let descriptor = |targets: &[Option<wgpu::ColorTargetState>]| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(&p.name),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &vs,
entry_point: Some("main"),
compilation_options: Default::default(),
// A geometry stage reads its vertices itself.
buffers: geometry
.filter(|_| p.geometry_triangles == 0)
.map_or(&[], |g| g.buffers),
let job = RenderJob {
label: p.name.clone(),
layout: pipeline_layout,
vs,
fs,
// A geometry stage reads its vertices itself.
buffers: geometry
.filter(|_| p.geometry_triangles == 0)
.map_or_else(Vec::new, |g| owned_buffers(g.buffers)),
// Rows flipped to GL's turn front faces clockwise.
primitive: wgpu::PrimitiveState {
front_face: wgpu::FrontFace::Cw,
cull_mode: geometry
.filter(|g| g.cull && p.geometry_triangles == 0)
.map(|_| wgpu::Face::Back),
..Default::default()
},
depth_stencil: geometry.map(|g| wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(g.depth_write),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: Default::default(),
bias: if p.name == "gbuffers_damagedblock" {
wgpu::DepthBiasState {
constant: -10,
slope_scale: -1.0,
clamp: 0.0,
}
} else {
Default::default()
},
// Rows flipped to GL's turn front faces clockwise.
primitive: wgpu::PrimitiveState {
front_face: wgpu::FrontFace::Cw,
cull_mode: geometry
.filter(|g| g.cull && p.geometry_triangles == 0)
.map(|_| wgpu::Face::Back),
..Default::default()
},
depth_stencil: geometry.map(|g| wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(g.depth_write),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: Default::default(),
bias: if p.name == "gbuffers_damagedblock" {
wgpu::DepthBiasState {
constant: -10,
slope_scale: -1.0,
clamp: 0.0,
}
} else {
Default::default()
},
}),
multisample: Default::default(),
fragment: Some(wgpu::FragmentState {
module: &fs,
entry_point: Some("main"),
compilation_options: Default::default(),
targets,
}),
multiview_mask: None,
cache: None,
})
}),
targets: targets.to_vec(),
};
let pipeline = descriptor(&targets);
// The program's own blend replaced by vanilla's for the part; a buffer's own
// `blend.<program>.<buffer>` still applies (Iris 1.11.7 blended the sky disc into that
// buffer only, with a probe pack of ours).
// buffer only, with a probe pack of ours). Two more pipelines of the program, made
// with the others (`compile`).
let sky_parts = p.vanilla_sky_blends.then(|| {
[None, Some(blend_state(horizon_shaders::STARS_BLEND))].map(|part| {
let targets: Vec<_> = (targets.iter().enumerate())
let targets: Vec<_> = (job.targets.iter().enumerate())
.map(|(l, t)| {
let (own, blendable) = own_blends.get(l).copied().unwrap_or((None, false));
t.clone().map(|t| wgpu::ColorTargetState {
@@ -1938,10 +2125,16 @@ fn build_pass(
})
})
.collect();
descriptor(&targets)
pack.render_pipeline(
device,
RenderJob {
targets,
..job.clone()
},
)
})
});
(layout, pipeline, sky_parts)
(layout, pack.render_pipeline(device, job), sky_parts)
});
let (layout, pipeline, sky_parts) = match created {
Ok(created) => created,
@@ -1973,6 +2166,7 @@ fn build_pass(
mapped_at_creation: false,
}),
block: interface.initial_values.clone(),
initial_block: interface.initial_values.clone(),
uniforms: interface
.uniforms
.iter()
@@ -1993,6 +2187,7 @@ fn build_pass(
draw,
atlas: geometry.and_then(|g| g.atlas.clone()),
material_maps: geometry.is_some_and(|g| g.material_maps),
paged: false,
ids: geometry.is_some_and(|g| g.ids.is_some()),
groups: HashMap::new(),
portal_groups: HashMap::new(),
@@ -2080,15 +2275,12 @@ fn build_compute(
label: Some(&name),
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(&name),
layout: Some(&pipeline_layout),
module: &module,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
(layout, pipeline)
let job = ComputeJob {
label: name.clone(),
layout: pipeline_layout,
module,
};
(layout, pack.compute_pipeline(device, job))
});
let (layout, pipeline) = match created {
Ok(created) => created,
@@ -2108,6 +2300,7 @@ fn build_compute(
mapped_at_creation: false,
}),
block: interface.initial_values.clone(),
initial_block: interface.initial_values.clone(),
uniforms: interface
.uniforms
.iter()
@@ -2135,6 +2328,7 @@ fn build_compute(
draw: false,
atlas: None,
material_maps: false,
paged: false,
ids: false,
groups: HashMap::new(),
portal_groups: HashMap::new(),
@@ -2156,17 +2350,80 @@ pub struct Refused {
pub notes: Vec<String>,
}
impl Refused {
/// The reason of a build stopped by its `cancel` flag (`PackPost::new`).
const CANCELLED: &str = "cancelled";
fn cancelled(notes: Vec<String>) -> Self {
Self {
reason: Self::CANCELLED.into(),
notes,
}
}
/// Whether the build was cancelled (another pack or dimension asked for): not the pack's
/// fault, so nothing to report to the player.
pub fn is_cancelled(&self) -> bool {
self.reason == Self::CANCELLED
}
}
impl PackPost {
/// The shared atlas's normal and specular paged maps (`paged_maps.rs`), which the programs
/// drawing its geometry read as `normals` and `specular`; set before the first frame (a pass's
/// bind groups are kept once made). A missing one reads as the dummy (flat, no specular).
pub fn set_paged_maps(&mut self, maps: [Option<wgpu::TextureView>; 2]) {
self.paged_maps = maps;
}
/// Builds the pipelines of `post` for a screen of `screen` format. An error when the pack
/// can't show anything on the screen: the game turns it off, as a pack that fails to load.
///
/// The pack is laid out first and its pipelines are made after, all at once (`compile`).
/// When wgpu or the driver refuses one, the pack is built again a program at a time, so that
/// the program is skipped with its note as it always was.
///
/// `cancel` set (the pack isn't wanted any more) stops the build between programs: a refusal
/// whose `is_cancelled` is true.
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
adapter: &wgpu::Adapter,
post: Post,
screen: wgpu::TextureFormat,
world: World<'_>,
cancel: &std::sync::atomic::AtomicBool,
) -> Result<Self, Refused> {
use std::sync::atomic::Ordering;
let spare = (post.clone(), world.clone());
let (built, jobs) = Self::build(device, queue, adapter, post, screen, world, true, cancel)?;
let refused = compile(device, &jobs, cancel);
// Some pipelines may be left unmade: the pack can't draw.
if cancel.load(Ordering::Relaxed) {
return Err(Refused::cancelled(built.notes));
}
let Some((program, refused)) = refused else {
return Ok(built);
};
log::warn!(
"shader pack: {program} was refused ({refused}): building it again a program at a time"
);
drop((built, jobs));
let (post, world) = spare;
Self::build(device, queue, adapter, post, screen, world, false, cancel)
.map(|(built, _)| built)
}
/// `new`'s work: with `defer`, the pipelines are left to make (the jobs returned).
#[allow(clippy::too_many_arguments)]
fn build(
device: &wgpu::Device,
queue: &wgpu::Queue,
adapter: &wgpu::Adapter,
mut post: Post,
screen: wgpu::TextureFormat,
world: World<'_>,
) -> Result<Self, Refused> {
defer: bool,
cancel: &std::sync::atomic::AtomicBool,
) -> Result<(Self, Vec<Job>), Refused> {
// What the pack's shaders need of its storage buffers, before its parts move out.
let minimums = post.buffer_minimums();
let features = device.features();
@@ -2207,6 +2464,12 @@ impl PackPost {
let textures = pack_textures::upload(device, queue, post.textures, &mut notes);
let shadow_config = post.config.shadow.clone();
let noise_size = post.config.noise_resolution.unwrap_or(256);
let half_lives = post.config.half_lives;
for (name, ticks) in horizon_shaders::HALF_LIVES.iter().zip(half_lives) {
if let Some(t) = ticks {
notes.push(format!("`{name}` is {t} ticks"));
}
}
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
let shadow_colors = (1u16 << post.shadow_colors) - 1;
let mut custom = post.custom;
@@ -2318,8 +2581,13 @@ impl PackPost {
shadow_linear: (0..2)
.filter(|&n| !shadow_config.nearest[n])
.fold(0, |m, n| m | 1 << n),
jobs: defer.then(Default::default),
};
let mut build = |mut p: horizon_shaders::PostProgram, geometry: Option<&Geometry<'_>>| {
// Not wanted any more: the rest is skipped and the build refused below.
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let in_shadow = p.group.shadow();
let mut compute = std::mem::take(&mut p.compute);
if !gpu.compute {
@@ -2535,6 +2803,13 @@ impl PackPost {
ids: None,
cull: false,
});
// Programs drawing the shared atlas's geometry read its paged maps (`pack_entities::inputs`).
let paged = |pass: Option<Pass>| {
pass.map(|mut pass| {
pass.paged = true;
pass
})
};
let entity_passes = programs.map(|p| {
p.zip(entity_geometry.as_ref()).and_then(|(p, g)| {
if p.name == "gbuffers_damagedblock" {
@@ -2547,15 +2822,15 @@ impl PackPost {
ids: g.ids,
cull: false,
};
build(p, Some(&decal))
paged(build(p, Some(&decal)))
} else {
build(p, Some(g))
paged(build(p, Some(g)))
}
})
});
let shadow_entities = shadow_programs.map(|p| {
p.zip(entity_geometry.as_ref())
.and_then(|(p, g)| build(p, Some(g)))
.and_then(|(p, g)| paged(build(p, Some(g))))
});
let entities = entity_vertices
.filter(|_| (entity_passes.iter().chain(&shadow_entities)).any(Option::is_some))
@@ -2598,7 +2873,7 @@ impl PackPost {
ids: None,
cull: false,
};
build(p, Some(&g)).map(|pass| (pass, geometry))
paged(build(p, Some(&g))).map(|pass| (pass, geometry))
});
// Translucent held items, drawn from the hand's own buffer.
let hand_water = post.hand_water.and_then(|p| {
@@ -2612,7 +2887,7 @@ impl PackPost {
ids: None,
cull: false,
};
build(p, Some(&g))
paged(build(p, Some(&g)))
});
let weather = post
.weather
@@ -2668,6 +2943,9 @@ impl PackPost {
.filter(|p| !skip.split(',').any(|s| s == p.name))
.filter_map(|p| build(p, None))
.collect();
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
return Err(Refused::cancelled(notes));
}
let stand_ins = pack_images::stand_ins(device, stand_ins.take());
// Terrain in two parts draws both or neither: one alone would leave the other part's
// terrain undrawn. Without either, the game draws all terrain, as when the terrain
@@ -3001,7 +3279,10 @@ impl PackPost {
let white = [255u8; 4];
let black = [0u8; 4];
let flat_normal = [128u8, 128, 255, 255];
Ok(Self {
let jobs = (pack.jobs.as_ref())
.map(std::cell::RefCell::take)
.unwrap_or_default();
let built = Self {
notes,
setup_pending: true,
passes,
@@ -3045,6 +3326,7 @@ impl PackPost {
custom,
atlas: world.atlas.as_ref().map(|a| a.0.clone()),
material_maps: world.material_maps,
paged_maps: [None, None],
lightmap: world.lightmap,
deferred_pre: mask(&post.deferred_pre),
composite_pre: mask(&post.composite_pre),
@@ -3124,23 +3406,7 @@ impl PackPost {
.create_view(&Default::default()),
textures,
shadow: (
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("Shader pack shadow stand-in"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
.create_view(&Default::default()),
super::pack_shadow::stand_in(device),
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Shader pack shadow"),
compare: Some(wgpu::CompareFunction::LessEqual),
@@ -3183,10 +3449,80 @@ impl PackPost {
let mut u = FrameUniforms::default();
u.atlas = world.atlas.map_or([0; 2], |a| a.1);
u.shadow = shadow_config;
u.half_lives = half_lives;
u
},
shadow_cache: Default::default(),
})
};
Ok((built, jobs))
}
/// `Renderer::finish_sequence_frame`: the centre depth the finished frame copied.
pub fn take_center_depth(&mut self, device: &wgpu::Device) -> Result<(), String> {
self.center_depth
.as_mut()
.ok_or("the shader pack copied no centre depth")?
.take_completed(device)
.map(|_| ())
}
/// Forgets what the pack kept from earlier frames, so the next frame draws as a freshly
/// loaded pack's first: buffers, images, storage buffers and shadow maps start over (made
/// again at their sizes when next drawn), the frame uniforms' history, the custom uniforms
/// and the centre depth too. Compiled programs and the pack's textures stay.
pub fn reset_history(&mut self, device: &wgpu::Device) {
self.uniforms.reset_history();
// A parsed pack's custom uniforms start at tick 0's seed, before any frame of the world.
self.custom.restart_at(0);
self.custom.forget_world();
self.center_depth = None;
self.targets = None;
self.main = 0;
self.mips_valid = 0;
self.rgb_unset = true;
self.final_target = None;
self.blit_group = None;
self.page_groups.clear();
for image in &mut self.images {
image.reset_history();
}
self.buffers.reset_history();
pack_images::reset_stand_ins(device, &mut self.stand_ins);
if let Some(shadow) = &mut self.shadow_targets {
shadow.reset_history(device, &self.uniforms.shadow);
}
self.shadow.0 = super::pack_shadow::stand_in(device);
self.shadow_cleared = false;
self.shadow_cache = Default::default();
self.setup_pending = true;
self.portal_texture = None;
self.portal_rect = [0.0; 4];
let sky = self
.sky
.iter_mut()
.flat_map(|s| s.basic.iter_mut().chain(&mut s.textured));
let entities = self
.entities
.iter_mut()
.flat_map(|e| e.passes.iter_mut().chain(&mut e.shadow).flatten());
for pass in self
.passes
.iter_mut()
.chain(&mut self.terrain)
.chain(&mut self.terrain_cutout)
.chain(&mut self.water)
.chain(sky)
.chain(entities)
.chain(&mut self.clouds)
.chain(&mut self.lines)
.chain(self.weather.iter_mut().flatten())
.chain(self.hand.iter_mut().map(|h| &mut h.0))
.chain(&mut self.shadow_terrain)
.chain(&mut self.shadow_water)
.chain(&mut self.shadow_comp)
{
pass.reset_history();
}
}
/// Writes `pass`'s uniforms for a target of `out` pixels; its bind group for the buffers'
@@ -3352,6 +3688,12 @@ impl PackPost {
Source::Dummy if s.dimension == wgpu::TextureViewDimension::D1 => {
&self.dummy_1d
}
Source::Normals if pass.paged => {
self.paged_maps[0].as_ref().unwrap_or(&self.dummy_normal)
}
Source::Specular if pass.paged => {
self.paged_maps[1].as_ref().unwrap_or(&self.dummy_black)
}
Source::Normals => self
.material_maps
.as_ref()
@@ -3446,11 +3788,22 @@ impl PackPost {
else {
return;
};
let pipeline = pipeline.get().expect("compiled with its pack");
let groups = groups.unwrap_or_else(|| {
let n = |k: usize| (size[k] as f32 * render[k] / local[k] as f32).ceil() as u32;
[n(0), n(1), 1]
});
let (pipeline, groups) = (pipeline.clone(), groups);
// wgpu panics on a dispatch over its limit. The translator reshapes a large
// `workGroups`; a `workGroupsRender` that large is not run.
let most = device.limits().max_compute_workgroups_per_dimension;
if groups.iter().any(|&n| n > most) {
log::warn!(
"shader pack: {}: {groups:?} work groups is over {most} per dimension; not run",
pass.name
);
return;
}
for image in (0..self.images.len()).filter(|k| pass.snapshots & 1 << k != 0) {
pack_images::snapshot(encoder, &self.images[image]);
}
@@ -3660,8 +4013,8 @@ impl PackPost {
pass.sky_parts
.as_ref()
.and_then(|[plain, stars]| match part.stage {
"SKY" | "VOID" => Some(plain),
"STARS" => Some(stars),
"SKY" | "VOID" => plain.get(),
"STARS" => stars.get(),
_ => None,
});
rp.set_pipeline(part_pipeline.unwrap_or(pass.render()));
@@ -3940,7 +4293,7 @@ impl PackPost {
frame: &super::Frame,
terrain: &mut TerrainDraw<'_>,
) {
if super::sky_visible(frame) {
if super::sky_visible(frame) || super::end_fog_sky(frame) {
self.draw_sky(device, queue, encoder, size, frame);
}
self.draw_terrain(device, queue, encoder, size, TerrainList::Solid, terrain);
@@ -5346,6 +5699,57 @@ mod tests {
assert_eq!(used, [1, 2, 3]);
}
/// A pack's pipelines are made together on several threads: each lands in its pass's slot,
/// and one wgpu refuses comes back by name (the pack is then built a program at a time).
#[test]
fn pipelines_compile_together_and_a_refused_one_is_named() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, _queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
let layout = device.create_pipeline_layout(&Default::default());
let module = |source: &str| {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(source.into()),
})
};
let job = |label: &str, module: wgpu::ShaderModule| {
let slot = Slot::default();
let job = ComputeJob {
label: label.into(),
layout: layout.clone(),
module,
};
(slot.clone(), Job::Compute(slot, job))
};
let plain = "@compute @workgroup_size(1) fn main() {}";
let go = std::sync::atomic::AtomicBool::new(false);
let (slots, jobs): (Vec<_>, Vec<_>) = (0..12)
.map(|i| job(&format!("composite{i}"), module(plain)))
.unzip();
// Cancelled before it starts: nothing is made (`new` then drops the pack).
assert_eq!(compile(&device, &jobs, &true.into()), None);
assert!(slots.iter().all(|s| s.get().is_none()));
assert_eq!(compile(&device, &jobs, &go), None);
assert!(slots.iter().all(|s| s.get().is_some()));
// A binding the layout doesn't have: wgpu refuses the pipeline.
let bound = "@group(0) @binding(0) var<storage, read_write> b: array<u32>;\n\
@compute @workgroup_size(1) fn main() { b[0] = 1u; }";
let (_, bad) = job("deferred3", module(bound));
let (slots, mut jobs): (Vec<_>, Vec<_>) =
(0..4).map(|i| job(&format!("c{i}"), module(plain))).unzip();
jobs.insert(2, bad);
let (program, _why) = compile(&device, &jobs, &go).expect("the pipeline is refused");
assert_eq!(program, "deferred3");
// The refusal stops none of the others: they are all made.
assert!(slots.iter().all(|s| s.get().is_some()));
}
/// #3525: naga's SPIR-V writer panicked on Photon's gbuffers programs as the pipeline was
/// made. Such a panic skips the pass, as a validation error does, and the device goes on.
#[test]
@@ -5435,6 +5839,7 @@ mod tests {
released: std::cell::Cell::new(("", 0)),
stand_ins: &stand_ins,
shadow_linear: 0,
jobs: None,
};
let reads = |group: Group| {
let (_, samplers, ..) = bindings(
@@ -5529,6 +5934,7 @@ mod tests {
released: std::cell::Cell::new(("", 0)),
stand_ins: &stand_ins,
shadow_linear: 0,
jobs: None,
packed: [None; BUFFERS],
};
let (_, samplers, ..) = bindings(
@@ -5612,6 +6018,7 @@ mod tests {
released: std::cell::Cell::new(("", 0)),
stand_ins: &stand_ins,
shadow_linear: 0,
jobs: None,
};
let mut notes = Vec::new();
let (.., images) = bindings(
@@ -5754,6 +6161,7 @@ mod tests {
released: std::cell::Cell::new(("", 0)),
stand_ins: &stand_ins,
shadow_linear: 0,
jobs: None,
};
let draws = DrawUniforms::new(&device);
let mut notes = Vec::new();
@@ -5896,6 +6304,34 @@ mod tests {
);
}
#[test]
fn an_image_is_made_in_its_layout_qualifiers_format() {
// `image.x = … RGB10_A2 …` read through `layout(rgb10_a2) image2D x`.
for q in [
"rgba32f",
"rgba16f",
"rg16f",
"r11f_g11f_b10f",
"r32f",
"rgba16",
"rgb10_a2",
"rgba8",
"rg8",
"r16",
"rgba16_snorm",
"rgba8_snorm",
"r8_snorm",
"rgba32i",
"r16i",
"rgb10_a2ui",
"rgba8ui",
"r32ui",
] {
let def = Format::parse(q).unwrap();
assert_eq!(Some(pack_images::format(def, true)), image_format(q), "{q}");
}
}
#[test]
fn samplers_read_custom_textures_of_their_type() {
use wgpu::TextureSampleType as T;
@@ -5941,6 +6377,10 @@ mod destination_tests;
#[path = "pack_material_tests.rs"]
mod material_tests;
#[cfg(test)]
#[path = "pack_paged_maps_tests.rs"]
mod paged_maps_tests;
#[cfg(test)]
mod rgb_alpha_tests;
@@ -99,6 +99,7 @@ fn the_atlas_blends_its_two_nearest_levels() {
weather: false,
hand: false,
},
&Default::default(),
)
.expect("the pack runs");
for p in gpu.render(&mut post, [4, 4]) {
Loaded 100 of 1600 files, more files were not shown because too many files have changed in this diff. Show more