Commit Graph

546 Commits

Author SHA1 Message Date
Ludovic Marie 4410c3ba44 Merge pull request #656 from LookImEcho-Rick/codex/fix-multiplayer-world-options
Fix multiplayer pause menu and World Options permissions
2026-10-17 08:39:44 +02:00
Ludovic Marie 9665a7c5b0 Merge pull request #609 from LookImEcho-Rick/fix/563-translucent-block-outline
fix(rendering): correct selection outline on translucent blocks
2026-10-17 08:01:13 +02:00
LookImEcho-Rick aeaccb0413 Fix multiplayer pause menu and World Options permissions 2026-10-16 23:39:36 -05:00
Ludovic Marie 1a68d8ddf0 Merge main into fix/furnace-on-server
lan_actions.rs: clear the old body's potions (main), then respawn with
the tick context (this branch). hub.rs: keep main's effect and chunk
tracking reset before this branch's vanilla respawn sequence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 05:35:16 +02:00
Ludovic Marie 2822d4e1b7 Content mods: refuse more block states than chunks hold; the pack against NeoForge, measured
Loading the eight carried-over mods in the game (not only the probe) showed
two limits real packs reach. Block textures past 2048 layers are another PR;
this one handles block states and measures a pack.

- Chunk sections store a block state in 16 bits. Vanilla 26.3 has 35,723
  states; Macaw's four mods add 36,154, the eight 41,389. Past 65,536 ids
  wrapped onto other blocks (a trapdoor became a hanging sign).
  mod_content::build now refuses a set of mods past MAX_STATES and names the
  states per mod; the game then starts without content mods, as for any
  refused content.
- tools/mod_port_harness.py probes each mod on its own against vanilla, as a
  player adding it would load it.
- tools/perf_bench.py --game neoforge: Minecraft 1.21.1 on NeoForge 21.1.259
  with --mod-jars and --deps, driven by a dev-only NeoForge port of the bench
  mod (mods/horizon/src/neoforge-bench); --port-work loads a work folder's
  carried-over mods into Horizon. tools/neoforge_oracle.py installs the
  client once under .local/neoforge-oracle (the installer, then the vanilla
  libraries it inherits, sha1-checked) and builds the driver. A NeoForge
  start that crashes before the driver loads (FML reading fml.toml while
  creating it) is retried once.

Measured (docs/modpacks.md): six ported mods in Horizon against the same
mods on NeoForge, Mac mini M4 Pro, 2 games per row on a loaded machine. The
world is ready 3.1x sooner at 8 and 16 chunks, the flights run 1.7x and
3.6x the frames, and Horizon's RSS is not lower (+1.1 GB with the mods).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:55:20 +02:00
Ludovic Marie e7f7236c34 Open furnaces, blast furnaces and smokers on a server
Right-clicking a furnace as a guest did nothing: the host only handed
chests, barrels, hoppers, dispensers and shulker boxes over. It now opens
the furnace's own menu (minecraft:furnace, blast_furnace or smoker),
runs the guest's clicks through the host's AbstractFurnaceMenu (fuel slot,
take-only result, shift-clicks by recipe and fuel, a new input restarts
cooking) and keeps the client's progress arrows in step with
ContainerSetData. The dedicated server harness now opens a furnace and
smelts an ore in it on both servers.

Fixes #540

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:17:54 +02:00
Ludovic Marie 66d187ed06 Respawn on a server in vanilla's order, so vanilla clients leave the loading screen
A vanilla 26.3 client that died on a Horizon server stayed on its loading
screen after pressing Respawn (#566): ClientPacketListener.handleRespawn
starts a LevelLoadTracker that waits for the level info's "level chunks
load start" game event and the chunk under the player, and Horizon's
respawn sent neither the level info nor the chunk cache centre.

The hub's respawn now follows PlayerList.respawn: the entities paired with
the dead player are removed (one packet each), its chunks are dropped
without ForgetLevelChunk (as PlayerChunkSender does for a dead player) and
sent again, the abilities, then Respawn (with the last death location),
the teleport to the spawn facing its direction, the default spawn,
difficulty, experience, post effects, the level info, the permission
level, the chunk cache centre and the inventory. A dying guest also gets
the death animation event and no health update while dead, as vanilla's
ServerPlayer.die does.

Guests' commands also ran at the host's permission level: an operator on
a dedicated server could not run operator commands (and LAN guests ran
the host's level). They now run at the guest's own level, as the command
tree they are sent says.

The dedicated server harness covers it: after trading, the vanilla-codec
client is made an operator by the console, runs /kill, respawns, and the
packets from the death screen to the respawned inventory are compared
with vanilla's, then the level must load where it respawned. The client
also stops sending a movement packet beside each teleport acceptance
during the respawn: the 26.3 client sends the acceptance alone, and
vanilla's server kicks a second position in the same tick.

Fixes #566

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 03:11:16 +02:00
LookImEcho-Rick 5d614f1cf6 fix(rendering): correct selection outline on translucent blocks 2026-10-16 19:03:48 -05:00
Ludovic Marie 32f5f01e22 Per-mod parity harness against a dev-only NeoForge oracle
The harness the modpack design calls for, for data-tier mods:

- tools/neoforge_oracle.py installs a NeoForge 21.1.259 server (ATM10's)
  under .local/neoforge-oracle, builds the exporter in
  mods/horizon/src/neoforge-oracle (our code, never shipped), runs it
  headless with the user's jars and writes every block (class chain,
  states, per-state hardness, light, tool need, map colour, sounds,
  outline and collision; drops for hand, diamond tools, shears and silk
  touch; placement; BlockSetType and WoodType), item, recipe and tag of
  the mods' namespaces, plus the vanilla blocks for templates. About 30 s.
- examples/mod_port_probe.rs builds the content root from the carried
  mods and dumps what Horizon's loaders make of the same things.
- tools/mod_port_harness.py compares them per mod and writes
  <work>/mods/<id>/harness/result.json. 1.21.1 sound events map to 26.3's
  through vanilla blocks both have, and vanilla items newer than 1.21.1
  are not counted against a recipe.

On ATM10's own jars (MIT): Macaw's Trapdoors passes everything; Doors,
Fences, Paths, Glassential, Factory Blocks and Cloud Glass pass every
drop, recipe, tag and placement and fail only on what API 0.2 cannot say
(shapes and sound events of their own, blocks of classes no vanilla block
shares). Jars, facts and results stay in the work folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:27:20 +02:00
Ludovic Marie bd42496af0 Ctrl + Pick Block copies the block entity's data in creative
ServerGamePacketListenerImpl.addBlockDataToItem: with Ctrl held in creative,
Pick Block gives the block's item the block entity's data. What each type
turns into item components (removeComponentsFromTag and
collectImplicitComponents: container, custom_name, lock, container_loot,
sign text, waxed, bees, banner patterns, pot decorations, skull profile)
leaves its NBT; the rest becomes minecraft:block_entity_data, tagged with
the type's id. Placing the item does the reverse
(BlockItem.updateCustomBlockEntityTag, op-only types only for a game
master, then applyComponentsFromItemStack), for every block entity type,
which also fills placed chests, barrels, hoppers and the like from their
container component, as vanilla does, where only shulker boxes were.

The client sends includeData on Ctrl in multiplayer, a picked banner keeps
its patterns without Ctrl (AbstractBannerBlock.getCloneItemStack), a picked
beehive carries its honey level, a placed command block with data keeps its
own auto and output tracking, and a placed waxed sign opens no editor.

tools/test_pick_block_data_parity.py compares both directions with vanilla's
own block entities: 148/148 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:59:56 +02:00
LookImEcho-Rick 924698e506 Support standing and walking on happy ghasts 2026-10-16 01:10:46 -05:00
LookImEcho-Rick 705fab9bed Fix happy ghast rider collisions and turning 2026-10-16 01:10:19 -05:00
Ludovic Marie 63453086b6 Trial spawners: become ominous, Trial Omen and ominous item spawners
A player seen with Bad Omen or Trial Omen turns a trial spawner ominous
(tryDetectPlayers' ominous branch): Bad Omen becomes Trial Omen, level
events 3020, the spawner's mobs vanish (3012) and the ominous config
starts over. While active, an ominous spawner drops an ominous item
spawner over a player or one of its mobs every 160 ticks
(spawnOminousOminousItemSpawner, getDispensingItems seeded per 30x20x30
cell); it floats, grows and turns for 60 to 120 ticks, then the item
falls or is fired down (OminousItemSpawner).

Zombies, piglins and zoglins read IsBaby from their tag, so the baby
zombie trial spawners spawn babies, and zombies save it.

The trial spawner harness covers the ominous branch, Trial Omen, the
discarded mobs and the item spawners' items, positions and delays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:04:30 +02:00
Ludovic Marie 2567a4a41a Add the vault's server logic with a parity harness
vault::Vault is VaultBlockEntity with its config, server and shared data:
load and save, Server.tick (display item cycling, VaultState.tickAndGetNext,
ejection, transitions with their sounds and level events) and
VaultBlock.useItemOn / tryInsertKey (keys, rewarded players, fail sounds,
the reward roll with its random sequence).

tools/test_vault_parity.py runs 3,000 cases against vanilla through a new
mode=vault of the natural spawn oracle and fixture.

Part of #223.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 05:03:28 +02:00
Ludovic Marie a061c40fde Move every matching stack on a Shift double click
AbstractContainerScreen.mouseReleased: a Shift double click QUICK_MOVEs
every slot of the clicked slot's container that holds the item the last
Shift click moved (lastQuickMoved), when canTakeItemForPickAll allows the
clicked slot. Menu::quick_move_all runs it on the menu engine;
test_menu_click_parity generates it as a QUICK_MOVE_ALL op that the oracle
runs with the real Slot.mayPickup, Slot.container and
AbstractContainerMenu.canItemQuickReplace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:44:18 +02:00
Ludovic Marie 96268647d3 Merge pull request #319 from ludovic111/feat/trial-spawners
Trial spawner state machine, checked against vanilla
2026-10-15 12:32:52 +02:00
Ludovic Marie fd3d456de8 Merge pull request #315 from ludovic111/feat/minecart-sounds
Minecarts roll and rattle when ridden, and hits on vehicles sound
2026-10-15 12:32:32 +02:00
Ludovic Marie 204e4fdccb Merge pull request #313 from ludovic111/fix/offhand-use
Use the off hand when the main hand passes: place blocks, eat, charge anchors
2026-10-15 12:32:20 +02:00
Ludovic Marie ca76075cc9 Trial spawner state machine, checked against vanilla
mobs::trial_spawner: TrialSpawner and its state data, the registry
configs (data/*/trial_spawner), the six states, player detection with the
visual line of sight, mob spawns and reward ejection, read and saved as
vanilla does (a field that fails to decode takes its default alone).

tools/test_trial_spawner_parity.py ticks 2,500 random trial spawners in
VanillaNaturalSpawnOracle and natural_spawn_fixture and compares states,
spawns, events and ejected items line by line.

Also: copper grates have no visual shape; a slime's or magma cube's
obstruction box scales with the Size its spawn data sets; LootData::roll_split
for an empty-context getRandomItems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:20:29 +02:00
Ludovic Marie 5e2935747b Use the off hand when the main hand passes: place blocks, eat, charge anchors
Minecraft.startUseItem tries each InteractionHand in turn: useItemOn, then
useItem, stopping at the first that succeeds or fails. Item uses now act on
the hand that started them (Game::hand), so blocks, food, potions, buckets,
spawn eggs and the rest work from the off hand, the swing and the
re-equip animation follow that hand, and the empty-hand block use
(useWithoutItem) stays main-hand only. The respawn anchor passes on the
main hand when the off hand holds glowstone, as vanilla does, replacing the
special case for it.

The use_item_on harness now puts items in the off hand too.

Part of #276

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:41:06 +02:00
Ludovic Marie e2473aafd8 Shields turn arrows back, as vanilla does
AbstractArrow.onHitEntity: an entity that takes no damage from the arrow
(a raised shield, invulnerability) turns it back with
ProjectileDeflection.REVERSE at power 0.2: a tenth of the speed, a half
turn give or take ten degrees from the arrow's own random; one left
almost still drops its pickup and is discarded. Arrows used to vanish.

The arrow tick now stops at the entity it hits: the game hurts the
entity, then arrows::finish_entity_hit spends the arrow (with its hit
sound, from the arrow's random) or turns it back, and ends the tick
(drag, gravity, fluids). The damage is taken from the speed at the hit,
the crit bonus from the arrow's random, the entity search follows
ProjectileUtil.getManyEntityHitResult (the box first, then
computeMargin's margin, which needs tickCount), Punch pushes only what
was hurt, and a refused burning arrow leaves the player's fire as it
was. Mobs' refusals keep the old ending until their hurt results follow
hurtServer.

The arrow harness gains an entity in the way that takes or refuses the
damage: squarely, grazing, slow enough to drop.

Part of #277

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:41:06 +02:00
Ludovic Marie adb19abd4c Minecarts roll, rattle when ridden, and take hits with the attack sounds
MinecartSoundInstance follows every minecart in the level (entity.minecart.riding,
louder with speed, silent when the tick rate is frozen), and getting in one starts
RidingMinecartSoundInstance's two inside sounds (normal and underwater). Hitting a
minecart or a boat plays Player.attack's strong/weak (and knockback) sounds.

The sound_engine harness now replays both instances against the vanilla client.

Fixes #227

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:29:15 +02:00
Ludovic Marie 9044c69429 Merge pull request #307 from ludovic111/feat/bee-hives
Bees live in hives and nests: they go home, come out in the morning and make honey (#208)
2026-10-15 11:18:21 +02:00
Ludovic Marie 68ec2236e2 Merge pull request #293 from ludovic111/fix/drowned-parity
mob_ai oracle: wait for the light engine before ticking (drowned flake, #290)
2026-10-15 11:13:48 +02:00
Ludovic Marie 111b237b6d Bees live in hives and nests: they go home, come out in the morning and make honey (#208)
BeehiveBlockEntity (crate::beehive): up to three bees stored as entity
tags, out when their time is up (not at night, in the rain or through a
blocked front unless in an emergency), honey for nectar, the flower they
knew; vanilla's bees / flower_pos save format. Bees find a hive with room
(BeeLocateHiveGoal), fly home and go in (BeeEnterHiveGoal through
MobEvent::EnterHive). In the game hives tick, breaking one lets the bees
out angry unless smoked, fire empties it, dispensers release them.

mob_ai: bee_hive_night, _rain, _morning and _honey match vanilla.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:14:21 +02:00
Ludovic Marie 5530d682ab Arrows interpolate from their last tick, not from where they were shot
arrows::tick never set the projectile's previous position (Entity.setOldPosAndRot
at the start of every tick); Projectile::tick does, but arrows don't go
through it. The renderer lerped from the spawn point to the current
position every frame, so a flying or stuck arrow jumped along its whole
path. The arrow harness now prints the old position too.

Fixes #278

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:03:39 +02:00
Ludovic Marie fa188fca42 mob_ai oracle: wait for the scene's light updates before ticking (#290)
The oracle started a scenario once hasLightWork() had been false for six
ticks, but light updates reach the light thread through the chunk task
dispatcher, where hasLightWork() can't see them. A drowned's first
sun-burn roll (Mob.isSunBurnTick draws only when the brightness at its
eyes is above 0.5) then read the dirt's light under a freshly filled
pool or the water's, depending on thread timing: one random draw more or
less, and drowned_pool / drowned_pool_swimmer failed at random.

Queue ThreadedLevelLightEngine.waitForPendingTasks on every forced chunk
after building the scene and tick only once they have all completed.

Fixes #290

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:34:05 +02:00
Ludovic Marie 7c2a31206c Bees fly, pollinate flowers, get angry and sting (#208)
A port of vanilla 26.3 Bee: FlyingMoveControl and flying navigation,
hover wandering, flower search and pollination (nectar), growing crops,
tempting and breeding, persistent anger with the hurt-by alert, the sting
(poison, stinger lost, later death), drowning, staying home at night and
in rain, roll, textures and NBT. Hives as homes are not simulated yet.

FlyingMoveControl clamps the pitch like Entity.setXRot, so a rotlerp
that wraps to 359 gives 90 as in vanilla.

Mob AI parity: 9 bee scenarios, 3609/3609 ticks; full harness 120/120.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:57:51 +02:00
Ludovic Marie 3df34df87b Merge pull request #284 from ludovic111/fix/bastion-chunks
Save every generated chunk and decorated proto chunks, as vanilla does (#249)
2026-10-15 06:48:44 +02:00
Ludovic Marie 1c3fd6e23c Save every generated chunk and the proto chunks decoration wrote to (#249)
Horizon saved a chunk only once a player changed it and never saved proto
chunks, so leaving an area dropped decorated chunks while the final chunks
next to them stayed loaded (or were regenerated later). Coming back, those
chunks were generated again and their decoration was skipped beside a final
neighbour: whole chunks of a bastion lost their pieces, other chunks their
features. Like vanilla, generated chunks are now saved, proto chunks that
decoration wrote to are saved with their status (terrain or features) and
restored onto the chunk generated again, columns that finish after the player
left are saved, and saving waits for the decorations in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:37:05 +02:00
Ludovic Marie b691b5ebaa Benchmark Horizon against vanilla 26.3 and vanilla with performance mods
tools/perf_bench.py runs Horizon, the vanilla 26.3 client and vanilla with
Sodium, Lithium, FerriteCore and ImmediatelyFast through the same steps:
fresh world (seed 1234, creative) per render distance, start to title, start
to in-world, then per scene teleport, chunk load, settle and a fixed 192-block
flight. Every frame time is kept and one function computes average FPS, 1% and
0.1% lows, p50/p95/p99/p99.9 and a frame-time histogram for every game; RSS is
sampled from outside the process for every game. One options.txt (vanilla's
keys, Fancy preset values with a Custom preset so the render distance holds)
is written for all of them.

Vanilla is driven by a dev-only Fabric mod (mods/horizon/src/bench) compiled
against the user's own client jar: it creates the world with
WorldOpenFlows.createFreshLevel, teleports with /tp and places the player on
the path every frame as horizon-ctl fly does. Nothing of the game is
redistributed and the performance mods are fetched from Modrinth with the
hashes pinned in packs/mods.lock.json.

Horizon side: `fly ... PITCH raw` also replies with every frame time, and
--offscreen drives frames from the event loop, so a window nobody sees (a
locked macOS session) still renders.

docs/benchmark.md describes what is measured, how fairness is kept, the
caveats and how to reproduce it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:40:12 +02:00
Ludovic Marie a2b2b3f776 Player water on #59: bubble columns, swim sounds, drown bubbles, vanilla fixes
Builds on Cosmic's #59 (player fluid travel, swimming, air supply) and
changes only what differs from vanilla 26.3 or what it lacks:

- BubbleColumnBlock.entityInside for the player (isPrecise, onAbove /
  onInsideBubbleColumn, not while flying) with its splash/bubble particles.
- LocalPlayer.goDownInWater before LivingEntity.aiStep's velocity cleanup.
- The aiStep jump phase as vanilla: nothing while flying, the fluid jump
  threshold from the eye height (also in travelInLava).
- Player.travel: swim steering samples y + 1.0 - 0.1, and LivingEntity.travel
  checks fluids before gliding; water and lava no longer end a glide.
- LocalPlayer.isUnderWater is wasUnderwater; sprint conditions get shallow
  water, flight and gliding; no flight toggle while swimming; the base tick
  runs before local crouching; the desired pose puts swimming before gliding.
- decreaseAirSupply creates the oxygen_bonus instance (getAttribute).
- Swim sounds and muffled steps in water; entity event 67 drown bubbles.
- Scene motion harness: swim_corpus (player travel in water and lava, bubble
  columns, air supply) and sprint-swim cases for --sprint-input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:45:13 +02:00
Ludovic Marie a5bd5ebf7a Merge pull request #201 from ludovic111/fix/cactus-mips-110
Sprite mips from vanilla's MipmapGenerator: no cactus corner gaps
2026-10-15 00:04:25 +02:00
Ludovic Marie b91288a937 Merge pull request #196 from ludovic111/fix/crosshair-97
Inverting crosshair and a working attack indicator
2026-10-15 00:02:02 +02:00
Ludovic Marie 95db0fce9d Sprite mips from MipmapGenerator: cutout edges keep their coverage
Horizon averaged each mip level's alpha plainly, so a cutout texture's
transparent edges grew past the alpha test with every level: the cactus
side's transparent border columns opened see-through slits along its
corners from about 40 blocks, more than vanilla shows. The sprite array's
mips now come from a port of MipmapGenerator.generateMipLevels: the
texture's .mcmeta mipmap_strategy (auto: cutout when a pixel is fully
transparent) and alpha_cutoff_bias, solidify or the dark fill on the base,
meanLinear or darkenedAlphaBlend, and scaleAlphaToCoverage keeping every
cutout level's alpha-test coverage at the base level's.

tools/test_mipmap_parity.py runs vanilla's MipmapGenerator and the port on
every static 16x16 block texture and a sample of item textures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:59:47 +02:00
Ludovic Marie 6ab958a423 Crosshair through the inverting CROSSHAIR pipeline, with the attack indicator
Hud.extractCrosshair draws every crosshair sprite with
RenderPipelines.CROSSHAIR, whose BlendFunction.INVERT (ONE_MINUS_DST_COLOR,
ONE_MINUS_SRC_COLOR, ONE, ZERO) inverts the colour under it; Horizon drew it
alpha-blended white. GuiDraw sprites now carry their pipeline, the HUD batch
records the vertex ranges that invert and the HUD renderer draws them with a
second pipeline.

The attack indicator (crosshair or hotbar, per the Attack Indicator option)
now shows Player.getAttackStrengthScale(0) from the attack strength ticker
instead of a constant full strength, and the full indicator shows at full
strength on a living entity under the crosshair with a weapon slower than
MIN_CROSSHAIR_ATTACK_SPEED.

The HUD harness records every sprite's pipeline on both sides and gives the
crosshair a stand-in crosshairPickEntity (alive or not) and attack delay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:32:36 +02:00
Ludovic Marie 89eaaab2ee Arrows can be picked up (#119)
Players could never pick up arrows: arrows had no pickup rule and no
item, nothing checked a player touching them, and arrows from saves were
loaded into a separate list that never ticked.

Arrows now carry AbstractArrow's pickup rule and pickupItemStack. The
player's bow and crossbow give them the drawn ammo (ProjectileWeaponItem
.useAmmo: creative, Infinity and multishot's extra arrows draw intangible
ammo, so those arrows are CREATIVE_ONLY), setOwner(player) turns
DISALLOWED into ALLOWED, dispensers make them ALLOWED, mobs' stay
DISALLOWED. Each tick the player touches the arrows in its box grown by
(1, 0.5, 1) (Player.aiStep): a stuck arrow that has stopped shaking goes
into the inventory (ALLOWED), or just goes for a creative player
(CREATIVE_ONLY, or ALLOWED with a full inventory), with the item pickup
sound at the arrow.

Saved arrows load as live arrows (life, shake, inGround, inBlockState,
pickup, item, fire) and live arrows are saved, so stuck arrows survive a
reload and despawn on time; the legacy never-ticked arrow list is gone,
and /kill, /tp and /data reach the live arrows.

The arrow harness adds who shot the arrow, intangible ammo and
dispensers' ALLOWED, then touches in flight, while shaking and when stuck
by survival and creative players with empty, partly and fully filled
inventories, against the real setOwner and playerTouch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:17:07 +02:00
Ludovic Marie 228bdda201 Burning arrows: lava, fire, Flame and what they light (#125, #124)
Arrows never caught fire: nothing ignited them (lava, fire blocks, a
Flame bow's projectile_spawned ignite), so they never set mobs, TNT,
campfires or candles alight. Flame only worked for mobs' bows, as a
5-second ignite on hit.

Arrows now keep Entity.remainingFireTicks: the blocks along their path go
through InsideBlockEffectApplier.StepBasedCollector (fire and soul fire
fireIgnite to 8 s, lava LAVA_IGNITE to 15 s, water EXTINGUISH with the
extinguish sound), the player's bow and mobs' bonus ignite them for 100 s
at spawn, and the fire burns down in flight but not while stuck. A
burning arrow sets its target alight for 5 s, primes TNT, lights
campfires, candles and candle cakes (onProjectileHit, mob_griefing for
mobs' arrows), and blows up a TNT minecart; arrows now hit minecarts like
other entities (VehicleEntity damage). Burning arrows show
FlameFeatureRenderer's fire quads.

The arrow harness adds lava pools, lava/water walls, fire and soul fire,
Flame arrows, and burning/non-burning hits on TNT, campfires, candles and
candle cakes against the real blocks' onProjectileHit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:44:37 +02:00
Ludovic Marie 1d93642094 Arrows get vanilla's water physics (#127)
Arrows flew through water with a point check for the 0.6 drag and nothing
else: no bubbles, no splash, no currents, no bubble columns, and the
stuck arrow logic (nudging back by the normalized motion, no falling
out, a 1200-tick age) was not AbstractArrow's.

Port AbstractArrow.tick for arrows (arrows.rs, state in
Projectile::arrow): the in-block check, the water inertia from the last
base tick's wasTouchingWater with four bubbles behind it, crit trails,
lerped rotation, the COLLIDER clip with entity hits first, the effects
of the blocks along the path (bubble columns push, water extinguishes),
air drag and gravity, then the base tick's updateFluidInteraction
(currents while not stuck, splash sound/particles), sticking 0.05 back
along each moving axis with the arrow's own random for the hit sound,
falling out when the block goes, and despawning 1200 ticks after
sticking.

New harness tools/test_arrow_parity.py drives real vanilla Arrow entities
(VanillaArrowOracle) and Horizon's arrows::tick (examples/arrow_probe.rs)
over the resident scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:44:31 +02:00
Ludovic Marie 60d9bffc86 Water cancels the player's fall damage (#180)
The player's movement treated every fluid as empty: is_in_water was
always false, LivingEntity.checkFallDamage's updateFluidInteraction was
a no-op and Entity.move's fall-reset ray ignored water. A player landing
in water (or a waterlogged block, a bubble column, kelp) still took the
whole fall.

Bind vanilla's Entity.updateFluidInteraction to the player over the
existing horizon-world kernels (EntityFluidInteraction heights, eyes and
currents, entity_fluid_tick, water_splash, fluid_flow): run it in the
base tick and from checkFallDamage, read wasTouchingWater for
isInWater, query the water's (first-query cached) shape in the
FALLDAMAGE_RESETTING ray, halve the fall in lava after the first tick,
and play the splash sound with bubble and splash particles.

The scene motion harness gains water/lava fixtures (fluid_tick plus
scripted falls through the real Entity.move) against the real vanilla
updateFluidInteraction with the #water/#lava tags bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:44:31 +02:00
Ludovic Marie 56aae0e902 Maces smash: fall damage bonus, Density, Breach, Wind Burst, knockback
A mace hit after falling more than 1.5 blocks is now a smash attack
(MaceItem): the fall-based damage bonus plus Density, mace_smash damage,
the stopped fall with its damage waived, the ground/air smash sounds, the
dust pillars (level event 2013) and the knockback of the creatures around
the target, then the fall reset and landing particles. Breach thins the
target's armor (CombatRules.getDamageAfterAbsorb), Wind Burst bursts at
the player's feet (ExplodeEffect, trigger), and weapons wear by their
weapon component and award the used statistic.

Vanilla details matched on the way: the hurt knockback is 0.4F and reads
the live knockback-resistance attribute, explosion exposure uses the
exact COLLIDER clip, a burst posts the explode game event and ends the
impulse grace unless it is a wind charge's, visible armor stands feel
bursts that affect block-like entities, attack sounds play at the player.

tools/test_mace_parity.py compares the hit with VanillaDispenserOracle's
real MaceItem / hurtServer / post-attack effects: 3000/3000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:23:57 +02:00
Ludovic Marie 190f89b214 Beds and straw beds blow up when used in the Nether and the End
A right click on a bed in the Nether or the End tried to sleep in it (and
set the respawn point) as in the Overworld.

AbstractBedBlock.useWithoutItem reads the dimension's bed rule, which in
the Nether and the End is destroy_on_use: BedBlock.destroyOnUse removes the
head, then the foot behind it if it is still the bed, and explodes at the
head (power 5, fire, BLOCK interaction, the bad_respawn_point damage source:
"Intentional Game Design"); StrawBedBlock.destroyBed plays
block.straw_bed.break_leave and removes it.

tools/test_use_item_on_parity.py clicks beds and straw beds (head or foot,
whole or missing their other half, among blocks) in the Nether and the End,
through vanilla's real explosion; the oracle gains the End.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:24:55 +02:00
Ludovic Marie 1596210bba Respawn anchors charge, set the respawn point, explode outside the Nether
A right click with glowstone on a respawn anchor did nothing and a charged
anchor never became the respawn point: the block reported the use and the
game dropped it.

RespawnAnchorBlock is now played out (game/respawn_anchor.rs): useItemOn
charges with glowstone (the block change, block.respawn_anchor.charge, the
used item and the charge_respawn_anchor criterion), passing to the off
hand's glowstone; useWithoutItem sets the respawn point in the Nether
(block.minecraft.set_spawn once, respawn_anchor.set_spawn) and elsewhere
explodes: the anchor goes, then a power 5 fiery BLOCK explosion with the
bad_respawn_point damage source whose centre resists like water when water
would flow into it. Dying with an anchor as the respawn point travels to
its dimension and respawns at findStandUpPosition (DismountHelper's safe and
unsafe passes, facing the anchor with calculateLookAtYaw), using a charge
and playing respawn_anchor.deplete; a missing or empty anchor falls back to
the world spawn with block.minecraft.spawn.not_valid. The point is saved in
the player's respawn compound with its dimension; a bed or /spawnpoint
replaces it. Dying in the Nether without one now respawns in the Overworld.

Explosions gained what these need: a damage type, a block resistance
override, BaseFireBlock.getState for their fire, the explosion radius in the
block loot context (survives_explosion and explosion_decay drawing from the
table's random sequence) and TntBlock.wasExploded without a game event.

tools/test_use_item_on_parity.py now charges, uses and blows up anchors in
both dimensions through vanilla's real ServerExplosion on the stand-in
level, and respawns at anchors among blocks through
ServerPlayer.findRespawnAndUseSpawnBlock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:18:49 +02:00
Ludovic Marie f83bc0ab70 Merge pull request #146 from ludovic111/fix/spawners-95
Monster spawners spawn, spin their mob and take spawn eggs
2026-10-14 19:54:54 +02:00
Ludovic Marie 79e04e22d6 Monster spawners spawn, spin their mob and take spawn eggs
Spawners were never ticked: Horizon kept their block entity as loaded NBT,
and a freshly generated dungeon or mineshaft spawner lost even that, so no
mob ever came out, nothing turned inside and a spawn egg used on one spawned
a mob next to it instead of setting the spawner's.

The spawner is now a modelled block entity (mobs::spawner): BaseSpawner's
load and save with SpawnData's codec (normalised ids, custom spawn rules,
weighted potentials, the next data shared with the one potential when the
potentials are missing), serverTick (the player range, spawner_blocks_work,
the delay, getOrCreateNextSpawnData, Pos, the spawn box against blocks and
solid shulkers, custom light rules and peaceful, SpawnPlacements, the nearby
cap, the yaw, checkSpawnRules and checkSpawnObstruction, finalizeSpawn for
an id alone, the 2004 burst, entity_place and the spawn poof) and delay with
its block event, in the level random's order. The client copy keeps its own
delay for the spin and puffs smoke and flames; SpawnerRenderer draws the
next mob small and turning. SpawnEggItem.useOn on a spawner sets its mob
(setEntityId), tells the player when spawner_blocks_work is off and fails in
peaceful for monsters. Breaking one pops 15 + nextInt(15) + nextInt(15)
experience whatever the tool. LAN guests get the update tag, the block event
and the entity event.

tools/test_spawner_parity.py ticks vanilla's SpawnerBlockEntity on the
natural spawn oracle's level against mobs::spawner on the same worlds (6,000
cases); tools/test_use_item_on_parity.py now covers spawn eggs on spawners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:33:25 +02:00
rdbtCVS 28b01e9eca Fix fall damage sounds in creative mode 2026-10-14 18:02:51 +01:00
Ludovic Marie 1f06156649 Nether portals: vanilla timing, linking, and items and mobs travel
The player's portal timer reset to zero on any tick outside a portal and
came back from a trip with a 300-tick cooldown that only ran down outside
portals; the overworld exit search covered 32 blocks of loaded chunks;
nothing but the player could use a portal.

The player now follows Entity.setAsInsidePortal / handlePortal with
PortalProcessor (decay by 4, the cooldown of 10 that a portal keeps
resetting, players_nether_portal_default/creative_delay), and
NetherPortalBlock.getPortalDestination through the new
horizon_world::level::portal_travel: PortalForcer.findClosestPortalPosition
(16 / 128 blocks, loaded and saved chunks), createPortal,
BlockUtil.getLargestRectangleAround, PortalShape.getRelativePosition and
findCollisionFreePosition, with the turned yaw and motion and
block.portal.travel. LocalPlayer.handlePortalTransitionEffect closes
screens and plays block.portal.trigger. Items and mobs go through after one
tick with a 300-tick cooldown and wait as saved entities until their
dimension loads around the exit.

tools/test_nether_portal_parity.py compares the destination with vanilla's
getPortalDestination over two headless levels; the scene motion harness
gains portal fixtures for the player's processor and cooldown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:06:24 +02:00
Ludovic Marie b2462d54c4 Merge pull request #116 from ludovic111/fix/world-spawn-seed
Find a new world's spawn as vanilla does (NoiseSpawnFinder, setInitialSpawn, PlayerSpawnFinder)
2026-10-14 18:04:54 +02:00
Ludovic Marie eccb222e9f Merge pull request #115 from ludovic111/fix/sweet-berries-93
Sweet berries can be planted; picking and bone meal work
2026-10-14 18:04:47 +02:00
Ludovic Marie 3f61995c8b test(entity_model): compare per-vertex normals with vanilla
The entity model parity harness now draws each posed model on both sides
under LivingEntityRenderer's root transform (body yaw, scale(-1, -1, 1),
the renderer's own scale) and compares every vertex's normal as the
signed bytes written to the vertex buffer: vanilla's ModelPart.render /
Cube.compile / PoseStack.Pose.transformNormal into a capturing
VertexConsumer, packed with BufferBuilder's own normalIntValue, against
Horizon's mobs::render::draw RGBA8_SNORM normals. A part-scale override
covers negative and non-uniform part scales (trusted sign flips and
untrusted 1/scale normalisation). A one-step difference is accepted only
where vanilla's float sits at a truncation step; a posed position
checksum per drawn part is compared too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:52:01 +02:00