- The command harness (HORIZON_COMMAND_SCRIPT) drives the world itself,
without the event loop, so it does not start the server thread.
- `status` lists the minecarts a client of a server was sent.
- save_roundtrip: single player's server thread finishes the chunks its
player tracks, as vanilla's integrated server does, including some that
vanilla's server (view distance 4) left before their terrain or light.
Those had no blocks, light or heightmaps to compare yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a client of a server (single player's server thread, a LAN guest) the
placed block comes with the server's answer: `place` waits up to a second
for it before reporting no change. agent_control_e2e tries the next free
spot when the tree's trunk above the broken log hides a spot's faces.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With single player on its server thread, the window is a client of the
server, so what agents and harnesses did on the window's own state no
longer reached the world:
- control `give`, `time` and the agent's `command` run on the server as the
world's owner (owner_console_command), in the owner's level;
- `defaultgamemode` run by a guest or the owner reaches the hub, so new
guests join in the right mode (the hub is out while a guest acts);
- the remote client sends its held slot before using or attacking
(MultiPlayerGameMode.ensureHasSentCarriedItem);
- remote chat lines feed the agent's event log (`wait-for chat`);
- the remote client applies another player's crouch flag, and the entity
tracker sends Entity's shared flags and pose back at 0 (a player standing
up again was never sent);
- a respawned player is removed from and paired again for the other
players (their clients kept the dead body);
- a teleport drops the guest's moves sent before it, so a fall in them no
longer hurts the guest where it lands;
- flight summaries report the server's tick time (`tick_ms`).
lan_e2e finds the guest by name in `lan status` (the owner is listed too
on the server thread), reads what the host window draws from its remote
view, respawns a dead guest by its death screen, and lets natural
regeneration heal during the pvp-off hit. agent_control_e2e waits two ticks
where the server answers a click.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player's world now runs on the integrated server thread unless
asked otherwise, as vanilla's does, so a world tick no longer holds up
a frame. `--no-server-thread` or `HORIZON_SERVER_THREAD=0` keeps the
world in the frame as an escape hatch; `--server-thread` stays.
lan_e2e.py sets the host's mode explicitly either way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server, single player's server thread included, the inventory
had no recipe book: the window ignored the recipe book packets, and
the server ignored a guest's ServerboundPlaceRecipePacket.
- horizon-protocol decodes the recipe book's packets and the recipe
displays they carry: RecipeBookAdd / Remove / Settings,
PlaceGhostRecipe and the serverbound PlaceRecipe. Every 26.3
`recipe_display` and `slot_display` type is decoded (shaped,
shapeless, furnace, stonecutter and smithing displays; empty,
any_fuel, with_any_potion, only_with_component, item, item_stack,
tag, dyed, smithing_trim, with_remainder and composite slots).
- The remote client keeps the book the server sends (ClientRecipeBook:
added, highlighted, removed, replaced), shows the unlock toast,
applies the open and filter settings, and counts the menu the
server last sent for the craftable filter. Clicking a recipe sends
PlaceRecipe; a ghost recipe from the server fades in.
- The server handles a guest's PlaceRecipe in the inventory grid and
the crafting table (ServerPlaceRecipe on the menu engine, for known
recipes only) and answers with the slots, or a ghost recipe when the
items are missing.
- The screen state reports the recipe book's buttons.
lan_e2e.py gains a `recipes` check: the guest gets oak logs, opens the
book, clicks the planks recipe, and a log lands in the grid.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window dropped ClientboundCommandsPacket ("argument nodes are not
decoded"), so on a server, single player's server thread included,
the chat was a bare input line without completion, colouring or usage.
- horizon-protocol decodes argument nodes: the 26.3
`command_argument_type` ids and every parser's properties
(ArgumentTypeInfo.deserializeFromNetwork), plus the custom
suggestion provider.
- command::network turns the tree into a Dispatcher. A node at a path
the client's vanilla tree also has takes that node's argument type
and suggestion provider; any other node takes the type its parser
and properties describe.
- On a server the chat is ChatScreen over that tree, so Tab completes,
arguments are coloured, usage and errors show, and Enter sends the
command to the server. Player-name suggestions come from the
server's player list. The old bare input line is gone.
- The screen state reports the chat input (`chat_input`).
lan_e2e.py gains a `commands` check: the guest completes `/gamem` to
`/gamemode` and `/gamemode cr` to `/gamemode creative` with Tab, and
the command runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server (the integrated server thread and the dedicated server)
the host has no player of its own, so effects that only touched the
host's player reached nobody; on a LAN world they reached only the
host. Beacons, the warden's and shriekers' darkness, the elder
guardian's mining fatigue, splash and lingering potions' splash and
the raid victory's Hero of the Village now run for each player in
the level, guests through `as_guest`.
The elder guardian's curse sends the guest the GUARDIAN_ELDER_EFFECT
game event, which the remote client now shows (the apparition and
the curse sound).
lan_e2e.py gains an `effects` check: a speed beacon next to both
players must give both of them speed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server (the dedicated server and single player's server thread) now
holds a level per dimension, like vanilla's `MinecraftServer.levels`. Each
server tick runs every level with players in it, one at a time, by swapping
its fields into place (game/levels.rs), so the world tick code runs
unchanged. Before this, a portal did nothing while two or more players were
online.
- Levels: the parked levels keep the world, streaming, save, block ticks,
entities, block entity state, raids, sculk and creaking scans. The
Overworld stays loaded; another level is saved and closed after 300 ticks
with nobody in it. Saving saves every level.
- Hub: `tick` is split into `poll` (the network, once per tick),
`tick_level` (the packets, chunks, block changes, containers and entities
of the players in the level in place) and `end_tick` (latency). Each guest
has a dimension; time, weather, sounds and events reach only the players
in their level, and the clock and weather last sent are kept per level.
- Travel: every player has their own portal state; a portal moves only that
player into the destination's level (`ServerPlayer.teleport` packets as
before). Respawning brings a player back to the Overworld's level.
- Player data saves and loads each player's dimension, and a returning
player is placed in their own level.
- tools/lan_e2e.py --server-thread --check portals: the guest goes to the
Nether while the host stays, then the host goes to the End while the guest
is in the Nether, and both come back.
The in-frame game keeps its one level that follows its player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's action runs on the host's game with the guest's state swapped in
(`as_guest`). The swap restored the open screen but not what opening one
does to the window: `capture(false)` released the host's pointer, so the
cursor showed and the camera stopped whenever a guest opened a chest, a
crafting table or any other menu. `keys.clear()` dropped the keys the host
was holding, and a sign, book, command block or mount menu the guest opened
was drawn on the host's screen.
- `capture` leaves the window alone while a guest acts.
- `as_guest` keeps the host's held keys, sign editor, book, command block
screen and mount menu, as it already kept the open screen.
- `state` reports `pointer_grabbed` (mouse movement turns the camera).
- tools/lan_e2e.py: `chest` and `crafting` check that the host keeps its
screen and its mouse look; a new `sign` check places a sign as the guest.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Interpolated animated sprites (`"interpolate": true`) mix two frames into
every mip level with `f32::round`. Baseline x86-64 has no rounding
instruction (it needs SSE4.1), so each byte was a call into libm's roundf:
7.8 to 8.5% of the render thread's samples on a Kaby Lake laptop.
Round with a truncation and an exact fraction test instead. The result
is bit-identical to `f32::round` for every value the blend can produce,
checked over all frame byte pairs at 144 blend times.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerList.tick broadcasts UPDATE_LATENCY once ++sendAllPlayerInfoIn > 600;
the hub sent it every 100 ticks. The extra updates landed in the
dedicated_server harness's respawn window and failed it at random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port CachedUserNameToIdResolver to the dedicated server, in place of the
in-memory name cache: usercache.json in the universe folder is loaded at
startup (bad entries skipped, a bad date warned about), players are added
when they join and kept for a month, and the file keeps the 1000 most
recently used, newest first, as Gson writes it.
The console's op, deop, ban, pardon and whitelist find players by name
through it, in any case. A name it doesn't hold is asked of the profile
service (profiles::find_by_name) on offline servers too, as vanilla does;
one the service doesn't know is the offline player of the lower-case name
on an offline server, and "That player does not exist" on an online one.
tools/test_user_cache_parity.py runs vanilla's server and Horizon's with a
mock profile service and compares the replies, the names looked up and the
files; the dedicated-server harness now compares usercache.json too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With oracle/facts.json, a mod the oracle did not load (a library it
needs is missing) lost every block: none of its ids were in the facts.
It now keeps the guesses from its blockstates, as without an oracle.
ATM10: Regions Unexplored (TerraBlender is CurseForge-only) gets its
805 blocks back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At render distance 32 the section arena holds about 1.5 GB of vertices
(2.1 GB of GPU memory in all while flying). Vulkan drivers fail an
allocation once video memory is full instead of paging like OpenGL
drivers, and wgpu's default error handler turns that into a panic: a
4 GB laptop GPU that also drives the desktop crashed a few seconds into
an RD32 world.
Arena pages are now created inside an out-of-memory error scope. A page
that doesn't fit in video memory is created mappable, which the
allocator places in host-visible system memory, so the terrain still
draws (more slowly) as OpenGL drivers do. A page that can't grow keeps
its contents and a smaller new page is tried; with no memory left at
all, the section is skipped rather than the game ending.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla skips a resource with an invalid path, a data file that does not
parse, one at a time; Horizon's content loader refuses the whole mod
set. carry now keeps them in outside/ with the reason (report
`unreadable`): upper-case paths, JSON that does not parse, shared
tags with "replace": true. A byte-order mark is dropped.
ATM10's 42 carried mods: 19 such files in 7 mods (Mekanism Tools'
ry_UA lang, CTOV's empty tag, Towns and Towers' replacing tags, broken
template pools, recipes and a lang file), and the 42 load as carried.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
lan/chunks.rs: keep the per-section encode cache and the cached
heightmaps, applied to the level's section range from #668. Heightmaps
use client_heightmaps_in with the level's min_y and height, and
set_dimension drops cached heightmaps. The light layers go over the
level's sections and drop sky light where the level has none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port 26.3's ServerWaypointManager for the players of a served world: each
player who receives waypoints gets ClientboundTrackedWaypointPacket for
every other player who transmits one, as a block (in view), a chunk (one
chunk out) or an azimuth (really far), with the update thresholds,
breaking rules and team colours vanilla uses. Crouching, invisibility and
a carved pumpkin or mob head hide a player; spectators see everyone and
are seen by no one; the locator_bar game rule turns it all off and on.
tools/test_locator_bar_parity.py runs vanilla's server and Horizon's with
two vanilla-codec players and compares every waypoint packet each receives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AvatarRenderer.getArmPose gives THROW_TRIDENT to the hand using a
trident: HumanoidModel lifts that arm over the head (xRot * 0.5 - pi)
and leaves the other arm's pose alone (affectsOffhandPose). Horizon
used the plain item pose, so the held trident, drawn with its throwing
model, pointed down through the body. An aggressive drowned raises its
trident the same way (DrownedRenderer), and zombie arms leave that arm
alone (AnimationUtils.animateZombieArms).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FloatGoal's constructor lets the navigation float, so Mob.jumpInLiquid
adds 0.04F a tick; Horizon set that flag only for a few species, and
every other FloatGoal user (pigs, cows, wolves, cats, horses, witches,
illagers...) got the non-floater 0.3 and bounced out of the water.
Register it from the goal (and from the constructors that set it:
allay, armadillo, camel, goat, parrot, wither), and add 0.04 as the
float vanilla adds.
Mobs that drop back into water now splash (Entity.doWaterSplashEffect
from updateFluidInteraction, skipped on the first tick), with their
swim splash sound. The new mob_ai deep_water scenarios also caught:
horses register addBehaviourGoals last (none for the skeleton horse, no
mount panic for the zombie horse), RandomStandGoal waits the horse
ambient interval (400 for every AbstractHorse), and the cat's sit-on-
block goal draws its delay only for tame cats once the last ran out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#656 handles ClientboundChangeDifficultyPacket and refreshes the World
Options screen too; this branch's own arm became unreachable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The player's model never read the swimming pose: sprint-swimming and
crawling drew it upright and walking. Add LivingEntity.swimAmount
(eased 0.09 a tick toward isVisuallySwimming), AvatarRenderer's swim
rotation (tipped by the pitch in water, -90 out of it, laid along the
swimming box) and HumanoidModel.setupSwimAnimation (the 26-unit arm
stroke, head down, flutter kick). The avatar rotations now follow
LivingEntityRenderer.setupRotations' death and upside-down turns, as in
vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
level/data.rs, level/mod.rs: load, load_with_behaviour_json and
load_with_gameplay all go through one cached() helper (try the snapshot,
build and store on a miss), so #607's client path keeps this cache.
Resolution from contrib-review-1's validated integration, without #601.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
encode_in built the light layers twice (encode filled them for every
section, then encode_in replaced them with the ones that exist around the
column). On the ThinkPad at 25 walking bots light_data was 8 % of the server
thread. The light is now built once, and a section's packed DataLayers stay in
the section cache while the section is unchanged (relighting moves it to a new
address, as an edit does). Uniform light is filled directly, not kept.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server holds one level at a time, as the in-frame game does. While one
player is online (single player on the server thread, or a world with one
player in it), a Nether or End portal swaps the server's level the
in-frame way (switch_level, split out of enter_dimension) and the player
is moved as ServerPlayer.teleport moves it into another level: a Respawn
packet keeping all data, the difficulty, permission level, teleport,
abilities, level info, inventory, effects and the portal travel sound,
then a teleport to the exit once the destination has loaded (the Nether
portal found or built, the End platform, the Overworld's spawn column).
Respawning away from the Overworld takes the level back there first.
What the hub sends follows the level:
- Chunks carry the level's sections only (y 0 to 255 in the Nether and the
End), without sky light where the dimension has none, and heightmaps
from the level's floor. Until now every level went out with the
Overworld's 24 sections, which a client in the Nether or the End reads
64 blocks off.
- The dimension type follows the level the hub names; the default spawn
position is the Overworld's.
- A level switch keeps the block change log (an open hub kept none after
a portal) and the sculk watch list.
Worlds whose player is in the Nether or the End now open on the server
thread too.
The network client maps the End to the End (it drew it as the Overworld)
and shows the death screen on its own PlayerCombatKill, as
ClientPacketListener.handlePlayerCombatKill does, whatever health came
last.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientboundUpdateAdvancementsPacket and ClientboundAwardStatsPacket are
new in the protocol crate, and the window applies them as vanilla's client
does:
- ClientAdvancements.update: a reset replaces the tree, added advancements
join it with their place on the tab (DisplayInfo x, y), removed ones go,
each changed one takes the progress sent, and one that is done in a
non-reset update with show set gets its toast. The advancements screen
lays a server's tree out where the server placed it and shows everything
it sent.
- handleAwardStats: the counters the server sends, by registry name.
Opening the statistics screen asks for them (StatsScreen.init's
REQUEST_STATS).
Joining and leaving a server start from empty advancements and statistics.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Modpacks page between Versions and Patch notes (docs/modpacks.md, the
working version's launcher step):
- Import a pack file (.mrpack, CurseForge zip) or an instance folder (Prism,
MultiMC, CurseForge): horizon-port-pack's import, inventory (Modrinth by
file hash) and report into ports/<slug>/ in the game's home.
- The coverage report: ported, partial, not yet and not needed, with every
mod's class, tier, licence and reason, filtered by status.
- Port: the portable mods' jars (content, data tier) the player's own way
(their folder, the pack's links, Modrinth's CDN; what only CurseForge has
needs its key and is said so), carried over for the selected version's
prepared tables; the pipeline's log shows while it runs.
- Play: the game as the pack's own profile, ports/<slug>/home/ (its saves,
options.txt copied from the player's the first time) with the carried-over
mods as dev mods. The session flow is shared with Play (start_client).
- The same from the command line: horizon-launcher modpack list | import |
port | play | remove.
Nothing is uploaded or shared; jars and ports stay in the game's home.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher's "Import modpack" needs the pipeline as a library and a way to
get the jars a pack names without redistributing anything.
- The steps move into the library (pipeline.rs) with a line callback, so the
command line and the launcher run the same code; main.rs is a thin CLI.
- fetch: the jars of the portable mods (content, data tier) into
<work>/jars/, the player's own downloads: copied from a folder they have
(by SHA-1, or a CurseForge app instance's minecraftinstance.json), else
from the pack's own link (.mrpack hosts only) or the same file on
Modrinth's public CDN found by SHA-1, kept only when the hash matches.
What only CurseForge has is listed as needing a CurseForge API key.
<work>/fetch.json records each file.
- port: every step after the import for the portable mods (fetch, the
inventory again with the jars read, carry, report).
- A CurseForge zip whose overrides hold Crash Assistant's modlist.json (All
the Mods 10 does) takes its jar names, mod ids and SHA-1s instead of bare
project ids, so Modrinth answers for most files.
- inventory goes on offline when Modrinth cannot be reached.
All the Mods 10 8.3's CurseForge zip: 496 files named; of the 54 data-tier
content mods 42 download from Modrinth's CDN (84 MB) and 12 need the key or
the player's instance; 42 carried over (partial: the harness is a dev step).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player on its server thread, LAN guests and dedicated server players
are network players, and the server gave them none of these:
- PlayerAdvancements: each player's progress loads from and saves to
players/advancements/<uuid>.json (an integrated server's owner keeps
single player's file). The game's triggers, awards and the advancement
commands work on the player the game acts for (as_guest swaps the
progress in), every tick runs the tick, location and inventory triggers,
and flushDirty sends ClientboundUpdateAdvancementsPacket with what became
visible or hidden and the progress that changed. A player who has been
here gets their saved advancements in the first packet; a new one gets
the same first tick as before. Announcements go to everyone
(PlayerList.broadcastSystemMessage).
- ServerStatsCounter: per-tick and movement statistics are counted for each
player, saved to players/stats/<uuid>.json, follow the scoreboard
criteria under the player's name, and go out in
ClientboundAwardStatsPacket when the client asks (REQUEST_STATS), all of
them the first time.
- ServerRecipeBook: the player file's recipeBook is kept, sent on joining
(settings and every known recipe), and recipes unlocked later go out in
ClientboundRecipeBookAddPacket.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's startup now falls back to vanilla resources when packs fail; the
behaviours snapshot folder is set just before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With single player on its server thread, the window is the server's client
and dropped several packets the in-frame game never needed:
- ClientboundBlockEntityDataPacket and the block entities of chunk packets:
the block entity of that type loads the tag (signs, spawners), as
handleBlockEntityData and LevelChunk.replaceWithPacketData do.
- ClientboundChangeDifficultyPacket: the level data's difficulty and lock.
- ClientboundSetDefaultSpawnPositionPacket: the respawn data spawn
compasses point at.
- ClientboundSetChunkCacheCenterPacket: ClientChunkCache's view centre;
columns outside the storage (the server's radius + 3) are ignored or
dropped, as ClientChunkCache.Storage does.
- ClientboundAnimatePacket: wake up, and the critical and magic critical
hit emitters.
- ClientboundTickingStatePacket / ClientboundTickingStepPacket (new in the
protocol crate): the level's TickRateManager. A frozen level holds its
time and non-player entities, and the server's rate only ever slows the
client timer down (Minecraft.getTickTargetMillis).
- ClientboundServerDataPacket: the server list entry keeps a valid icon
(ServerList.saveSingleServer); single player has no entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Path searches (vanilla's PathFinder with WalkNodeEvaluator) were 14 % of
the world tick with 80 villagers. Two costs vanilla doesn't have:
- the node index and the per-query path type and collision caches used the
standard SipHash; vanilla keys them by packed position in fastutil maps.
They use the Fx hasher now (nothing iterates them, so no order changes);
- a position's static path type (its block, the one below and up to 24
neighbours, `getPathTypeStatic`) was worked out again for every mob box
that covered it. Vanilla caches the block part level-wide
(`PathTypeCache`); the finder now remembers each position's type for the
query, during which the world doesn't change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When chunks arrive, the sculk index looks through them for sensors,
shriekers and catalysts. It unpacked and tested all 4,096 blocks of every
section that wasn't uniform: 10 to 18 ms in a tick after a world loaded,
more over a larger render distance (#489).
A section whose palette holds no sculk state has no sculk block, so it is
passed over without reading its blocks; the others are scanned as before,
and the same blocks are found.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every tick, each item and mob that can use a portal sweeps the blocks its
box passed through since the last tick, looking for a nether portal block
(`Entity.checkInsideBlocks` reaching `NetherPortalBlock.entityInside`).
Each sweep allocated a visited set and read every block of the path.
Every block a sweep visits lies in the box covering the entity's box at
both ends. When no section that box touches has a nether portal state in
its palette, no visited block is a portal: the sweep is skipped. Sections
are checked once per tick, and a box over more than 64 sections (an
entity teleported far) is swept as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A loaded world had none of its structure starts in the generator's cache.
The first time natural spawning (or anything else) asked which structures
hold a position in a chunk referencing a village, the game thread built the
village start: 60 to 120 ms in one tick, once right after the world opened
and again a few ticks later for the next start (#489).
Vanilla reads the starts with the chunk. The streaming worker that loads a
saved chunk now builds the starts it references into the same cache, so the
game thread finds them there. The starts are the same: the same function
builds them from the same seed and chunk, only earlier and on a worker.
The cache now keeps the first starts built for a chunk when two threads
build them at once, so every user shares one start and its pieces'
placement state, as vanilla's single StructureStart does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Every PR of the startup batch adds a different tools/profile_client_startup.py,
so any two of them conflict. This one becomes tools/profile_shared_json.py; its
guide and tools/profile_gameplay_decode.py follow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every PR of the startup batch adds a different tools/profile_client_startup.py,
so any two of them conflict. This one measures only Behaviours::load, so it
becomes tools/profile_behaviour_cache.py, and --cache-dir passes the snapshot
folder to the example so warm loads can be measured.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Every packet sent to a guest woke its writer thread, and at 25 walking bots
those wake-ups (mpsc notify, semaphore_signal) were about a quarter of the
server thread's tick. While tick_lan runs, packets for each guest are held
and handed over in one Out::Packets at the end; a send outside a tick, or a
disconnect, first hands over what is held, so the bytes and their order are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
options_ui.rs: #585 put Mods... in the telemetry slot; keep it and put
Horizon Extras... in the credits slot as this branch intended.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
component_nbt wrapped the numbers of a mixed list under the empty key, and
the NBT writer then wrapped those wrappers again, as ListTag.wrapIfNeeded
does. A vanilla client reading /give's reply ("Gave 1 [Raw Iron] to ...")
unwrapped once, found a {"": 1} argument and dropped the connection. The
list is now left to the writer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
world_spawn now takes about 1480 s on a loaded Mac mini runner, just
under the old 1500 s limit, so PRs failed on a timeout rather than a
mismatch (#567, #514).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The resource folder is shared: worktrees and parity harnesses link one copy,
and the launcher copies a prepared folder to each new compatible version. A
snapshot there would be read by builds that resolve the tables differently
while the VERSION constant stays the same. Keep it in the game's own
.horizon-cache (set by the client; tools, tests and servers don't cache), add
the running executable to its key, and skip hashing the resources when there
is no snapshot. Postcard without default features drops heapless and the
unmaintained atomic-polyfill from the lockfile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a server the F key swapped the hands only on the client, and the
inventory menu's off-hand slot (InventoryMenu slot 45) was missing from the
client's slot table, so clicks on it went nowhere and the server's updates
of it were dropped (#543).
- F sends ServerboundPlayerActionPacket SWAP_ITEM_WITH_OFFHAND and predicts
nothing, as vanilla's Minecraft.handleKeybinds does (not for spectators).
- Inventory menu slot 45 is the off hand; SetPlayerInventory slot 40 too.
- The server swaps only for non-spectators and stops the item in use
(ServerGamePacketListenerImpl.handlePlayerAction).
Fixes#543
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest's STOP_DESTROY_BLOCK cleared the digging before the host's own
count was done, so a block the client broke stayed on the server and the
player fell through or froze on it. The host now follows
ServerPlayerGameMode: it breaks the block when its progress since START is
at least 0.7 and otherwise finishes the dig alone (the delayed destroy).
Block acks go out after the tick's block updates, and the client keeps
predicted blocks until the server acknowledges them
(BlockStatePredictionHandler), so a refused break comes back instead of
being lost.
Fixes#500
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Chunk packets took nearly all of the server's tick with players moving
about: every send re-encoded the column for that player (each section
unpacked and re-paletted with linear searches, light layers packed a
nibble at a time, heightmaps primed again) and unpacked all 9 x 24
sections around it to find which light layers exist. At 50 walking
bots that was ~85 % of the tick, 280-470 ms ticks against vanilla's
33 ms on the benchmark machine.
The encoder now keeps what a resident section gives the wire (its
bytes, whether it holds block entity blocks, whether it is all air) by
the section's address. Sections are immutable behind Arc and edits go
through Arc::make_mut, so an edited section moves to a new address,
and the cache's Weak keeps an old address from being reused: the same
address is the same unchanged section. Heightmaps are kept per column
the same way. Block entity tags are still read for every packet. The
bytes sent don't change.
Also: Paletted::from_values uses an index table instead of linear
palette searches (the same palette order and bits), the all-air check
reads the palette first and stops at the first block, uniform sections
skip unpacking, and light layers pack two blocks per byte in one pass.
On a busy Mac mini with 50 walking bots (`tick query`): about 400 ms
ticks with the faster palette and air checks alone, 14 ms with the
caches.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With single player on its server thread, the window is the server's
client, and some of what it answered was its own empty side:
- `horizon-ctl save` and `save-all` returned ok without saving: the
window's save skips worlds it is connected to. They now save on the
server thread (`Integrated::call`).
- `horizon-ctl chat-log` read the window's local chat, which never
showed what the server said (command feedback, joins). It reads the
connection's chat, as the agent's chat query already did.
- The game's own messages (Open to LAN, screenshots...) went to that
local chat, which is not drawn while connected: they now join the
connection's chat.
- `state` names the world (the server thread's `LevelName`).
- The client follows its permission level (EntityEvent 24..28,
LocalPlayer.setPermissionLevel), which the commands the chat offers
and checks depend on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game reads the data generator's registries.json (and commands.json)
next to the resources (#573). Versions whose resources the launcher copied
from another installed version, or that were prepared before the preparer
put the reports there, have them only in the scratch output of the version
that ran the data generator, if anywhere. At start, on `versions`/`update`
and before starting the game or a server, the launcher now copies them in
from the version's own scratch output or any installed version pinning the
same server jar, so existing installs can join servers again without
preparing again.
Complements #576 (the preparer and the client side of #573).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each dimension change builds the destination's generator again, and with
it a new structure state, so the 128 stronghold ring searches ran again on
every return to the Overworld (about 1.5 s on two workers) and held up the
first new chunks. The ring positions depend only on the shared generator
data, the seed and the dimension's generator definition, so the generator
data now keeps them by those (the last eight), and later generators for
the same world and dimension share them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Single player on its server thread could not be opened to the LAN: the
window is a client of that server, and Open to LAN, /publish and
`horizon-ctl lan` tried to start a second hub on the window's side.
The server thread now publishes itself, as IntegratedServer.publishServer
does:
- The window asks its server with `Integrated::call` (executeBlocking):
a task run between two ticks, while paused too, with its answer.
- Publishing binds the port at once (a busy port fails right away), then
the hub starts listening and announcing the world (LanServerPinger)
with the MOTD "<owner> - <world>". Until then only the owner may log
in; afterwards anyone may, and status pings are answered.
- Unpublishing stops the listener and sends every player but the owner
away (multiplayer.disconnect.server_shutdown).
- /publish and /unpublish run on the server for the player who types
them, and the integrated server's command tree (with publish, without
the dedicated-only commands) is what its players get.
- The owner's level follows Allow Commands; other players get game
master commands only when guests may use commands and the world allows
them (IntegratedServer.getCustomPermissionLevel). Every player's
command runs at their own level.
- The world options screen reads and applies its settings on the
server: difficulty, default and personal game mode, Allow Commands and
the LAN settings; the window shows the outcome.
- A published world does not pause (Minecraft.pause needs
!isPublished), and F3+Escape follows the same rule.
- Memory connections are never compressed, whatever the threshold
(Connection.isMemoryConnection), so network players get the usual 256.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An online-mode server checks players by their account's UUID, so op,
whitelist, ban and pardon now resolve a name as vanilla's
CachedUserNameToIdResolver does: a player on the server, then one seen
since the start (in memory; vanilla also keeps usercache.json), then
the services' profiles/getByName endpoint through the discovery
document. A name with no account is "That player does not exist", with
vanilla's "Couldn't find profile with name" warning. Offline servers
keep the offline profiles.
horizon-server does the same while the server is stopped (op,
whitelist add, and the players init writes), and removes entries by
the name already on the list. `init --offline` (or HORIZON_ONLINE=false)
sets online-mode off; init's note now warns about offline servers only.
The online-mode harness adds a sixth login: `whitelist add` for the
player who joined earlier needs no lookup and lets them in, and an
unknown name is looked up and refused, on vanilla's server and on
Horizon's (43 checks).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dedicated server reads registries.json and commands.json (the data
generator's reports) next to the prepared resources, and otherwise only
finds them in the .local/datagen folder of a development checkout. A
prepared folder copied or mounted on its own, as the Docker image
expects, stopped the server at start with "registries.json not found
next to the resources". The prepare step now copies both reports into
the folder (again whenever the server jar changes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
online-mode=true now authenticates players as vanilla's
ServerLoginPacketListenerImpl does. The server makes a 1024-bit RSA key
pair at startup ("Generating keypair"), sends it with a random challenge,
checks the challenge and decrypts the shared secret from the client's key
packet, encrypts the connection with AES/CFB8, and asks the session
service's verify endpoint (hasJoined, through the services discovery
document) whether the player joined with the server's hash, from the
"User Authenticator #N" thread. A verified profile keeps its UUID and
properties, so skins show in the player list; an unverified one is
refused with "Failed to verify username!", and an unreachable service
with authservers_down. The offline-mode warnings are only printed in
offline mode.
Refused logins are logged as vanilla logs them: "Disconnecting
<name> (/<address>): <reason>" and "<name> (/<address>) lost
connection: <reason>", with log-ips respected.
horizon-protocol gains RSA key generation (Miller-Rabin primes,
e = 65537), the X.509 encoding of the public key and PKCS#1 v1.5
decryption. test_online_mode_parity.py runs its logins against Horizon's
server too, plus a verified player the whitelist refuses, and compares
the two servers' log lines about the player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every block sprite is a layer of one texture array, and GPUs give an array
at most 2048 layers (Metal, most Vulkan drivers). Vanilla takes 1250; eight
real NeoForge mods carried over (docs/modpacks.md) take 2183, and the game
stopped at start with "GPU supports 2048 texture layers; 2200 required".
- Sprites: a still texture with the same pixels and mip settings as one
already loaded reuses its layer (mods copy vanilla's textures under their
own names): 17 layers fewer for those mods, none for vanilla.
- The renderer splits the layers over up to four arrays of 2048 (8192
sprites). terrain.wgsl picks the array per fragment with derivatives taken
in uniform control flow; a pipeline constant keeps vanilla, which needs one
array, on the same single textureSample as before.
- HORIZON_SPRITE_SPLIT (dev only) forces a smaller split: vanilla drawn from
three arrays of 512 renders the same blocks as from one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve game.rs and titles.rs against the resources split (#537): layer
names and item names now come from self.pack, the atlas from self.atlas.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#528 was tested before #537 moved the asset registry from Resources to
the reloadable pack, so main stopped compiling after both merged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla draws a section when its column is within the render distance of
the camera's column with a column of slack on each axis
(SectionOcclusionGraph.isInViewDistance → ChunkTrackingView): 533
columns at render distance 12. Horizon meshed a circle of radius 12 (441
columns) and drew sections whose centre lay within 16 x distance + 24
blocks, so the edge of the world, mostly in fog, missed columns vanilla
shows, and fell short of vanilla's corners.
`streaming::in_view_distance` is vanilla's test. Streaming meshes the
columns it accepts around the player (and forced ones), and the
renderer's column walk draws the ones it accepts around the camera.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A player who joins over the network, which is now single player's own
player on its server thread, had no status effects: what they drank, ate
or were given went to the host, and their client never heard of any.
- Each guest has their own effects. `as_guest` trades them, and what
their body was given for them (attribute modifiers, armor protection,
enchantment modifiers), with the host's while the guest acts.
- They tick on the server with the guest's body (LivingEntity.baseTick's
tickEffects), spectators included.
- The client is told as vanilla's ServerPlayer does: every effect when
placed (PlayerList.sendActivePlayerEffects), then added (blended),
updated, refreshed every 600 ticks left, and removed. An effect that
only counted down is not sent again.
- The client keeps its own effects from those packets
(LivingEntity.forceAddEffect), counts them down (tickClient), and its
predicted movement, HUD icons and night vision follow them. A respawn
starts without effects on both sides.
- Guests save and load them in their player file.
Saving also stops writing effects that have run out: the loaded player
compound kept the old `active_effects`, and they came back on reopen.
Potions in flight now tick on a server without a local player too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On its server thread a single-player world is joined like a server, so
the Horizon pause menu named it "Multiplayer" with a "Disconnect" button,
and F3+Escape opened the full menu as on a server. The server thread now
hands the window the world's name with its connection, the pause menu
shows that name and "Save and quit", and `has_singleplayer_server`
(Minecraft.hasSingleplayerServer) covers both ways of running a world for
the pause checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The integrated server draws nothing, but it built the window's full HUD
atlas (GUI, item, block, entity and particle textures plus the run-time
slots). It now builds an atlas of the font sheets only, reads the glyph
widths from it and drops the pixels: text still measures the same (books
split their pages with it), without holding the textures.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`report <work>` writes coverage.json and coverage.md: per mod ported,
partial, not yet or not needed with the reason, and the totals by class,
tier and licence family. It reads the inventory, each mod's carry report
and its harness result: ported is a green harness with nothing left out
(blocks with a class of their own are counted and named); partial lists
the failing checks, the blocks left out and what the templates miss; not
yet names the API tier or the dependency it waits for; not needed covers
libraries, performance mods and pack tooling. Unclassified mods are not
yet, never not needed.
`import` also reads Crash Assistant's modlist.json, the list ATM10
publishes in its repository (mod ids, names, versions, Modrinth hashes,
the loader entry), and the inventory uses its mod ids and names when
there is no jar.
ATM10 (496 files, the eight mods we downloaded carried and checked):
ported 1, partial 7, not yet 367, not needed 121.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Carrying over seven real ATM10 mods (Cloud Glass, Factory Blocks,
Glassential, Macaw's Doors, Fences, Paths and Trapdoors) showed what the
guesses missed:
- Loot tables: 1.21.1's `conditions` lists, `functions` and
`condition`/`function` kinds are now 26.3's single `condition`,
`modifier` and `type`; silk-touch and shears tool matches become
`minecraft:tool/can_silk_touch` and `can_shear`. Before, every drop
condition was ignored (glass dropped itself by hand, upper door halves
dropped doors, double slabs dropped one).
- With the oracle's facts (<work>/oracle/facts.json, from
tools/neoforge_oracle.py in the next PR), every registered block gets
the vanilla template of the same vanilla class and states that matches
its shapes, sounds, BlockSetType and WoodType best, and the mod's own
hardness, blast resistance, light, tool need, friction, map colour and
sounds. Items get stack size and rarity. The carry report lists what a
template does not reproduce and why a block was left out.
- Convention tags a recipe uses (c:dyes/red) are written from the
oracle's tags, vanilla and own members only; the jar's own c tags stay
inside the mod.
- Without the oracle, blockstates that omit properties their models
ignore (powered, waterlogged) still find their template, and names pick
between fences and panes, doors and trapdoors.
- `version = "${file.jarVersion}"` is read from MANIFEST.MF.
- `--resources <prepared>` limits templates to blocks Horizon has.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Loading seven real NeoForge mods carried over by horizon-port-pack (about
1,000 blocks) broke the content root in four places:
- The per-state NBT tables (cached collision, occlusion, movement, worldgen
block properties and support) passed the NBT reader's 1M-node cap once
mod states were appended. They are trusted prepared tables with a row
per state, so they now read and write with Limits::tables() (16M nodes,
256 MiB) everywhere they are loaded.
- The overlay linked the base root by the path it was given, so a relative
base gave dangling links. The base is made absolute first.
- Carried-over tags name blocks the port left out and other mods' items.
Vanilla fails such a tag as a whole; entries naming ids nobody registers
(or tags nobody defines) are now dropped, so the rest loads.
- Mods use the convention tags (c:dyes/red, c:glass_blocks/colorless).
Content mods may add to data/c/tags/ as to vanilla's, and with content
mods loaded the tag loaders read the c namespace and the mods' own.
Stonecutter and smithing recipes in mod namespaces load too (ids
<ns>:<name>), as crafting and cooking ones already did. With no content
mod nothing changes: the loaders read the minecraft namespace only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The per-frame section walk iterated a SipHash map of every resident
section, then looked each visible one up twice more by key. Resident
sections now live in a dense list grouped by chunk column: one distance
and frustum test rejects a whole column (exact: a section's box lies in
its column's box and the plane test rounds monotonically), the rest are
reached by slot, and the visible list sorts on integer keys with ties
broken by section key, so the order no longer depends on hash order.
FaceGroups looks its axes up in a table instead of searching for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What the dedicated server does today (measured against vanilla 26.3), the horizon-server supervisor and
its packaging, server.toml, the CLI and TUI, the web panel, backups and updates, the control API and MCP,
the bot benchmark plan against vanilla and Paper, the working version for 2026-10-23, and the PRs that
build it (#551 to #556).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-bots is a load client on horizon-protocol for any 26.3 server: offline login, configuration,
keep-alives, pings, teleports, chunk-batch acknowledgements, and movement at 20 Hz. The bots play in
creative and fly above their spawn point, so they need no world of their own and no server refuses their
moves; idle, walk, explore and mixed behaviours are deterministic from a seed.
tools/server_bench.py runs the same bots on Horizon's server (horizon-server run) and on the pinned vanilla
jar, each fresh from the same server.properties, and measures MSPT from each server's own /tick query,
RSS and CPU of the process tree, the chunks the bots receive, and the start time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One page embedded in the binary (crates/horizon-server/panel: HTML, CSS and JS, no framework or CDN): status,
live TPS, MSPT, RAM and CPU graphs over the last 10 minutes, players with op and kick, the console, and
backups with restore, plus restart and stop.
It listens on [panel] bind, 127.0.0.1:8130 by default; a tailnet address reaches it from the owner's
devices, and any other address is refused unless allow_public is set. `horizon-server panel` prints a
one-time login link (10 minutes) that becomes an HttpOnly, SameSite=Strict session cookie. The Host header
must name the panel, every POST needs X-Horizon: 1, failed logins are rate-limited, and the page is served
with a strict Content-Security-Policy. The control API gains GET /api/v1/metrics and, with the token
only, POST /api/v1/panel/login.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window now joins its server thread through an in-memory pipe, as
vanilla's integrated server player does (LocalChannel), rather than a TCP
socket on 127.0.0.1. The server opens no port until it is opened to the LAN.
- horizon-protocol: Connection runs over TCP or one end of a Pipe (a
blocking byte pipe with read timeouts and shutdown; dropping an end
closes it).
- Lan: NetConfig.listen false starts no listener; connect_local serves the
other end of a pipe on its own thread, through the same handshake, login
and configuration as a socket.
- Session::connect_local joins through a pipe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With --server-thread or HORIZON_SERVER_THREAD=1, opening a single-player
world starts a game of its own on a "Server thread" over the shared
resources, as vanilla's IntegratedServer does, and the window joins it as a
player over a local connection. The world tick then never runs inside a
frame.
- The server listens on 127.0.0.1 only, lets in the window's profile alone,
without compression, and makes it an operator when the world allows
commands.
- The window's player is the world's own player: it loads from and saves to
players/data/<singleplayer_uuid>.dat, so the world opens the same way in
either mode.
- A pause screen pauses the server (IntegratedServer.tickServer), and
leaving the world or quitting waits for the server to save.
- The integrated server does not mesh sections it never draws.
- Worlds whose player is outside the Overworld open the usual way for now.
- The bench summary reports the 0.1% low.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
run() loads the shared resources and the pack, then hands them to a
constructor that builds a game with no world open. The same constructor will
build an integrated server's game on its own thread. The struct literal moves
unchanged; Options derives Clone so a server can start from the client's.
No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resources now holds only what is loaded once and never changes (blocks,
items, recipes, biomes, mobs, collision, motion) and is Send + Sync behind
an Arc, so an integrated server thread can share it with the client.
What a resource pack reload replaces moves to PackResources (assets,
meshing context, item renderer, names, panorama, clouds, forcefield), built
by one loader used at startup and on reload. The client-only mutable state
(the HUD atlas with its font cache and skin slots, fog state, screen effects)
becomes the game's own fields.
No behaviour change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wgpu validates every indirect draw with a compute pass and a per-draw
batcher before the render pass, for untrusted WebGPU content. The section
draws are the only indirect draws and are in bounds by construction; the
validation cost about 0.1 ms of the render thread per frame at render
distance 32 on Metal. Debug builds keep it, and
WGPU_VALIDATION_INDIRECT_CALL=1 turns it back on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AcquirePoi (job sites, beds, meeting points) and the bed searches read
every block of the sections in a 48-block sphere. Like vanilla's
PoiManager, each section now keeps the list of its POI blocks, rebuilt
from its blocks after an edit (a weak reference detects edited and
replaced sections). The searches visit the same positions in the same
order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- packaging/server/install.sh (Linux, macOS) and install.ps1 (Windows) install the latest release (or
--version) from GitHub, check the SHA-256 GitHub publishes for it, and keep releases side by side:
<dir>/current points at the one in use, so an update or a rollback is one link, and the game files
prepared from the owner's Minecraft stay in <dir>/game-files across updates.
- packaging/server/Dockerfile builds the server and the engine; the prepared game files are mounted
read-only, and the first start writes server.toml from HORIZON_EULA, HORIZON_WORLD, HORIZON_SEED,
HORIZON_PORT, HORIZON_WHITELIST, HORIZON_PLAYERS and HORIZON_OP (`init` reads them too).
- `horizon-server service install|uninstall|print` writes and loads a systemd unit (user, or --system),
a launchd agent, or a Windows task at logon. It runs the installer's launcher, so updates apply on
the next restart.
- Releases build horizon-server and ship it beside horizon-ctl and horizon-prepare; PLAY.txt says how
to run a server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`backup [NAME]` zips the world and the settings into backups/; on a running server it turns saving
off, flushes, zips and turns saving back on, so the game stays up. `restore BACKUP` puts a backup's
world in place and keeps the current one in backups/replaced-<time>/; a running server restarts with
it. `[backups] every = "6h"`, `keep = 12` in server.toml back up on a schedule and prune the
automatic backups only. The control API gains GET/POST /api/v1/backups and POST /api/v1/restore.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The supervisor serves a small HTTP API on 127.0.0.1 (a token in .horizon/api.json, mode 0600, and a
Host check): status, logs, console commands, stop and restart. `start` runs the server in the
background and returns once the game is up; `restart` and `cmd` wait for it too. A sampler reads the
game's tick rate, tick times, players and chunks every second over the existing control endpoint
(`--control-socket`), and RAM and CPU from the system.
The dedicated loop now records its tick times in the game clock, so `/tick query` reports real
numbers on a dedicated server, as it does in single player. The changes to game/dedicated.rs are
additive: the log capture for command replies, the tick counter and the control requests
(game/dedicated/status.rs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first part of hosting a Horizon server with one small program
(docs/server.md). horizon-server is a new 1.3 MB binary with no window,
GPU, audio or controller code; it runs the game headless
(horizon-client --dedicated) as its engine.
- horizon-server-files: server.properties, eula.txt and the user lists,
moved out of horizon-client unchanged so both programs share vanilla's
exact reading and writing; horizon_client::server re-exports it.
- server.toml: typed settings for what owners change, [properties] for
any other vanilla key, vanilla 26.3's defaults (a test checks them
against ServerProperties). It is written into server.properties before
every start; keys the game changes there itself (/whitelist on) are
copied back afterwards, keeping the owner's comments (toml_edit).
`import` turns a vanilla server.properties into server.toml.
- init: the EULA (only a yes writes eula=true), world, seed, port (checked
free), whitelist and first operator; every question has a flag, so it
also runs unattended.
- run: the console on stdin, a clean `stop` on Ctrl-C, SIGTERM or SIGHUP
(the game runs in its own process group), and a restart when the game
crashes: 5 s, doubling to 5 min, giving up after 5 crashes in 10 min.
One supervisor per folder (a file lock).
- op, deop and whitelist for a stopped server, with vanilla's messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The threaded test added seed + 1 for seed = i64::MAX, which overflows in debug
builds (CI's verify).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
carry <work> [<mod id>...] turns each content mod with a jar into a folder
content mod (API 0.2) in <work>/mods/<id>/carry/, for local use:
- assets/<id>/, data/<id>/ and data/minecraft/tags/ are copied; other
namespaces and vanilla data go to outside/ for review; no class file is
copied.
- Vanilla-type recipes get 26.3 ingredients ("id", "#tag", lists);
custom types and NeoForge conditions are kept and listed.
- Item models get the items/ client definitions 1.21.4 introduced.
- Each block is declared from a vanilla template guessed from its
blockstate properties; unknown property sets are listed. Items
without a block are declared from minecraft:stick.
The test builds a small NeoForge jar, carries it over, and reads the
result with horizon-mods' own Content reader.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A CLI for the first steps of docs/modpacks.md's pipeline:
- import: a Modrinth .mrpack, a CurseForge zip or a Prism / MultiMC
instance (or a folder of jars) to pack.json: Minecraft and loader
versions, each file with its hash and sides, and the overrides.
- inventory: Modrinth metadata by SHA-1 (cached in modrinth.json, so
reruns work offline), and the jars when the user's instance has them
(neoforge.mods.toml / mods.toml / fabric.mod.json and a file census,
without decompiling). Each mod gets a class and API tier from the
reviewed ATM10 table, then rules on categories, sides and the census,
and a dependency layer. Writes inventory.json and inventory.csv.
Only metadata is downloaded; jars are never fetched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game builds the content root from the enabled content mods at startup.
Loaders that only read the minecraft namespace now also read the content
mods' namespaces (from the root's marker file): block loot tables, recipes,
blockstates, language files, and the HUD atlas's item sprites. /give and the
command registries know the new items and blocks. A block without a
blockstate of its own takes its template's, so every state has a model.
Adds examples/ruby-content, a folder content mod: a ruby block and a ruby
item with recipes, loot, tags, models, textures and names.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_client::mod_content builds the resource root for content mods as
an overlay over the base root (vanilla, or the resource-pack overlay):
- the base is linked;
- the block and item tables are rewritten, each mod block getting its
template's rows appended. That covers 13 JSON tables (facts, gameplay,
behaviours, models, render, landing, items, command registries, mob
data, worldgen flags, structure, survival) and 6 NBT ones (occlusion,
collision, support, movement, worldgen block properties). Names,
translation keys and in-table state ids move over, and the declared
overrides apply;
- each mod's own assets/ and data/ are copied in;
- tag additions are merged into the tag files and both block-tag bundles.
Vanilla state ids never move: mod states come after them, mods and blocks
in id order. Rewritten JSON keeps its key order. With no content mod the
root is the base itself. The game doesn't call this yet.
Tested over the real prepared resources: every loader reads the overlay
(Assets, BlockFacts, Items, collision, visibility, light, motion, worldgen
GeneratorData). The mod blocks carry their templates' facts with the
overrides applied, and vanilla's rows and ids are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A .hmod may carry assets/ and data/ beside mod.wasm, or instead of it. The
manifest needs api = "0.2" and the new registries capability ("add blocks
and items to your worlds"). Content mods have no WIT interface: code still
links API 0.1.
horizon_mods::content::Content reads a mod's content and checks it:
- plain resource paths only;
- 65,536 files, 16 MiB each, 512 MiB in all;
- files in the mod's own namespace, plus additions to vanilla tags;
- blocks and items declared in data/<id>/horizon/{block,item}/*.json, each
copying a vanilla template with typed overrides (hardness, resistance,
light, tool, friction, map colour, sounds; stack size, rarity), unknown
keys refused.
Mods::load refuses an enabled mod whose content fails these checks, with
the reason on its row. A content-only mod is on with no engine and no hooks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Loading a world read every file name under a folder pack's data/ to find
functions, function tags, predicates and item modifiers. The built-in pack
holds about 9,900 other data files, so this walk took 190-340 ms on the
render thread at every world load. A folder pack now walks
data/<namespace>/<kind>/ for those four kinds only, in the same order as
before; zip packs keep filtering their index.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/modpacks.md: how any pack (ATM10 down to small Fabric packs) gets to
Horizon: import (.mrpack, CurseForge zip or instance, Prism, packwiz),
inventory and classification, dependency order, mechanical carry-over of
data and assets, one agent per mod for code with a per-mod harness against
a dev-only NeoForge/Fabric oracle, and a coverage report. Also the mod API
gaps against 0.1, the registries 0.2 design (template blocks through the
overlay root), licence handling, how "10x" is measured against NeoForge,
and the scope for 2026-10-23.
docs/modpacks/atm10-mods.csv: ATM10's 495 jars from the pack's own
crash_assistant modlist (2026-10-15), with Modrinth metadata looked up by
hash: class, API tier, licence, sides. Metadata only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fills an area with N workers, optionally flies the centre along +x, and
reports wall time, chunks generated, sections meshed, how often a column
near the player is still unlit, and what Streaming::update costs the
calling thread. Allocations can be counted with
HORIZON_BENCH_COUNT_ALLOCATIONS=1 at build time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The macOS release build has failed since 05:54 UTC on 2026-10-16: on the
self-hosted Mac mini runner the game drew its 1500 frames in about 2.5 s,
before the world had started generating (chunks 0, player null), so no
release has been published since v2026.10.16.03.
--world-frames N exits after N frames drawn with the player in the world.
It is otherwise a frame-limited run (classic menus, --timeout-seconds), and
--frames keeps its meaning for the harnesses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 128 stronghold ring positions each need a biome search over a 57x57
quart area. The first worker to ask computed all of them inside a OnceLock
while every other worker blocked on it: about 2 s on an M4 Pro with 12 of
13 workers idle, after every world load and every return to the Overworld.
Vanilla's generateRingPositions runs each search as its own task on the
background executor. Now the ring chunks and search randoms are planned
serially (cheap), and every thread that needs the rings takes searches
until none are left, then waits for the rest. Positions are unchanged:
each search only uses its own forked random.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mesher groups each section's solid quads by the axis side they face
(Sodium's ModelQuadFacing) and records each group's nearest plane. When
the eye is behind every plane of a group, back-face culling would discard
all of it after vertex shading, so the renderer leaves the group out of
the section's draws. Seen from the village path, 29-36% fewer solid quads
reach the GPU at render distances 8-32; the picture is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BlockCollisionScan asked every cursor cell for its collision shape,
moved it into place and intersected it with Shapes.create(box), even
for air. Mob movement and every other Entity.collide spent most of
their time there. Now:
- CollisionBlock carries empty_shape, set by the registry for states
with no contextual rule and an empty cached shape (air, plants): the
scan skips them without asking. Providers that build CollisionBlock
themselves (the query-order fixture) leave it false, so their query
logs are unchanged.
- A shape whose bounds miss the box is skipped before moving it:
joinIsNotEmpty(AND) of the two is false anyway.
- Shapes.create(box) is built the first time a shape is not the unit
cube (finite bounds always make one), not for every scan.
- SceneCollisions hands the scan the resident chunk itself, so a cell
costs one section read instead of a chunk-map lookup.
Results are unchanged: running both paths side by side over 900,000
mob moves in a 600-mob village gave bit-identical movement.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PoiTypes.forState ran per palette entry of every section a villager
searched (and per block of the sections that hold one), with a string
match on the block name each time and a per-search hash map in front.
At world load, when every villager looks for a job site, bed and
meeting point at once, that was 16 % of the tick. The POI type of
every state is now worked out once, the first time a villager looks,
and read from a table after that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The probe builds MobWorld itself; it now keeps the same SeenGrid over
its snapshot for the mob ticks, so the mob_ai harness checks pushes
through the grid against vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.checkSupportingBlock runs findSupportingBlock after every move of
a mob on the ground. Horizon looked up and built the collision shape of
every cell of the cursor (27 to 48 per call, twice when it falls back
to where the mob came from). Like BlockCollisions' cursor, a block whose
shape stays inside its own cell (neither hasLargeCollisionShape nor a
moving piston) can only meet the area when its cell does, so only those
cells, and large shapes such as fences, are shaped now. The pick is a
total order, so visiting column by column finds the same block.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.pushEntities compared each mob's box with every other mob
of the tick's snapshot: quadratic, about 1 ms per tick with 600 mobs.
The snapshot is now also kept in a grid of 4-block columns by each box's
minimum corner, updated as each mob finishes its tick, and the push
reads only the columns its box can reach. Candidates come back in
snapshot order, so the pushes, their order and their sums are the same:
over 800,000 pushes in a 600-mob village both ways gave the same
targets in the same order. A query wider than the grid (or not finite)
falls back to the full scan.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drop this branch's own staging buffer (gfx/uploads.rs); the sprite uploads
go through the StagingBelt from #501. What stays is the animation mip-chain
cache holding every vanilla frame, with tests.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Renderer::render's buffer writes (globals, section origins and draws,
entity, cloud, celestial, border, debug-line and screen-effect vertices)
and the HUD's vertices now go through the frame's staging belt instead of
one Queue::write_buffer staging buffer each. The lightmap is uploaded only
when its pixels change instead of every frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every frame that crossed a tick re-uploaded the animated block sprites, and
that took about 1.8 ms on an M4 Pro (more than the world tick itself):
- The mip-chain cache held 8 MiB, which fits 256 of the 594 vanilla
animation frames (64-pixel layers, chains stored with spare capacity), so
the other 338 frames had their resize and mip chain rebuilt on the render
thread whenever their animation reached them. The budget is now 32 MiB and
chains are allocated at their exact size: all 594 frames fit in 13 MB.
- Each changed sprite mip level went through its own Queue::write_texture,
which allocates and frees a GPU staging buffer per call (a few hundred a
tick). They are now packed into one reused buffer, written once, and
copied with one submission per tick.
The texels are unchanged: a GPU test compares the batched copies with
write_texture over several flushes, and the existing cache tests compare
cached and generated chains.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Queue::write_texture creates a new staging buffer for every call, a kernel
call each on Metal. Animated sprites write every changed frame's mip
levels once per tick, so tick frames spent about 1.3 ms in buffer creation.
The copies now go through one wgpu StagingBelt recorded into the frame's
encoder; a GPU readback test checks they write the same texels as
write_texture on every mip level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nobody could reproduce it, and no issue tracks it, yet every release since
2026-10-14 repeated it. Players noticed (rdbt on Discord).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rules by category under a search box, a switch or number box each,
with vanilla's tooltip, number validation (Brigadier readInt and the
rule's range, red text, Done held) and ConfirmScreen before discarding.
Done sets the changed rules and tells the player, as handleSetGameRule
does. The button follows the commands the player has, as updateButton.
The control socket's `type` now reaches World Options' boxes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Lock button toggles the wanted lock; Apply with a new lock asks first
(PopupScreen "Lock World Difficulty"), Yes locks the level's difficulty
(difficulty_settings.locked, now read and written), No returns to the
options. Restrictions opens RestrictionsScreen: the chat restriction the
Chat option makes, with its way to Chat Settings, and the four chat
permissions. A locked or hardcore level keeps both the difficulty and the
lock inactive, and LAN guests are told about the lock.
Part of #464
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ItemContainerContents.addToTooltip: an item with a container (a shulker box,
or a chest Ctrl + Pick Block copied) lists its first five stacks as
"count x name" and how many more there are. Spawner.appendHoverText: a
spawner or trial spawner item names the entity its block_entity_data spawns,
else how to set one. ItemStack.OP_NBT_WARNING: for a player with game master
permission, an item carrying data of an op-only block entity type (command
blocks, lecterns, signs, spawners) warns that it might run commands.
The item argument now refuses block_entity_data without an id or with an
unknown block entity type, with vanilla's TypedEntityData messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
The control socket's key command went straight to key_pressed, so Escape
paused the game instead of closing the chat a server connection opens
(remote_chat_key handles the real keyboard).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerGamePacketListenerImpl.handleUseItem takes the client's rotation and
uses the held item, nothing else. The host ran the whole right click, with
the block the guest had looked at before: a guest that turned from an
ender chest to a double chest could get the ender chest opened again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerDetector reads level.getPlayers(): the guests count for a trial
spawner's detection and a vault's connected players, and a key a guest
inserts rewards the guest's UUID.
Part of #462
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Feedback about the player (target_name) names the guest a command runs
for. A client reading a server's chat now unwraps NbtOps' {"": value}
wrapper of any bare value, not only strings, so translation arguments that
are numbers show ("Replaced 1 slot(s)", the coordinates). horizon-ctl
messages reads a server's chat too.
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SubmitNodeCollection.submitNameTag: a name that is not discrete is drawn
in full over the world and, faded on its background, again with
RenderPipelines.TEXT_SEE_THROUGH (no depth test) after the rest of the
world; a discrete one (a crouching player) only once, faded, on its
background. The background is ARGB.color(0.25, black): alpha 0x3F.
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With "View what would be sent" open in a short window, "Send nothing" and
"Save choices" sat below the overlay's scroll range. The card now fits the
window (max height 100%): its body scrolls, and the answers row stays pinned
at its foot.
Fixes#460
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`wait` ended with release_agent_keys like every other task, so
`key w down` + `wait 40` + `wait 20` walked for the first wait only.
A wait holds nothing: it leaves keys and buttons alone. They stay held
until `key ... up`, `release` or `stop`. Documented in horizon-ctl's
key/wait help (and so the MCP tools) and docs/agent-control.md.
Fixes#469
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
GameRenderer.render3dHud draws DebugCrosshairRenderer after the hand and
the screen effects, with the hudFov projection, no bob and FogMode.NONE.
Horizon drew it among the world's on-top gizmos, so the axes bobbed with
View Bobbing.
Fixes#475
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.attack on a player: the host hits the guest on its crosshair, a guest
hits the host or another guest. The hit is worked out as the attacker
(Game::melee_damage, shared with attacking mobs) and lands on the victim as a
player_attack (blocking, armour, knockback to the guest's client), when the
pvp game rule allows it (ServerPlayer.canHarmPlayer).
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub's container menus now say where their slots live (lan::menu::Slots):
one block entity, both halves of a double chest (CompoundContainer, the right
half first) or the guest's own ender inventory (PlayerEnderChestContainer,
saved as EnderItems). The host's ender inventory stays the host's while a
guest acts. A Horizon client shows a server's chest-like menu with the rows
and title the server sent, and keeps its slots off a block that holds no such
container.
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host now runs the rest of the server's body tick for each guest:
LivingEntity.baseTick's drowning and lava, fire blocks, suffocation
(isInWall) and the void (checkBelowWorld, 4 a tick, creative too), and per
move handleMovePlayer's jump (ServerPlayer.jumpFromGround's exhaustion) and
checkMovementStatistics (sprinting, swimming). The first move of a tick now
starts from the ground state before it, so take-offs are seen. Hurt sounds
are heard at the guest; lan status lists each guest's health and food.
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #372 a guest's commands need the world's Allow Commands as well as
the LAN's; the test's survival world had none, so every guest command (tp,
gamemode, setblock) was refused and the checks built on them failed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each mob sees the nearest player (host or guest), as Level.getNearestPlayer
gives it, and keeps a guest it targets or that hurt it in the last 100 ticks.
Its hits, pushes, fire and launches on that player run with the guest swapped
in; mobs' projectiles hit guests too, and knockback reaches the guest's client
(SetEntityMotion on its own entity).
Part of #435
Co-Authored-By: Claude Opus 5.5 <[email protected]>
vanilla_world_check.py (used by the light harness) defaulted to port 25671, so two parity runs on one machine failed with 'FAILED TO BIND TO PORT'. It now picks a free port unless --port is given. The save round trip, datafix and upgrade_world tools derived their port from the pid or a random number; they ask the OS for a free one like test_dedicated_server_parity does.
Fixes#461
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A creative player with game master commands (Player.canUseGameMasterBlocks)
places command, chain and repeating command blocks and opens
CommandBlockEditScreen on them: the command, its previous output with the
O/X tracking toggle, Impulse/Chain/Repeat, Conditional and Needs
Redstone/Always Active, applied as handleSetCommandBlock does.
The block entity (CommandBlockEntity with BaseCommandBlock) saves and
loads its vanilla fields. CommandBlock.tick runs impulse blocks on a
rising signal, repeating blocks every tick while powered or always
active, and the chain in front (executeChain), honouring conditional
blocks, command_blocks_work and max_command_sequence_length. Commands
run through the command system as the block (position, facing, game
master level); their feedback becomes the block's last output and the
[@: ...] broadcast under command_block_output. Comparators read the
success count.
Fixes#444
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod's commands ran but the chat box parsed against vanilla's tree only,
so `/coords` showed red as unknown and never completed. The game now keeps
the chat's tree as vanilla's plus a literal per running mod's command (every
permission level, optional greedy `args`), rebuilt when mods start, reload
or stop.
Fixes#456
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's AbstractSliderButton keeps its own position while held and
only snaps to the option's stepped value in resetValue (on release, or
600 ms after the last change for sliders that apply late).
Fixes#385
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Generated chunks now keep the block entities Horizon does not model
(decorated pots, brushable blocks) as placed, like a loaded chunk's, so a
trial chamber pot keeps its loot table and sherds. The loot table is
rolled into the pot's item the first time it is read
(RandomizableContainer.unpackLootTable: the seed, else the table's random
sequence, else the level random), so breaking the pot drops it. Pots
render their sherds from the saved block entity, and DecoratedPotBlock's
useItemOn / useWithoutItem insert items (wobble, sounds, dust plume,
game event) or wobble and refuse.
Part of #223.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelEventHandler 3011-3021 (spawn, detect, eject, ominous and item spawner
particles and sounds, vault activation and deactivation), the trial
spawner's client tick (state particles, the spinning display mob, the
ambient sound) and the vault's client tick (idle smoke and flames, the
connection particles towards the player, the ambient sound).
Part of #223.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vaults tick in the game now, and an idle one pushes
state_updating_resumes_at to gameTime + 20 on every update, so Horizon,
which saves later than vanilla, stores a later value. Stage 3 already
treats it as volatile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vaults are block entities now (loaded from saves and structure templates,
so trial chambers keep their ominous configs) and tick every game tick:
their state changes, sounds, level events and ejected rewards reach the
world, keys go in through VaultBlock.useItemOn before the item's own use,
and VaultRenderer draws the display item spinning inside the cage.
Part of #223.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
EditWorldScreen's SystemToast goes through vanilla's ToastManager: it
slides in from the right over 600 ms (squared), shows for 5 s times the
notification display time, slides out, and plays ui.toast.in / out. The
in-game toasts' slide moves to a shared toasts module for it.
Fixes#384
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world list's search, Edit World's name and Create New World's name and
seed kept only their String and appended to it. They now edit through an
EditBox (ui::Caret keeps the focused field's cursor, selection and scroll
between frames): EditBox.keyPressed's moves and deletions (Ctrl by word,
Shift to select), Ctrl+A/C/V/X on the selection, onClick placing the
cursor under the pointer (Shift extends, a double click within 250 ms
selects the word) and onDrag selecting.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.drop swings the main hand for every drop, so throwing a
slot's item with Q over the inventory, a container or a creative tab
plays the swing (the inventory's player model shows it). Horizon only
swung for the drop key in game; Player.drop's port now swings itself.
Fixes#442
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelLoadingScreen while the player waits for its chunks: the title's
panorama, blur and menu background when a world opens (Reason.OTHER),
the nether portal sprite after a nether portal, Loading terrain... above
the 17x17 chunk status map (ChunkLoadStatusView, radius 8, COLORS) and
the 200x2 progress bar easing towards LevelLoadProgressTracker's value.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
KeyboardHandler.keyPress sends a key repeat (InputConstants.REPEAT) to
Screen.keyPressed and KeyMapping.click like a press, so holding key.drop
keeps throwing the held item, the hovered slot's item in a menu, and
the hovered item of a creative tab. Horizon dropped every repeat; the
drop key's now reach key_pressed. horizon-ctl `key KEY repeat` sends
one auto-repeat, for scripted checks.
Fixes#443
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WorldOptionsScreen in a HeaderAndFooterLayout: the title in the 33 pixel
header, the content in a ScrollableLayout over the in-world list
background between the separators, the footer buttons at h - 27.
Bold underlined category headings (FocusableTextWidget), the difficulty
button 130 wide beside its lock, Edit Game Rules... and Restrictions...,
the port EditBox always shown with its hint and error colour, and the
buttons' active states from updateButton and the update*Button methods.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pre_present_notify ties winit's RedrawRequested to the compositor's
frame callbacks on Wayland, one per refresh, so a window drew at the
monitor's rate whatever the present mode. Only Fifo (VSync) asks for
that pacing now; Mailbox and Immediate draw as fast as the frame limit
allows, like vanilla's GLFW window.
Fixes#441
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CenteredIcon draws its 15x15 sprite at x + 20/2 - 15/2 = x + 3 (integer
division), y + 3, with the widget's alpha only: Horizon drew it at +2
and tinted it grey.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WorldListEntry.extractContent dims a hovered row's icon (0xA0909090) and
draws world_list/join over it, join_highlighted under the pointer;
mouseClicked joins the world when the click lands on the icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Allow Commands is the integrated server's setting; a dedicated server's
guests run what their permission level allows (dedicated_server parity).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When no folder has the pinned version and the selected Microsoft account
owns Minecraft: Java Edition (the mcstore entitlement, asked again with the
saved token), Prepare downloads it the way other launchers do: the version
JSON from piston-meta by its pinned sha1, the client jar, this system's
libraries (the JSON's os.name rules) and the asset index with its objects
(the sounds), each checked against its sha1, written whole, 8 at a time,
into the data folder's minecraft/ in the official layout. The next
preparations find it there first. An asset that fails is a line in the log;
anything else is an error.
Not signed in, or signed in without the game, the not-found error says so as
its last line. Ownership stays required: there is no other path to the
download, and nothing of Mojang's is part of Horizon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brigadier's getCompletionSuggestions lists every child whatever its
requirement (command_parse compares it at permission 4, 2 and 0); only the
tree the server sends the client is pruned (Commands.sendCommands). The
player's chat now asks client_suggestions, which skips what the player
may not use; guests' ask_server requests keep brigadier's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The official launcher crashes on some Linux systems (rdbt's Arch), and
Horizon only read its layout. Preparing now finds the pinned version in the
chosen folder (or a folder above it, so an instance folder works), then in
every launcher's default folder on Linux (Flatpak included), macOS and
Windows:
- the official launcher: versions/<v>/<v>.jar
- Prism Launcher, MultiMC: the jar their meta/net.minecraft/<v>.json names
(a Maven name in libraries/), libraries/com/mojang/minecraft/<v>/...
- the Modrinth App: meta/versions/<v>/<v>.jar, meta/libraries
- ATLauncher, GDLauncher (data/, Next's datastore/):
libraries/net/minecraft/client/<v>/client-<v>.jar
The client jar is checked against the lock's sha1. horizon-prepare reads the
official layout and pins the version JSON's own sha1, so files found
elsewhere are linked into that layout in the data folder
(linked-minecraft/), with the pinned version JSON from piston-meta (by its
sha1, checked) when the launcher keeps only its own format or a rewrite.
Nothing else is copied or downloaded. Every preparer, old Python ones
included, reads the linked folder as an official one.
When nothing is found, the error lists the chosen folder and every installed
launcher's, with what each lacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Prepare used to stop with "Minecraft 26.3 is not in <dir>. Play 26.3 once from
the official launcher, or choose another folder." followed by an unrelated log
line ("Checking Java and your Minecraft install"). It now lists every folder it
looked in with what each one lacks (no such folder, no versions/26.3/26.3.json,
no 26.3.jar) and the two ways out, one per line. A missing client jar is caught
here too, instead of later inside horizon-prepare.
Only a preparer process's "Exited with ..." gets the last log line appended;
the launcher's own errors are full sentences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A folder holding shaders/, or a .zip, in shaderpacks/ now loads from the
Shader Packs screen: every planned begin, prepare, deferred, composite
and final pass is translated and drawn each frame over Horizon's world,
with Iris's formats, clears and buffer flips.
The targets keep OpenGL's layout (the translator flips gl_Position and
remaps its depth), the world enters as OpenGL depth, and the pack gets
OpenGL matrices. Textures share four samplers by kind (Metal allows 16
per stage), and isnan/isinf test bits (Metal's fast math folds x != x).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GitHub's releases page sorts a day's tags as text, so v2026.10.15.9 showed
above v2026.10.15.18 (reported by rdbt on Discord). Tags now count .01, .02...
Every reader (release_train.tag_key, the launcher's compare_versions) compares
the parts as integers, so older unpadded tags still order correctly.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Delete opens JoinMultiplayerScreen's ConfirmScreen (title, wrapped
warning, Delete and Cancel; Escape answers no) and only removes the
server on yes. Direct Connection puts its label at y 100 and the box at
116 like DirectJoinServerScreen, and Add/Edit Server its boxes at 66 and
106 like ManageServerScreen, labels one pixel in.
Fixes#380, #381
Co-Authored-By: Claude Opus 5.5 <[email protected]>
OnlineServerEntry draws the server's 64x64 favicon (from servers.dat, then
from each ping, saved back like updateServerList), the description as a
styled component with legacy codes, formatPlayerCount's dark grey with
grey numbers, and on hover the join and move arrows over the icon. The
icon's right half joins, its left quarters move the server up or down.
Fixes#365, #378, #370
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The begin, prepare, deferred, composite and final passes of a program
folder: what each writes (the later of DRAWBUFFERS and RENDERTARGETS),
which side of every ping-ponged colour target it reads (Iris's
BufferFlipper, with flip.* overrides), mipmapped targets, scale.*, and
the targets' formats, clearing and sizes from the consts of every pass
in Iris's order. The preprocessor now keeps the directive comments of
active code, as Iris's does, so a DRAWBUFFERS in an #if'd out branch
doesn't count.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Java .properties parsing, the two reads Iris makes (the raw file for
screens, sliders and profiles; the preprocessed one, with enabled options
and the environment's macros defined, for everything else), option
screens, profiles and the current one, program.*.enabled through a port
of Iris's BooleanParser, image formats for shadowcomp's writeonly
images, and blend overrides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The legaliser of docs/shader-packs.md step 1, on a syntax tree (glsl-lang)
instead of the spike's tokens: attribute/varying to located in/out with
matrix and array varyings split, loose uniforms in one std140 block that
extends across a pass, combined samplers split into texture + sampler
bindings (through function parameters, respecting locals that hide them),
compatibility built-ins and Iris's core-profile names declared, legacy
texture and shadow calls, hoisted global initialisers, sized unsized
arrays, isnan/isinf/modf helpers, and dead code after a jump dropped (the
spike's 'naga constructor bug' in BSL was code after discard). naga then
parses, validates and writes WGSL. The programs of one pass share a Link
so locations, bindings and the uniform layout agree.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new crate for the OptiFine/Iris translation (docs/shader-packs.md, order
step 1, first half): open a pack folder or zip (shaders/ found as Iris finds
it), list its programs per dimension folder, build the #include graph,
discover options per include component (OptionAnnotatedSource rules: boolean
defines referenced by #ifdef, value defines and configurable consts with a
[...] list, conflicting defaults dropped), write the player's values into
the lines, and expand Iris's standard macros with pp-rs, keeping line
numbers so errors name file:line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The default debug profile enables 3d_crosshair while the overlay is open:
GameRenderer draws DebugCrosshairRenderer's axes one block ahead of the
camera (0.01 x GUI scale long; X red, Y green, Z light blue, 2 px over a
4 px black line) and Hud.extractCrosshair skips the 2D crosshair and its
attack indicator. Hidden with reduced debug info, in third person and
with F1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The overlay is drawn after screens and toasts, so it covers menus, and on
the title and options screens too (Gui.extractRenderState); F1 hides it
only in game. It lays out at the debug GUI scale (debugGuiScale, default
half the largest automatic scale) and shows the default profile's entries
sorted by id, split into vanilla's two columns: fps, version, position,
section position, performance impactors, system specs (CPU, display,
window, GPU, backend) and the integrated server's tick time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GraphicsPreset.apply also sets render distance (8/16/32), simulation
distance (6/12/12), chunk update priority, smooth lighting and menu blur,
and its cloud range (32/64/128) and Fabulous entity distance (1.25)
differ from what Horizon set. Dragging the classic preset slider only
moved the label: the preset now applies whenever its value changes
(Options.applyGraphicsPreset), in both menu styles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The single-player owner was always at permission level 4. Like
IntegratedServer.getProfilePermissions, the owner now gets OWNER (4)
only when the world allows commands, else ALL (0): /gamemode, /give and
friends answer "Unknown or incomplete command" and are not suggested
(the client's tree only holds usable nodes, Commands.sendCommands).
allowCommands is read from level.dat, set by World Options, and worlds
made without the Create New World screen take its default (Creative only).
The server console, the command harness and horizon-ctl `command` keep
the owner's level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Save and Quit to Title kept boats, minecarts, armor stands, item frames,
paintings and other non-mob entities in memory. Opening the world again
loaded them from disk a second time and the next save wrote both copies,
so each reopen doubled them until the game crawled. Closing and opening a
world now let go of every entity of the level (MinecraftServer.stopServer
unloads them with the level). tools/test_world_reopen_parity.py reopens a
world three times and checks its minecarts stay the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host drew each guest with the host's own armour (player_armor_layers),
and a guest ignored ClientboundSetEquipmentPacket, so it drew the host and
other guests bare-handed and without armour. Player models now carry what
they wear in extra.head..feet: the host fills it from the guest's armour,
the guest from SetEquipment (main and off hand too, for mobs as well), and
both draw HumanoidArmorLayer from it. The host also draws what a guest holds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The player model (third person, the inventory's preview) was only ticked by
the single-player tick; on a server it was never created.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two Horizon games under horizon-ctl, each with its own scratch home: the
host opens a survival world to LAN with commands, the guest joins by
address, and each check (tab, self, armour, nametags, chest, crafting,
damage) is reported on its own with screenshots of both sides.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest could not craft: the host's inventory menu for guests had no
crafting slots (0-4 were always empty), and a crafting table the guest
used switched the host's own grid to 3x3 and opened nothing for the guest.
The guest's inventory menu (window 0) and a crafting table window
(minecraft:crafting) now run the host's vanilla-checked menu engine
(horizon_client::menu, Kind::Inventory / Kind::Crafting) over the guest's
inventory, kept between clicks so drags work, with the usual click
corrections. The 2x2 grid and its result are real slots of the guest's
inventory menu, so the slot sync carries them. Closing a menu puts its grid
back into the inventory (dropped if full); a guest leaving keeps it too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
No player had a name tag: only mobs with a custom name drew one. The other
players now get theirs on both sides (the guests on the host, the players a
server sends on a client), shown as LivingEntityRenderer.shouldShowName
allows: no custom name or crosshair needed, a crouching player's only within
32 blocks, team visibility and colours by player name, the below-name score.
The host also joined its own LAN world as "Player": the guests' player
list, name tags and chat, the MOTD and the offline UUID now use the host's
profile name, as IntegratedServer does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Nothing hurt a guest: the host only set a guest's position from its move
packets, so a fall did nothing, and their body never ticked on the server
(no damage clock, no regeneration, no food tick).
Each tick the host now runs a guest's server-side body like ServerPlayer:
the damage clock and Entity.baseTick's fluid phase, then
ServerPlayer.doCheckFallDamage for every move packet accepted that tick
(the supporting block, the fall distance and the landing, through the same
landing code as the host's own falls, so water resets a fall and lava
halves it), then peaceful regeneration and the food tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A chest a guest opened opened on the host only, and never closed: the
host ran the guest's open as its own player's (one opener, a local block
event), nothing was sent to the guests, and the guest's close was not
counted. Chests now count their openers like ContainerOpenersCounter (the
host and each guest; a guest's close, a menu it replaces and its leaving
take it off) and the block event goes to the guests too (shulker boxes as
well). A guest's client handles ClientboundBlockEventPacket and ticks its
block entities (ClientLevel.tickBlockEntities, without the server copy's
bell sound and gateway ticks), so lids move on its screen.
The control socket's block reply gives a chest's or shulker box's lid
openness (lid, 0..1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In singleplayer the middle button did nothing outside creative, and in
creative it overwrote the selected hotbar slot. It now runs the server's
handlePickItemFromBlock (tryPickItem): a stack of the block's item
already in the hotbar is selected; one in the main inventory is swapped
into getSuitableHotbarSlot (the first empty hotbar slot from the selected
one, else the first unenchanted); only creative adds the item when none
is there, moving what the slot held to a free slot. Servers already got
the packet and do this themselves.
Fixes#367.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GuiGraphicsExtractor.textHighlight paints an inverting white fill, then
adds pure blue (GUI_TEXT_HIGHLIGHT, additive). Horizon only inverted, so
selections lacked the blue: black text on a light box, not blue text.
The anvil's name box skips the inversion (setInvertHighlightedTextColor
false), as in AnvilScreen.
The creative search and anvil name box now draw like EditBox: the cursor
blinks from when the field took the focus (300 ms on, 300 ms off), turns
into a bar once the field is full, and Select All highlights the value.
Sign lines draw their Select All highlight too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Extras page's Inventory section gains "Sort buttons" (sort, off) and
"Sort key" (sortKey, unbound). A small button right of a chest, barrel,
ender chest or shulker box title sorts the container; one right of the
"Inventory" line sorts the main inventory (the hotbar stays). The key
sorts the part under the cursor.
inv_sort::plan merges partial stacks, then orders the stacks by creative
tab order (the search tab's), then name, full stacks first, using only
left PICKUP clicks with an empty hand: the game sends them as ordinary
clicks, so vanilla servers accept them. A unit test replays the plans
through the menu engine on 40 random chests.
The Extras page binds any extra key the way it bound the zoom key
(Item::key_name, Extras::set_key).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Extras page gains an Inventory section with "Mouse Tweaks controls"
(mouseTweaks, off) and "Scroll wheel moves items" (mouseTweaksWheel). In
the player inventory and chest-like containers, locally and on servers:
- right-drag holding a stack puts one item in each slot entered, at once;
- left-drag holding a stack gathers the slots of the same item into the
hand, while vanilla's drag covers one slot (a drag over empty slots
still splits as vanilla does); with Shift they are quick-moved;
- Shift + left-drag with an empty hand quick-moves every slot entered;
- the wheel over a slot moves one item to the other side (down) or
brings one back (up).
Every action is ordinary slot clicks: the menu engine's, or ContainerClick
packets for any slot (remote_slot_click, factored out of remote_click).
With the extra off each hook returns at once.
horizon-ctl gains drag-at and scroll-at, which send the window events of
a mouse drag and of wheel steps, to test these and vanilla's drags.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreativeModeInventoryScreen.slotClicked moves the search box cursor to
the end and sets the highlight to 0 on any click of a display slot, so
the next typed character replaces the whole query. Horizon kept the
query and appended to it.
horizon-ctl's type now edits the creative search as typing does (the
selection and the 50 character limit apply) instead of appending.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
While a held stack is dragged over slots, each dragged slot shows the
stack it would get over a white highlight, its count yellow when the slot
caps it, and the cursor shows what stays in hand (extractSlot,
recalculateQuickCraftRemaining, extractCarriedItem); a drag over a single
slot hides that slot's item. Slots join the drag by
shouldAddSlotToQuickCraft (same item or empty, placeable, canDragTo, and
not more slots than carried items for a right drag), so the preview counts
the slots the menu will fill.
The menu engine computes both (Menu::quick_craft_accepts and
quick_craft_preview) for the inventory, crafting table, chests, hopper,
dispenser, shulker box, furnaces, crafter, loom, trading and creative
screens; the slot and carried-stack drawing of those screens goes through
draw_slot_item and draw_carried.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
The item tabs handled clicks on press with their own rules: Shift sent the
item to the inventory, the hotbar row swapped with the carried stack, a
carried stack clicked on the tab was always deleted, and there were no
drags. They now go through the same mouseClicked/mouseReleased as the other
container screens, over ItemPickerMenu:
- display slots: slotClicked's CONTAINER branch (creative::click_display):
an empty hand takes the shown stack (a full one with Shift), the same item
grows by one, fills with Shift or shrinks with a right click, another item
is deleted or loses one; number keys fill a hotbar slot, Q drops copies;
- the hotbar row: the menu engine's new ItemPicker kind, whose
quickMoveStack deletes the stack (Shift-click on the hotbar from an item
tab), and whose display slots take no drags or PICKUP_ALL.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tests pin player 1 (0x04), the default blue (0, 0, 128) and the instant
bit (0x20) to hid-playstation.c and SDL; the docs say what Horizon resets
on quit (more than SDL or the kernel do) and that the mute LED is never
touched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The pad's audio device is also found by Windows' "Wireless Controller"
endpoint name (the four channels rule out a DualShock 4); a name test.
- docs/controllers.md: the haptics need reports without the rumble flags
(SDL, nondebug/dualsense, dse-haptics), which Horizon now always sends while
they are open; Windows needs the Quadraphonic speaker setup; on Linux ALSA
can't open the card while PipeWire holds it; Bluetooth haptics (report
0x32, SAxense) are documented and not done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A loaded crossbow and the boosts (rocket, pig, strider) get a weapon
catch that breaks in the zone where the trigger threshold presses, so the
click is the shot (the boost was a feedback wall that never gave way).
- Draws start at zone 2 and stop at 6 of 8 (held for as long as you like).
- A shield in the use hand weighs (feedback 2, 3); disabled by an axe, the
trigger locks (feedback 1, 8), as Deathloop does for a jammed gun.
- Mining buzzes slower on harder blocks (40/30/20/10 Hz).
- A 0 Hz vibration frees the trigger (Nielk1's factory); tests pin the
bytes to DSX++'s and dualsensectl's examples.
- docs/controllers.md: the table with the effect of each action and why.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Every report carries the motors; still motors send no rumble flags, as SDL
stops rumble (a report without them ends the emulation and re-arms the
audio haptics), so a trigger change no longer cuts a vibration.
- Legacy vibration at half strength (SDL); the Edge always takes V2; an
unreadable firmware version falls back to legacy.
- The first report after opening clears the motor/trigger attenuation
(flags 1 bit 6, byte 36; dualsensectl, pydualsense).
- The LED reset goes in a report of its own before any colour
(hid-playstation's dualsense_reset_leds); over Bluetooth the colour waits
for the connection animation (SDL's sensor clock check).
- Bluetooth output only where gilrs keeps reading the pad (Linux), else with
HORIZON_DUALSENSE_BT=1: any output switches the pad to its full report.
- USB is preferred when the pad is on both.
- Tests pin the reset report and its CRC (zlib), the flags and the clock offset.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The lightbar shows the player's health (green to amber to red) or the
dimension, at half brightness, chosen in the controller options (Lightbar:
Health / Dimension / Off; health by default). The player LEDs show player 1.
When Horizon lets go of the pad, the lights it changed go back to a host's
defaults.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each rumble event plays a short shape instead of one flat block: a hit
starts at full strength and dies away, an explosion rolls on, a totem
swells, a broken block is a tick. They drive the motors' emulation in the
HID report.
On USB the pad is also a four-channel audio device whose channels 3 and 4
drive its actuators: with HORIZON_DUALSENSE_HAPTICS=1 the shapes play there
as waveforms (80 Hz and 200 Hz tones for the heavy and light parts),
channels 1 and 2 silent, until that path is confirmed on a real pad.
docs/controllers.md says what each OS and transport allows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each trigger feels the action bound to it: resistance growing while a bow
draws, a trident charges or a crossbow loads, a gun trigger's click on a
loaded crossbow, a wall where pulling boosts (a rocket while gliding, the
stick of a ridden pig or strider), and a light buzz while mining, scaled by
the block's hardness. Free in menus, paused, unfocused, on another pad,
with controllers off, and on quit. On by default, "Adaptive Triggers" in
the controller options turns them off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- First run (and when CONSENT_VERSION changes): a question over the launcher
with four switches, all off, what each sends in plain words, what is never
sent, "View what would be sent" (the exact JSON), and "Send nothing" /
"Save choices".
- Settings → Data collection: the same switches any time, the install ID and
what waits, "View what would be sent" and "Delete my data" (server deletion,
outbox cleared, new install ID).
- The outbox is sent at start and on close (at most 3 s), only when there is a
server (none yet) and only what the player allows.
- CLI `horizon-launcher data status|allow|deny|show|delete|send|bug|benchmark`
and MCP tools data_collection, set_data_collection, data_preview,
delete_my_data, report_bug. Bug reports and benchmark uploads are printed for
the player to confirm; logs are scrubbed and lose chat lines.
- horizon-telemetry joins the launcher's paths (launcher.yml, release_train,
pr_ready).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The horizon-dualsense crate builds the DualSense's output report (USB 0x02,
Bluetooth 0x31 with its CRC-32 seeded with 0xA2) and writes it with hidapi
from a thread. The game sends its rumble pulses through it when the active
pad is a DualSense, so vibration now works on macOS too, and falls back to
gilrs force feedback when the pad can't be opened. Quitting frees the
triggers and stops the motors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SpriteContents.AnimationState.drawToAtlas mixes an animation with
"interpolate": true towards its next frame every tick
(animate_sprite_interpolate.fsh: mix(current, next, subFrame / time),
per mip level, into the 8-bit atlas). Horizon stepped the frames: the
sprite upload now mixes the two frames' mip chains the same way and
re-uploads every tick while the mix changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The design sections now describe the implementation: ids for rows,
switches and segments, the redraw wait, wait-for --since and event KIND,
the events log (subscribe comes next), the launcher proxying through
horizon-ctl mcp. A before/after table and the PRs of each step replace
the plan.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The workstation screens (anvil, grindstone, stonecutter, smithing,
enchanting, cartography, loom, beacon, villager trading, brewing stand),
the horse/llama/camel inventory and the creative item grid read Shift from
the sneak mapping (`keys`). Behind a screen a Shift press never reaches the
mapping (KeyboardHandler.keyPress: only releases do) and Right Shift isn't
mapped at all, so shift-click there worked only when Shift was already held
before the screen opened. They now read `has_shift_down` like
AbstractContainerScreen (`InputWithModifiers.hasShiftDown`), and the recipe
book reads Ctrl the same way (`Minecraft.hasControlDown`).
`status` reports `physical_keys` so scripts can check what the OS delivered.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Shader Pack row cycles Off and every folder in shaderpacks/; the game
loads the pick on the next frame. Not part of the presets, off by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Converting a proto chunk for saving (VanillaTerrain::saved_proto copies it
block by block into a column, then encodes it) ran on the main thread: in
Streaming::save during the autosave, and in Streaming::update for every far
proto unloaded while exploring. The protos are now checked out (like a
decoration job does) and saved by a worker, ahead of every other job, then
handed back. The explicit save and quitting still save them in place, after
waiting for the saves in flight; dropping the streamer waits for them too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A light job for a chunk that was already lit (a neighbour loaded, or a block
changed in an unlit neighbour) dirtied every section of the 3x3 columns
around it: 216 remeshes, whether or not any light changed. It now writes only
the sections whose light changed and dirties the sections around the changed
positions, as the incremental relight already does. A chunk lit for the
first time still dirties its 3x3 neighbourhood.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameLevel::heightmap found getHeight by testing every block from the top
of the world down (with a block tag lookup each), for every spawnable chunk
every tick: a quarter of the main thread in a village. Sections holding
only air are now passed over whole; air counts for no heightmap, so the
height is the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With shaderPack set in horizon-graphics.txt, the world renders into
world_post's target with its depth kept, each composite stage of the
pack (gfx/pack_stages.rs) draws a full-screen pass reading the scene
colour and depth, the previous stage and the Frame uniform (camera,
matrices, time, sun and moon, sky and fog colours and distances,
weather, camera state, dimension), and the last output goes to the
screen through the usual copy/FXAA, under the hand, screen effects and
HUD. The game loads the pack from shaderpacks/, checks its files twice
a second and on F3 + T, and keeps the last good pack on errors, which
go to chat. Without a pack nothing changes: no target, no extra pass,
depth discarded as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
shader_pack.rs reads shaderpacks/<pack>/pack.toml, the player's option
values (shaderpacks/<pack>.txt) and each composite stage's WGSL, puts
the api 1 prelude (bindings, the Frame uniform, depth helpers) before
it and has naga parse and validate it, with errors pointing into the
pack's own file. Options are WGSL override constants. A file stamp
tells when the pack was edited, for hot reload. Nothing draws with it
yet. examples/shaderpacks/horizon-sample is the MIT sample pack.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wait-for screen NAME|widget ID|loaded|chat TEXT|event KIND replies once
the game gets there (--timeout, default 30 s, ends in an error of kind
timeout; --since SEQ counts chat and events from an earlier point), so
scripts and agents stop polling. events [--since SEQ] lists what
happened, oldest first, numbered: screens, chat lines, deaths, respawns,
items gained or lost, dimensions, worlds opened and closed. The state is
compared once per event loop turn and only under --control, so watching
never changes the game. respawn now says when the death screen's button
is not active yet instead of clicking it to no effect.
horizon-ctl and its MCP gain both; tools/agent_control_e2e.py gains an
events scenario.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
options-get [KEY...] gives every options.txt option by its vanilla key
with its value and what it accepts (type, range, step or choices; enums
by name, key bindings as key.keyboard.w). options-set KEY VALUE sets one,
applies it at once and saves options.txt as the options screen does
(language and resource packs reload too); a value the option refuses is
an error naming what it accepts, and the option keeps its value. After a
set, ui reads the redrawn screen.
horizon-ctl and its MCP gain both; tools/agent_control_e2e.py gains an
options scenario.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Without an options.txt the game ran unlimited (260), while an options.txt
without maxFps, like vanilla, gives 120. With VSync off (or rendering
offscreen) that kept a core busy drawing frames nobody sees.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An agent now needs one MCP server. horizon-launcher mcp runs the selected
version's horizon-ctl mcp as a child and offers its tools as game_<name>
(all but launch, which launch_client covers), passing calls and image
results through. launch_client starts the game with a control socket in
the launcher's data folder, takes world, creative and offscreen, and
returns once the socket answers. A game tool called while no game listens
says to call launch_client.
tools/launcher_mcp_e2e.py checks it end to end against this checkout's
builds in a scratch launcher home.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Discord server banner (page + PNG), the server icon render and Kira's avatar (SVG master +
render) now live in the private kira repo (ludovic111/kira#1). xtask icons no longer renders them;
the brand README points there, and CONTRIBUTING states what belongs in this repository.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ui` lists every widget the last frame drew (title menus, pause, options
in both menu styles, inventories and menus) with an id, its kind, label,
bounds and value; `click ID` clicks one through the same window events
as the mouse, `set ID VALUE` moves a slider along its track. Ids come
from labels (FOV: 70 is fov), world/server/language rows carry their
names, Horizon switches, sliders and segments their row's caption, and
inventory slots are slot-ID with their stacks. After a click, the next
widget command waits for a redrawn frame.
horizon-ctl and its MCP gain ui, click and set, and launch --offscreen.
tools/agent_control_e2e.py gains a ui scenario and runs offscreen on
macOS; both scenarios now handle Singleplayer opening Create New World
when there are no worlds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/mods.md: the sandboxed WebAssembly runtime, capabilities, hooks, budgets,
authoring, versions, multiplayer and distribution, with measured hook costs.
docs/research/java-mod-compat.md: why Fabric/NeoForge jars cannot run, data
import, which mods to port natively first, a porting guide outline.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Manifest and .hmod reading, capability checks against the component's imports,
fuel and memory limits per call, a trapping mod stopped alone, hot reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-mod-sdk wraps wit-bindgen: a Mod trait with default hooks and export_mod!.
coords-hud shows coordinates, facing and the time of day, with /where and /coords.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/port_report.py turns a diff into work items: rank 1 blocks the switch (facts,
packets, DataFixers, data component types, report formats, tables to regenerate),
rank 2 and 3 one item per Horizon file that ports changed vanilla code (with or
without a harness), rank 4 new content, rank 5 summaries. version_port.py report
writes report.md and report.json; port prepares, diffs and reports in one go.
The class map now resolves names of both versions. The doc records the 26.2 -> 26.3
replay against Horizon's own code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/class_index.py hashes every method's bytecode with constant-pool references
resolved and branch targets as instruction indices, lambdas folded into their
declaring method; class diffs list moved classes. tools/vanilla_map.py maps vanilla
classes to the Rust files whose comments name them and the harnesses whose oracles
or probes reach them. version_port.py prepare indexes, diff reports both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
detect reads Mojang's manifest; prepare downloads and verifies a version into the
version store (outside the repository), runs its data generator and reads the
jar's own version facts; diff compares facts, packets, registries, blocks, item
components, commands, data packs, assets and lang.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What the launcher, the game socket, horizon-ctl and the two MCP servers
can do today, and one model for what comes next: shared names and JSON
shapes, a widget tree with actions by id, options get/set, waits, an
event stream, the launcher's MCP proxying the game, errors with a kind,
and the safety rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Trial spawner block entities are kept (generated chunks and saves), tick on
the server with the level random and random sequences, spawn persistent
mobs, eject their rewards as items and set their block state, with their
level events and shutter sounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Scraping and waxing off copper already swing the arm, show the scrape and
wax-off particles and play the axe sound on main (#59, #104). Two gaps were
left in BlockTransformer.transformBlock:
- playerHasBlockingItemUseIntent: with a shield in the off hand, right-clicking
with an axe raises the shield instead of scraping, unless sneaking.
- The block_change game event, which sculk sensors pick up.
Fixes#222
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FirstPersonHandsAndItemsRenderer draws both hands: the off-hand item sits
on the other arm with its own equip height and swing, uses its own pose
when it is the hand in use (eating, blocking, drawing a bow), and
evaluateWhichHandsToRender hides a hand while a bow or crossbow is drawn
or a charged crossbow is held in the main hand. Only the main hand's empty
arm shows, as in vanilla.
Part of #276
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Player.getProjectile: what the weapon supports in the off hand, then in
the main hand (getHeldProjectile; crossbows also take firework rockets
there), else the first arrows in the inventory. Crossbows only looked
for arrows in the inventory, so a rocket in the off hand was never
loaded. A charged rocket is shot at an angle from 0.15 under the eyes
(CrossbowItem.createProjectile), every shot of a load holding one at
power 1.6 (getShootingPower), for three durability (getDurabilityUse).
Part of #276
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
LivingEntity.hurtServer's playHurtSound goes through Player.playSound,
which leaves the player itself out; the player hears its hurt sound from
the client's handleDamageEvent, and a blocked hit sends BlocksAttacks'
block sound (onBlocked) instead of the damage event. Queue the local hurt
sound on the damage broadcast rather than on the server's hurt sound.
Part of #277
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The fishing rod switches to its cast model while its hook is out
(FishingRodCast), in first person, third person and the hotbar.
- FishingHook.catchingFish's particles: splashes where fish tease the bait,
the fishing-particle trail and bubbles of a fish swimming up, and the
bubble and fishing burst when it bites.
- FishingHookRenderer: the line is 16 black `lines` segments of the window's
line width (2.5 px at 1080p) from a quarter block above the hook to the
rod hand (on the near plane in first person, beside the body in third),
and the bobber is centred on the hook.
Fixes#265
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
DebugScreenOverlay.toggleLightmapTexture and showLightmapTexture: the 16x16
lightmap at 64x64 on a black border, 2 pixels from the corner, sky light up
and block light right, as vanilla's flipped blit. It shares the bottom of the
screen with the F3+2 and F3+3 charts, so each switch turns the others off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreateWorldScreen as in 26.3: a MenuTabBar (tab sprites, header
separators, underline) over the transparent tab header, each tab's
GridLayout aligned at (0.5, 1/6) between it and a 33-pixel footer with
Create New World and Cancel.
Game: the world name box with its save-folder tooltip, Game Mode
(Survival, Hardcore, Creative), Difficulty and Allow Commands, locked
in Hardcore. World: World Type (Default, Superflat), Customize, the seed
box with its hint, and the Generate Structures / Bonus Chest switches.
More: Game Rules, Experiments and Data Packs. Tooltips follow vanilla's.
The choices reach the new world: hardcore, difficulty and allowCommands
in level.dat, and world_gen_settings with the flat preset, structures and
bonus chest written before streaming starts. Re-Create reads them back.
Fixes#281
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DebugScreenOverlay's FpsDebugChart, TpsDebugChart, PingDebugChart and
BandwidthDebugChart with vanilla's scales, colours and places: 240 samples
(LocalSampleLogger), F3+2 and F3+3 toggles that show the overlay and exclude
each other, the overlay's "Debug charts" lines, and the play-state ping
request and pong response packets the ping chart uses on servers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SelectWorldScreen's list now sits at vanilla's 49..h-60 with the list
background, header and footer separators, white selection outline and
the widget/scroller sprites at the row's right + 8. The scroller follows
the pointer while held (AbstractScrollArea.mouseDragged), the wheel moves
half a row, and the pack screen's lists can be dragged the same way.
Part of #282
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Changes to horizon-core or horizon-world make ~280-300 harnesses rerun, which takes
60-90 min on the Mac mini runners; at 45 min those runs were cancelled and proved nothing
(#269, #280, #292 today).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
JoinMultiplayerScreen's layout: the title centred in the 33-pixel header, the
ServerSelectionList between the separators (list background, 305-wide rows,
white selection outline, scissored rows, the scroller sprites), the footer
buttons centred in 60 pixels. The list always ends with the LAN header,
"Scanning for games on your local network" over LoadingDotsText's dots,
then the LAN games. Saved servers show two MOTD lines, the player count and
the ping, pinging or unreachable icon; Edit and Delete need a saved server.
The list scrolls with the wheel and a draggable scrollbar (ui::Scroll, after
AbstractScrollArea).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The autosave queued its chunks without waiting (#284), then saved the
entities with the waiting call. The region writer is one FIFO thread,
so the main thread still waited for every chunk queued before the
entities: a 1.9 s frame at the 5-minute autosave on a world with ~900
columns. Like PersistentEntitySectionManager.autoSave (saveAll's flush
only on save-all and quit), the autosave now queues the entities too.
Part of #224
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Screen.extractBlurredBackground: every classic screen reached from the title
(world selection and creation, multiplayer, options) blurs the panorama with
vanilla's minecraft:blur post chain, three horizontal and vertical box blur
passes of menuBackgroundBlurriness pixels, then draws menu_background over it
instead of a flat dark fill. The title screen itself stays sharp.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FireworkEntityRenderer submits the rocket's item in the GROUND context,
turned to the camera (and over on its side when shot at an angle), at the
packed light of its eye. Horizon drew a flat 0.5-block sprite at full
brightness, so rockets glowed at night and had no item shading.
Fixes#279
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options.cutoutLeaves, ambientOcclusion and biomeBlendRadius call
LevelExtractor.allChanged when they change. Horizon only handed the new
value to the mesher, so after turning See-Through Leaves off the loaded
sections kept their see-through leaves, pale against the sky, beside the
opaque ones of sections rebuilt later.
Fixes#273
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3's Projection passes its planes reversed and every pipeline
tests GREATER_THAN_OR_EQUAL. Horizon kept forward depth with a 0.05 near
plane, so far geometry lost precision and fire_side's quad, 0.01 pixel in
front of the block it burns on, z-fought with it.
Fixes#272
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
RecipeToast keeps the station and result of each display unlocked while it
is up, draws the station at 0.6 scale in the plaque's corner over the result,
takes turns through them over the display time and stays up five more seconds
after each unlock. Toasts slide in and out on ToastInstance's squared curve,
honour notificationDisplayTime and play ui.toast.in / ui.toast.out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Bats fell into the generic fallback AI and walked like land animals; their
animation states were never started. Port vanilla Bat: hanging from a solid
block until a player comes within 4 blocks or the block goes, the take-off
level event, customServerAiStep's flight towards random nearby air, Bat.tick
(held to the ceiling while resting, vertical speed scaled by 0.6 in flight),
setupAnimationStates (flying and resting keyframes), waking when hurt,
BatFlags in the save, no fall damage, movement events without sounds, the
lower voice pitch, and no omnidirectional air drag (vanilla gives that only
to bees, parrots and sulfur cubes).
Mob AI parity harness: six bat scenarios, 6/6 match (1906/1906 mob-ticks).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Striders fell into the generic fallback AI, whose FloatGoal jumped them
up and down in lava. Port vanilla Strider: its goals (no FloatGoal,
StriderGoToLavaGoal, RandomStrollGoal every 60 ticks, LookAtPlayerGoal for
striders), floatStrider, standing on lava sources through the living
collision context and the half-block liquid shape, travelling in air where
lava holds it up, StriderPathNavigation's lava destinations, the walk target
value, going cold (suffocating speed modifier, cold texture) away from warm
blocks, the happy/retreat sounds and a lava-proof checkFallDamage.
Mobs now keep Entity.mainSupportingBlockPos from their last move, as
getBlockStateOnLegacy reads it.
Mob AI parity harness: five strider scenarios, 5/5 match (1105/1105
mob-ticks; 0/5 and 5/1105 before).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mob.readAdditionalSaveData assigns persistenceRequired from the tag,
overriding the constructor's setPersistenceRequired (copper golems,
elder guardians). Horizon ORed it in, so a copper golem vanilla saved
with 0 came back with 1. The monument's elder guardian, which Horizon
builds from NBT where vanilla calls the constructor, now carries the
flag in its spawn NBT.
Fixes#289
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Generation asked for columns within the view distance + 3 or two of a
finished chunk, but a column that came back finished (loaded from the
save) was kept only within + 2 or one of a finished chunk. After a
teleport, saved columns in between were loaded, dropped, asked for and
loaded again on every frame, forever: 8 of them at render distance 16
held 8 of the 39 in-flight generation slots and kept the disk and a
worker busy. The requested, kept and unloaded ranges now live in Ranges
and nest; requested_columns_stay_loaded checks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On macOS the Horizon client renders offscreen (--offscreen) with audio off,
from the logged-in session; Linux keeps vscreen and the null ALSA device.
The vanilla side was already a headless dedicated server. --bin-dir is
resolved, since the client runs from the run folder. parity.py no longer
skips save_roundtrip off Linux; it stays local-only (no rendering over SSH).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new chunk relights its eight neighbours, and every finished light job
dirties all sections of the 3x3 columns around it. Meshing waited only for
the column itself to be lit, so sections near the loading frontier were
meshed with half-settled light and again after each neighbour's relight:
about 2.25 meshes per section while a world loads. Wait until no column of the
3x3 is being lit or queued for light. The final meshes are the same; they are
built once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Same rule as perf_bench: with status.streaming, the last sections are drawn
once no stage has held work for half a second, so the remeshing that
follows a load (fluids settling, random ticks) isn't counted as loading.
Without it, meshing still has to go quiet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Builds that report status.streaming are loaded once chunk counts are
stable and no stage (generation, decoration, light, meshing) has held work
for half a second. Meshing going quiet also waited through the remeshing
that follows a load (fluids settling, random ticks: 1500 falling to 60
meshes/s for ~150 s at RD16), which is simulation, not loading. Older
builds keep the meshing-rate rule.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sections wait for their neighbours' light before they are meshed, so the
chunk counts settled while edge sections were still undrawn.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each game runs --repeats times (3) per render distance and writes one v1
report per distance (benchmarks/perf/<label>-rd<N>.json): figures as means
with standard deviation and 95% CI, per-flight frame entries with the fixed
histogram, the hardware without the host name. frames() and summary() match
crates/horizon-bench value for value (tested on the same inputs).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each session rewrites benchmarks/launcher-<time>-session.json in the data
folder (report format v1): start to first frame, release list fetch,
install/update and prepare durations, Play to the game's first frame (the
game now logs its first frame), RSS, UI frame times, binary size.
horizon-launcher benchmark starts the launcher N times (cold, then warm),
switches pages, measures idle CPU and memory and the release list fetch,
and writes one report with means and 95% CIs. Nothing is uploaded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One versioned JSON report for every benchmark Horizon writes (the game's
--benchmark, the launcher's measurements, tools/perf_bench.py): the local log
a player can attach and, with consent, what the telemetry upload sends.
Summaries over repeats (mean, stdev, 95% CI) and fixed-edge frame-time
histograms, never raw frame streams; no host, user or path in uploads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
The mesh loop dropped every dirty section of a loaded column outside the
render distance. Columns load up to three chunks past it, and a column
loaded from a save with its light runs no light job, so nothing asked for
its sections again: once the player came closer it stayed invisible until
a block in it changed. Those sections now wait and are put back when the
centre, radius or forced chunks change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`status` gains `streaming` (queued jobs, chunks generating, protos waiting for
decoration, decorating, finishing, lighting, meshing) and `dirty_light`, so a
slow load shows which stage it waits on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla trims each line and drops only the hidden splash (hash 125780783);
a blank line stays a (blank) splash.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PalettedContainer::pack hashed all 4096 values with SipHash to build the
palette. It now shares from_values' scan, which only switches to a map
past 16 distinct values. Same palette order, same output.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every mesh job cloned the whole FaceVisibility (every state, every face
VoxelShape and the occlusion cache) and dropped it afterwards: about half
of the workers' busy time while a world loads. Each worker now keeps its
copy, like vanilla's per-thread occlusion cache, and clones a new one only
when the mesh tables change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Sharing no longer needs a relay. The panel shows the full invite code in
groups (monospace, wrapped) with Copy and Stop sharing, then who can join:
friends anywhere when the router opened the port or this computer has a
public address; otherwise what to do (UPnP, the port to forward and the
LAN address, a relay, a VPN; CGNAT called out) with a link to the
troubleshooting docs. A relay set in horizon.txt (friendsRelay) or
HORIZON_RELAY_URL adds its short code. horizon.txt keeps other lines when
the menu style is saved.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A read timeout restarts with every byte, so a peer trickling one byte at
a time could hold the host's 8 handshake slots (or relay threads) for
hours. The host's openings and the relay's first line now end after 10 s
in all; the relay closes connections past RELAY_MAX_CONNECTIONS (4096).
The docs say what happens to a router mapping when the game crashes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game joins `friend:CODE` with either code: a full invite is dialled
directly, then through the relay it names; a short code goes through the
relay the player set. Join a friend gains a Relay (optional) field, saved
to horizon.txt (friendsRelay) and used for hosting too; the code field
shows the end of long codes and says what is wrong with one (typo, other
version, short code without a relay). The launcher validates both kinds.
tools/test_friends.py now runs (a) the code alone with no relay anywhere
and (b) a relay run locally with the short code; routers and STUN stay
out of tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hub::admit read the entity counter before incrementing it, while the game
increments first (the current value is the last id in use). The first
guest after Open to LAN got the host's own entity id, so its client took
the host's AddEntity for itself and never showed the host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Horizon pause menu gains "Invite friends" beside Open to LAN. It opens
the world to LAN if it isn't (Open to LAN's defaults) and shares that port
behind an invite code through the relay; the menu shows the code with Copy
and Stop sharing, who is here and why the code changed. Closing the LAN
server ends the code; stopping sharing closes LAN again if sharing opened
it. `horizon-ctl friends host|stop|status` drives it in harnesses.
Without HORIZON_RELAY_URL (the default until a relay is deployed) the button
is disabled and says the feature isn't available yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game joins `friend:CODE` addresses: the network thread resolves the code
through the relay, agrees on the key, settles on a direct or relayed stream
and connects the client to the tunnel's loopback port; everything after is
an ordinary join. Errors (unknown or ended code, wrong secret, host not
answering, version mismatch, rate limits) end on the Disconnected screen in
plain words. Shown and logged addresses hide the code's secret half.
Horizon's multiplayer page gains "Join a friend" (a code field); the classic
screens keep vanilla's buttons. The launcher's Play page gains a "Join a
friend" row (paste works) that starts the game straight into the join;
`horizon-launcher play --join CODE` does the same.
tools/test_friends.py runs a relay, a host Horizon sharing a world and a
guest Horizon, over plain LAN (--baseline), the direct path and the forced
relay path: chat, movement and blocks both ways, then the ended invite.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic can't pay for a hosted relay, so nothing depends on one now. The
host's game finds how it can be reached on its own: its interface
addresses (LAN, VPNs such as Tailscale or ZeroTier, IPv6), a port opened
on the router for an hour at a time (PCP, then NAT-PMP, then UPnP IGD;
renewed while sharing, deleted on stop) and its public address from STUN
(Cloudflare's and Google's public servers by default, configurable). The
invite code carries those endpoints and an 80-bit secret in base32 groups
with a checksum; guests dial every endpoint at once and run the same
SPAKE2 + ChaCha20-Poly1305 handshake, no rendezvous involved. When nothing
can reach the host it says why and what to do (UPnP, the port to forward,
a relay, a VPN; CGNAT and double NAT detected).
The relay stays as something anyone can run (binary, Dockerfile, systemd
unit; Railway config removed): with one configured the host also gets a
short code, and the full code names the relay so guests fall back to it.
No relay by default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dual-stack listener reports them as ::ffff:a.b.c.d, which the other end
can't dial from an IPv6-only socket.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-friends: codes (Crockford base32, nameplate + secret), the relay's
JSON-line protocol, SPAKE2 then ChaCha20-Poly1305 frames keyed off the
code's secret, direct connections (candidates, TCP simultaneous open from
a reuse-port socket) with the relay as fallback, and the host and guest
ends that carry the game's TCP stream between loopback ports.
horizon-relay: the service (codes, rendezvous, byte relay, rate limits,
bandwidth caps, idle timeouts, counts only), its Dockerfile and Railway
config. Nothing is deployed and nothing in the game uses it yet.
Design: docs/play-with-friends.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/privacy says what the launcher may send when a player allows it (performance
summaries, crash and bug reports, benchmark uploads), why, on what basis,
where it is kept and for how long, and how to delete it. The footer links it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
crates/horizon-telemetry-server, its own workspace like the launcher's (the
game's workspace never builds a web server):
- POST /v1/batch (zstd, decoded and checked by horizon-telemetry), DELETE
/v1/installs/<id>, GET /v1/aggregate (cached 10 min, CORS for the website),
GET /healthz.
- SQLite: machine descriptions stored once, one row of columns per session
with 80-byte summary blobs, zstd bodies for crash/bug/benchmark, daily totals
rebuilt when a day changes and kept after raw rows expire (180 days). About
190 bytes per session on disk.
- Per-address token bucket (addresses in memory only), per-install daily cap,
groups under 5 install-days hidden from the totals.
- Tests: refusals, rate limit, daily cap, deletion (rows and totals), retention,
storage size, and end to end with the launcher's uploader.
- Dockerfile and a path-filtered CI workflow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game never sends anything itself; it keeps items in the outbox for the
launcher, and only for kinds the player allows:
- Crash reports: crash::deliver keeps a scrubbed copy (error, stack, thread,
Horizon version, graphics adapter and render settings; never the log, world,
server or resource packs; the world's name and folder and the server address
are scrubbed wherever they appear). Without consent crash-reports/ stays
local, as before.
- Performance: a Session records frame times while playing and the integrated
server's tick times into fixed-size Timings, with the time to the title
screen and to the world; on leaving a world or quitting, a session of a
minute or more becomes a summary (under 1 KiB) when the player allows it.
- The GPU, its driver and backend come from the wgpu adapter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
The real-pack benchmark on the production resource_root path stays as the
reproducible measurement. On the Mac mini it saves 1.18 s (Faithful 32x) and
5.29 s (FPBR) per startup with the pack enabled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Horizon graphics setting (Performance, off by default, batteryMode in
horizon-graphics.txt): at most 60 frames per second in a world, 30 in menus
without a world or behind other windows, and half the automatic worker
threads. It only changes when frames are drawn; ticks and what frames show
are untouched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-telemetry, shared by the launcher, the game and the ingest server:
- consent.json in the game folder: four switches, all off until the player
answers, a consent version that re-asks when what is collected changes, the
answer's time, and a random install ID made at the first yes.
- Payloads: perf summaries (percentiles + 14 frame-time buckets from a
fixed-size Timings recorder), crash and bug reports, and benchmark reports
carried as docs/benchmark-report.md defines them; size caps per kind.
- A scrubber for user folders, user names, e-mail and IP addresses, UUIDs and
caller-known private terms.
- The outbox: only allowed kinds get in, daily caps, 200 items / 2 MiB.
- zstd batches with bounded decoding; the uploader re-reads the consent and
drops withdrawn items before connecting, backs off and caps batches a day.
- tests/consent_gating.rs: a local mock server sees zero connections with the
consent missing, outdated or withdrawn.
No server is set (DEFAULT_ENDPOINT is None): nothing is sent yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pr_ready is the one self-check to run before asking for review: from the
files a branch changes it runs rustfmt, clippy with warnings denied, the
tests of the touched crates (mapped through cargo metadata), the launcher's
own workspace when it is affected, the tokens.css check, the Cargo.lock
rule, the Python tests and the public-tree check, and the vanilla harnesses
the change can affect, picked by tools/parity.py exactly as CI's parity job
picks them. It runs locally or on a fleet machine through ~/fleet/rbuild;
on the Mac mini the harnesses use the parity runner's own workspace and
scripts. It prints one Markdown block for the pull request body and exits
non-zero on any failure.
tools/parity.py gains --changed-files for copies without Git history, and
tools/ci/vanilla-data.sh accepts the preparation key from outside.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Horizon-style Singleplayer page had no way to delete a save. A Delete
(danger) button now shows beside Play when a world is selected and opens a
confirmation page in the design language: the world's name and last-played
date in a notice led by the danger dot, "It will be lost forever. This
can't be undone.", then Delete world / Cancel. After deleting, the list
refreshes and the selection moves to the world that took the deleted row
(or the new last one). A failure shows a Horizon error page whose Back
returns to the question.
Both menu styles share world_management::delete, now a port of
LevelStorageAccess.deleteLevel: it takes the world's session lock first, so
a world open in this or another instance is refused, deletes everything but
the lock (retrying five times, 500 ms apart), then releases and removes the
lock and the folder. Only a single plain folder name directly in saves is
accepted, and links inside the world are removed, never followed.
Leaving a world now flushes the region writer and releases the session
lock (vanilla closes the storage access), so the world just played can be
deleted from the title.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 has a MapItem per explorer map (buried_treasure_map,
abandoned_camp_map, woodland_mansion_map...) and treats any stack with a
map_id as a map. Its ExplorationMapFunction finds the nearest structure of
the tag, makes an unlimited tracking map centred on it, draws the biome
preview (MapItem.renderBiomePreviewMap) and adds the target decoration
(MAP_DECORATIONS, drawn by tickCarriedBy). Horizon only recognised
filled_map, turned trade maps into filled_map, cleared every decoration but
the player's each tick, and chest loot only flagged the map as found: the
treasure maps of shipwrecks and underwater ruins and the maps of 26.3's
abandoned camps had no map id, no target and no picture.
- map_id: any stack with the component.
- tick_maps: the item's map_decorations (red X, mansion, camp...) are drawn
as addDecoration places them (off the map only when unlimited).
- render_biome_preview: vanilla's water outline picture from the noise
biomes at sea level (new generator query noise_biomes, fuzzed through
BiomeManager's zoom) and #water_on_map_outlines.
- Chest loot (rolled with the chunk here) keeps the function's search in
minecraft:custom_data; opening the chest runs it from the chest (vanilla
runs it when the chest is unpacked on first open), else reaching the
inventory runs it from the player. Nothing found drops the item, as the
tables' filtered map_id check does.
- Trades keep their map item.
Fixes#147
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Releases now ship as often as changes land, so nothing they leave behind may
grow without bound. Workflow uploads keep 3 days (nightly parity logs 7); the
releases carry their own assets.
tools/ci/runner-gc.sh, run every 6 hours by launchd on the Mac mini from a copy
of main, skips a runner in a job, removes a checkout's cargo build folders past
12 GB and harness work folders under .local/ untouched for 3 days, and keeps the
2 newest shared preparations while no runner works. The runners build through
the machine's sccache, so what it removes comes back from the cache.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic wants releases as often as changes land. A kind now releases when
player-facing pull requests merged since its last release and the newest of
them merged 15 minutes ago (a burst of merges becomes one release), or the
oldest an hour ago when merges never pause; release-now still ships at once.
There is no minimum time between releases. The scheduled checks run every 10
minutes (game at :02, launcher at :07) so a settled release goes out without
another merge.
Release titles keep proper nouns (Horizon's mark, Microsoft, LAN, ...) when
they lowercase the clauses after the first.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The classic title screen's banner now reads "RUST EDITION". It is rebuilt
at load time from the player's own textures/gui/title/edition.png:
"EDITION" is kept column for column, the "T" is lifted out of it, and R, U
and S are Horizon's own glyphs (rectangles in title_edition.rs) drawn the
way the texture's letters are built, in shades sampled from it. "RUST"
spans "JAVA"'s columns, so the banner stays centred. Only vanilla 26.3's
exact texture (pixel fingerprint) is rebuilt: a resource pack's banner is
left as drawn. No Mojang pixels are added to the repository.
The logo and banner are now blitted as LogoRenderer does: the logo's top
44 of 64 rows and the banner's top 14 of 16 rows at y = 30 + 44 - 7
(the banner was 20 px too low and squeezed 16 rows into 14).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The icon files kept their names while the server lets browsers cache /favicon*
for a week, so visitors still saw the old mark. The pages now link each icon
with its content hash (?v=…), and the build writes /favicon.ico (the 32-pixel
PNG in an ICO container) for browsers that ask for it on their own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's ClientPacketListener.handleTakeItemEntity adds an
ItemPickupParticle for every take: the taken entity's render state
(extracted at partial tick 1) moves to the collector's x, (y + eyeY) / 2, z
along ((life + partial) / 3)^2 over three ticks, the target lerped between
ticks (ItemPickupParticleGroup). Horizon removed taken entities at once.
game/pickups.rs keeps those particles (joining the group at the end of the
particle tick, as ParticleEngine.tick does) and the item, orb and arrow
renderers draw them frozen at the moving position. Takes by the player
(items, orbs, arrows), by mobs (allays, villagers, piglins, sulfur cubes,
raid banners), by LAN guests on the host, and ClientboundTakeItemEntity on
a LAN guest or server all add one.
Fixes#142
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla plays the hurt sound on the client from broadcastDamageEvent
(LivingEntity.handleDamageEvent -> Player.getHurtSound, the damage type's
DamageEffects sound, pitch (r - r) * 0.2 + 1). Horizon only played
entity.player.hurt by hand for falls and mob hits, so sweet berry bushes
(poking -> hurt_sweet_berry_bush), fire and lava (burning -> hurt_on_fire),
drowning, freezing, cactus and the rest were silent.
The combat port's broadcast_damage now carries its damage source
(HealthEvent::DamageBroadcast, same trace line as before) and the game plays
hurt_sound(effects) for it; the two hand-written plays are gone.
Fixes#144
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every offline Horizon player joined as Player, the LAN host's own offline
profile, so the host refused every guest with "You logged in from another
location". Vanilla's client defaults --username to Player + millis % 1000
and the launcher passes the account's name. Horizon now generates
Player + five digits once and keeps it in horizon-player.txt next to
options.txt (same name, same offline UUID, same inventory on a friend's
world). --name, HORIZON_PLAYER_NAME and a signed-in account still win.
tools/test_lan_lifecycle.py: two default guests join one host together,
keep their names across rejoins, and a guest named Player is refused.
Fixes#195
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every page and the in-world screens veil the backdrop at 0.90; the home page at 0.80, with its
secondary text in ink_soft (Ui::muted). Muted text over the backdrop is BACKDROP_MUTED. The home
page's faint wordmark is now whole: as wide as the content column, above the footnote, instead
of cut off at the bottom right.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HORIZON_MSA_MICROSOFT_ENDPOINT (debug builds only) points the device-code and token requests at
a local mock, so the restyled code panel can be seen and screenshotted without Microsoft.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
macOS: packaging/macos/package.sh (run by launcher.yml) builds Horizon.app (Info.plist, bundle
id dev.horizon.launcher, macOS 11+, the .icns), signs it ad hoc, and makes the two release
assets: horizon-launcher-macos-arm64.dmg, the installer (the app and an Applications link over
a background, laid out by dmgbuild without a GUI), and the .zip the launcher updates from. A
launcher inside the app replaces the whole bundle: copied beside it with ditto, swapped in by
renames, restarted; a bare binary, as older launchers are, still finds its binary in the zip.
A macOS test zips, unpacks, swaps and re-verifies a signed bundle. Signing for real is a
MACOS_SIGN_IDENTITY away (packaging/macos/README.md, with what Gatekeeper asks meanwhile).
Linux: release builds keep horizon-launcher.desktop (with the binary's current path),
horizon.desktop (hidden, for the game window's icon) and the hicolor icons under
$XDG_DATA_HOME, without root; `horizon-launcher desktop-entry --remove` takes them out and
stops it, `desktop-entry` puts them back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The README banner is redrawn in the new design (docs/media/banner.html), and the launcher and
menu screenshots (README and website) are the restyled ones. The website's launcher section
gains a Launcher tab: its screenshots no longer show the old background, whose licence was
never confirmed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The mark is an H whose crossbar is the horizon: a violet line edge to edge, the stems above it
and their reflection below at a third, on whole units of a 16-unit grid so it stays sharp at
16 px. docs/media/brand holds the SVG masters (square icon, macOS tile, avatars, the mark for
light and dark backgrounds), the two alternatives that were shown with it, the GitHub social
preview and the Discord banner, and a README of what goes where and how to upload what only
Ludovic's accounts can.
`cargo run -p xtask -- icons` renders the masters (rsvg-convert) and packs, in plain Rust:
packaging/icons/horizon.ico and horizon.icns, Linux's hicolor icons 16–512 and the scalable
SVG, the game's run-time window icon, the avatars and the website's favicons.
packaging/linux has .desktop entries for the launcher and the game.
On Windows, build.rs embeds the .ico as icon resource 1 in horizon-client (and its tools) and
horizon-launcher (winresource, a Windows-only build dependency); gpui loads that resource as
the launcher's window icon. The game window sets the icon itself (X11, Windows) and names
itself `horizon` on Wayland so the desktop entry matches.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page now reads the design tokens generated from crates/horizon-theme (src/tokens.css, put
before style.css by build.mjs), and follows the system's light or dark appearance, with a
toggle that remembers the choice. The full-screen sunset hero is replaced by type: a headline,
one paragraph, the download, and an index list of what the page holds. Every section is a mono
label, a display heading and its content as numbered rows with one-pixel rules: the facts, the
fourteen subsystems, the launcher's features, the community links. Rows fade a soft plate in
under the pointer; content arrives with a short fade, rise and unblur. The footer ends on the
giant faint wordmark. The 404 page and the social preview (og.html) follow.
Instrument Serif, the hero images, the parallax, the statement fill and the magnetic buttons
are gone; the icons come from Horizon's new mark (xtask icons). Touch targets are 44 px high.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only Horizon's own style changes (what HORIZON_MENU_STYLE=horizon and "Menu style: Horizon"
show); vanilla's classic screens and the HUD are untouched.
The shared components in ui_theme follow the launcher: pill buttons, the sidebar as text items
with a soft sliding pill under the mark and a name instead of a tinted panel, the segmented
control's ink pill, a 36x20 switch, a thin ink slider, fields without borders, display-size page
titles, mono capitals for status lines and section labels. Settings rows, worlds and servers are
numbered index lists (01, 02…) with one-pixel rules; list rows fade a soft plate in under the
pointer and keep it under the selection. Plates are translucent ink so they read the same over
any backdrop. The veil over the live backdrop is flat (no gradients), lighter on the home page,
which ends on the faint wordmark. The opening's wordmark is "horizon" in Geist.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A top bar (the mark, Horizon, the pages as text) replaces the sidebar; a mono footer carries
the launcher's version, its update and the community links. Every page is a display title, a
short muted line, then an index list: Play's resources and folders, the versions with their
names large and their size and state in mono, the patch notes, the settings. Buttons are pills,
the main action is the one violet thing on the screen, selection is an ink pill, the console,
notices and the crash report sit on soft plates without borders. The Play page ends with the
faint wordmark, half set below the window's edge.
The sunset photograph and its veils are gone (and the `image` dependency with them): the
window is the page colour, light or dark with the system. The opening draws the wordmark
letter by letter, then the launcher rises into place. HORIZON_LAUNCHER_APPEARANCE=light|dark
overrides the system's appearance, for screenshots and tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The veil goes to 0.90 (0.80 on the title's home page, which keeps to ink and ink_soft), and
muted text over the backdrop is lifted to #9fa3ae; a test checks every pair over a backdrop of
luminance 0.77 (the snowiest title panorama measured reaches 0.49), bare and on a row's plate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/design-system.md describes the new design language (after mkdir.directory: type as the
interface, numbered index lists with hairline rules, one violet accent, space instead of
borders, quiet motion, a faint wordmark) and its tokens, components, surfaces and mark.
crates/horizon-theme now holds the whole system: the two palettes (cool neutrals, light and
dark, each text colour checked against WCAG AA on every background by a test), the type scale
with web sizes, spacing, radii, rules, control heights and motion with CSS easing curves.
Instrument Serif is gone; display type is Geist at medium weight.
`cargo run -p xtask -- tokens` writes website/src/tokens.css from the crate, and a test fails
when the file is out of date. The launcher and the game's menus read the new names through
small adapters; the restyles follow in their own changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A saved spawner's block entity id is minecraft:mob_spawner, not its block's
name, so decode_block_entity found no block entity for it and dropped it:
every spawner loaded from a save (a vanilla world's dungeons and mineshafts,
or Horizon's own after a reload) lost its spawn data, and the server sent
its chunk with no update tag. The id now maps to the spawner block.
The chunk packet lists block entities in LevelChunk.blockEntities' order,
which Horizon reproduces from the position hashes; positions sharing a
bucket keep their insertion order, which for a loaded chunk is the save's
block_entities list (SerializableChunkData -> pendingBlockEntities). That
list order is now kept with the chunk and used as the insertion order.
dedicated_server: 542 transcript lines identical (was 10 differing).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
EnderDragonFight's boss event sets createWorldFog, and while such a bar
shows AtmosphericFogEnvironment.setupFog pulls the fog in (start at most
10, end at most 96, sky and cloud fog ending there): Horizon never did, so
the End stayed clear during the fight. FogInputs gains boss_world_fog,
set while the dragon's bar shows.
FogRenderer's void darkness starts voidDarknessOnsetRange (32) above
level.getMinY(), which is 0 in the Nether and the End; Horizon used the
Overworld's -64 everywhere, so the End's fog never darkened below y 32.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crystals were a hand-built mesh without normals (so unshaded), without
the bedrock base and without beams. They now draw the baked
minecraft:end_crystal#main layer under EndCrystalRenderer's transform
(scale 2, down 0.5, not flipped) with EndCrystalModel.setupAnim's poses
(end_crystal::poses: the base shown per showsBottom, the glass raised by
half the bob, outer glass, inner glass and cube turned with rotateBy), the
entity light at their block, and the beam to their target
(submitCrystalBeams from the target's centre up to the crystal) while the
spikes are being summoned back. Crystals placed outside the End draw too.
The entity model harness no longer skips end_crystal: the oracle runs
EndCrystalModel.setupAnim on EndCrystalRenderState and the fixture poses
the layer with end_crystal::poses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
The host sends Creeper.DATA_SWELL_DIR and DATA_IS_IGNITED with the
creeper's entity data, and a guest runs the client half of Creeper.tick
on them, so the swell and white flash CreeperRenderer draws now show on
the guest too. Creeper.readAdditionalSaveData's ignited lights the fuse
(summon creeper ~ ~ ~ {ignited:1b}).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Classifies the online-mode login outcomes from #161: invalid session,
banned from playing online, multiplayer disabled, no account signed in
and the server failing to verify the username are never retried;
unreachable session servers (the client's or the server's) are. A retry
of an online-mode server joins with the account held in memory for the
whole run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the Disconnected screen after a lost multiplayer connection, a
countdown "Reconnecting in N s" with a Cancel button, then the same
address again. Attempts back off (5, 10, 20, 40, 60 s) and stop after
five; the count resets only after a minute connected. Only failed
connections and the server's shutdown, restart, time-out or full
reasons are retried, never kicks, bans, whitelists, version refusals or
duplicate logins: the network thread now says whether the server gave
the reason (login refusals included).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
nightly.yml runs every harness from scratch on main at 03:00 Paris time on the Mac mini's runner, after
preparing the resources again if the preparer changed. tools/ci/parity_issues.py compares the results with
the previous audit: a harness that passed and now fails gets one issue labelled parity-regression (an open
one is commented on), and an issue closes when its harness passes again. The bugs session picks them up.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
launcher.yml releases the launcher by itself: after a push to main that changes launcher/,
crates/horizon-prepare or crates/horizon-theme, hourly, and by hand, with the game's rules
(tools/release_train.py decide --kind launcher: player-facing pull requests and a 3-hour cooldown,
release-now, skip-release, RELEASES_PAUSED). launcher-vX.Y.Z is the next patch, or the next minor after a
pull request labelled feature. Linux on ubuntu-22.04, Windows on GitHub, macOS on the Mac mini's runner;
published with --latest=false.
No version-bump commits: VERSION reads HORIZON_LAUNCHER_VERSION with option_env!, falling back to
Cargo.toml's, and --version, the User-Agent and the self-update comparison use it; each build checks
--version says the release's version.
With several game releases a day, the newest launcher release soon falls out of the newest 100 (launcher)
or 30 (website) releases: both now look the newest launcher-v tags up one by one when the list has none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla's resource packs refuse symbolic links inside a pack: with linked sounds the vanilla sound oracles found
no sound files while Rust did (sound_event, sound_engine on #133's parity run).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Checked on the Mac mini: 1500 offscreen frames in 12 s, 81 chunks generated, 4153 sections meshed, a player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first audit on the Mac mini failed 7 audio harnesses (no sounds in the prepared resources: the Mac mini has
no official launcher for horizon-prepare sounds to copy from) and save_roundtrip (it needs vscreen). The
sounds depend only on the Minecraft version: kept once in shared-local/sounds/<version>/ and linked into each
preparation.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A second runner (ludovics-mac-mini-2, same labels) shares the first one's vanilla data through a link, so
vanilla-data.sh prepares under a lock; a 30-minute audit no longer holds a release's macOS build back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
release.yml now releases the game by itself: after every green checks run of a push to main, every hour
(a release the cooldown held goes out without another merge) and by hand. tools/release_train.py decides
from the pull requests merged since the last published game release: a release when one changed what
players run and the last release is 3 hours old, at once for a pull request labelled release-now;
skip-release pull requests don't count. RELEASES_PAUSED=1 stops it. Tags vYYYY.MM.DD, then .1, .2 the
same UTC day.
Linux (ubuntu-22.04) and Windows build on GitHub with Cargo caches, macOS on the Mac mini's runner
(MAC_RUNNER=macos-latest sends it to GitHub). Each builds horizon-client, horizon-ctl and horizon-prepare
(all three named: --bin selects only the named binaries), checks all three exist, checks their libraries,
starts each one, packages with xtask and archives; the Mac also runs the game offscreen for 300 frames on
a new world with resources prepared from its own Minecraft. The notes: New (label feature), Fixed (the
rest), Known issues (docs/known-issues.md). A hand-started run off main, or with dry_run, builds
everything and writes the notes but publishes nothing.
docs/release-flow.md describes the flow, the labels, pausing and rolling back; the launcher gets a test
that a release turned back into a draft is never offered.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first parity run kept only .local/horizon-client per preparer version; the exporters' outputs beside it
stayed in the checkout, and the dedicated server, which finds registries.json at
<resources>/../../datagen, didn't find it next to the prepared folder (world_spawn failed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
verify keeps its dependencies and workspace build in a cache saved by main's runs (Swatinem/rust-cache).
The new parity job runs on the Mac mini's self-hosted runner (label horizon-game), which holds the
maintainer's own Minecraft files: tools/ci/vanilla-data.sh links .local/ to the runner's copy and prepares
the resources with the pull request's own horizon-prepare, once per preparer version; then
tools/parity.py --changed <base> runs only the harnesses whose traced dependencies changed since the merge
base and that have no pass for their current inputs. A fork's pull request runs it only when a maintainer
adds run-parity, once per label, on the commit it had then.
parity.py: --changed REF, and linux_only in tools/parity_hosts.json for harnesses that differ on other
platforms for a reason that is not a Horizon bug (skipped there with the reason, unless named).
docs/release-flow.md starts with the pull request checks and merging.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Device-code OAuth, Xbox Live, XSTS, Minecraft login, ownership and profile;
secrets in the OS keychain (oo7 on Linux, keyring on macOS/Windows), accounts.json
without tokens; Settings account rows, 'Playing as' on the Play page; CLI
'account' and MCP account tools; the game gets a 0600 session file it deletes.
Client ID is configuration (HORIZON_MSA_CLIENT_ID), empty = not configured.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Players' skins on online servers come from https://textures.minecraft.net;
the old plain-HTTP fetch left everyone with their default skin.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read the signed-in account from a session file (--session-file, deleted
on read), answer ClientboundHelloPacket like vanilla: shared secret, server
hash, session join through the services discovery document (authlib's
error mapping and messages), RSA-wrapped key packet, AES/CFB8 both ways.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A corner map of the loaded chunks in vanilla's map colours with the map
item's height and water shading: square or round, three sizes, four
zooms, north up or turning with the player, with the player arrow. In
the Nether, underground and under a roof it maps the floor at the
player's height and cuts walls. Each loaded chunk keeps a 16x16 tile,
filled nearest first within 0.3 ms a frame and rescanned after block
changes; the image is recomposed only when the view or a tile changed
and uploaded into its own atlas slot. Nothing runs while it is off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The zoom key went to the zoom on every screen, so with zoom on, Z could
not be typed in screens with text fields that share the game's key
handler. Now it zooms only while playing with no sign or book open, lets
go when a screen opens, and always passes the key on, like a vanilla key
mapping. A vanilla mapping bound to the same key still acts; the Extras
page marks the zoom key "Also bound" and names it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Agents press keys and buttons through the control socket, which skipped
the keyboard and mouse hooks; under control the window may also lack
focus without pausing, so the zoom no longer lets go then.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An Extras section in Horizon's Options holds optional additions saved in
horizon-extras.txt beside options.txt. Zoom divides the field-of-view
target vanilla's Camera.tickFov eases toward (the spyglass path), with a
wheel-adjustable level and lower mouse sensitivity; scoping keeps the
spyglass view. The FPS counter (with minimum and 1% low) and the keystroke
overlay (WASD, space, mouse buttons with clicks per second) draw in the
HUD layer, hidden with F1. docs/extras.md describes all the extras.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The health baseline (HealthWatch) is forgotten when there is no player,
on the title screen, and whenever a new player entity takes over (spawn
and respawn, joining a server, End and Nether dimension changes), so
arriving with less health than the last player had isn't damage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Short, light pulses through gilrs force feedback, on by default
(Vibration in horizon-controller.txt): damage taken (stronger with the
damage, from any source, here or on a server), a block broken, an
explosion nearby (fading over four times its radius), a totem of undying.
Only the pad that is playing vibrates; macOS has no force feedback in
gilrs and stays still. pad status reports the last vibration asked for.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Since #52 the shared vertex carried an RGBA8_SNORM face normal for
entity.wgsl's directional light, so every section vertex grew from 32 to
36 bytes although vs_terrain never reads it. Vanilla's terrain uses
DefaultVertexFormat.BLOCK (position, colour, UV0, UV2: 28 bytes): the face
shade is baked into the colour by ModelBlockRenderer and terrain.vsh only
multiplies it by the lightmap.
Sections now mesh into TerrainVertex: position, UV, the sprite layer as a
u16 (bit 15 marks opaque leaves), the two light bytes as a u16 and the
RGBA8 colour, 28 bytes. Every value the shader reads is unchanged; entity
vertices keep their normal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The status reply's renderer block gains the section vertex arena's buffer
bytes, the bytes resident sections use in it, and the backend allocator's
allocated/reserved device memory (Vulkan), to measure terrain memory.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
options.txt, horizon.txt, horizon-graphics.txt and the rest showed up as
untracked after running the game from the checkout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ui::interact records every enabled control (sidebar items, switches,
sliders, segments) as a D-pad focus target, apart from the labelled
button list horizon-ctl clicks by name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The left stick moves a cursor drawn over the screen, the D-pad steps
between slots and buttons (focus_step), and A / X / the right stick send
the window the pointer events a mouse would, so every screen keeps its
own mouse behaviour (drags, sliders, scrollbars). Y quick-moves in
inventories, LB/RB switch creative tabs, B and Start go back. Focus lands
on the first button, or the held hotbar slot, when a menu opens while the
pad is in use; moving the real mouse hands the pointer back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With --bin flags, cargo builds only the named binaries of every selected
package, so -p horizon-prepare built nothing and the import checks failed on
target/release/horizon-prepare (every run since #92).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla samples keys once per tick, so a stick click shorter than 50 ms
(sprint) could come and go between two ticks and be lost: its release
waits for the next tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`cargo run -p xtask -- package BINARY OUTDIR` does what package.sh did:
OUTDIR wiped and recreated, the `git archive HEAD tools
mods/horizon/src/test/java crates/horizon-prepare research-lock.json LICENSE`
tree extracted (the tar read in Rust, executables kept executable), the client,
horizon-ctl and horizon-prepare copied (.exe beside an .exe, the same error when
one is missing) and PLAY.txt. release.yml calls it; package.sh is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 has no gamepad input, so this is Horizon's own, on Bedrock's
default layout (docs/controllers.md). gilrs reads the pads; the pure part
(bindings, radial deadzones, the look curve, button edges, the
horizon-controller.txt settings file) is in gamepad.rs with unit tests.
Buttons act as the vanilla key mapping they're bound to, through the same
path a rebound keyboard key takes (the keyboard's post-rebinding handling
moves into Game::mapped_key). The left stick is an analog move vector,
the right stick turns the view per frame, LB/RB step the hotbar, B and
Start leave screens and A respawns. horizon-ctl pad injects a virtual pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/fetch_sounds.py (and the lab.py functions it used) becomes
`horizon-prepare sounds` (horizon_prepare::sounds): the pinned asset index,
sounds.json and every .ogg from the player's own launcher install, each SHA-1
checked, into .local/horizon-client/<version>. The launcher runs it as part of
Prepare (in-process, the version's own binary when its sources differ and it
knows the subcommand, in-process for older versions), and a launcher install
without the assets only logs that the game will be silent. Player docs no
longer mention Python for sound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hurt red and the white row's alphas come from overlay.rs, which
reproduces OverlayTexture's texels (checked against values printed by
the 26.3 JVM) and is prepended to entity.wgsl; a test parses and
validates the composed shader with naga.
Swelling creepers flash white as CreeperRenderer.getWhiteOverlayProgress
says (the white row's column, red winning while hurt; layers keep the
red only), and CreeperRenderer.scale's wobble uses the unclamped
swelling as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3 has no gamepad code; Horizon's own design follows Bedrock's
default controller layout. Mapping, menus, glyphs, options file, rumble,
Steam Deck and the PR plan.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
entity.fsh mixes the OverlayTexture texel into the textured, cardinal-lit
colour and only then multiplies by the lightmap. Horizon applied the lightmap
first and mixed a 0.3 red into the vertex colour, so the red was multiplied
by the texture and never darkened: hurt mobs glowed red at night and in caves.
The vertex stage now passes the lightmap colour and the overlay texel
(0xB2FF0000 hurt red, white alpha 63 for OverlayTexture.u(1.0)) to the
fragment stage, which mixes the overlay, then the lightmap, then glint and fog.
Layers vanilla submits with OverlayTexture.NO_OVERLAY no longer take the hurt
tint: armour (EquipmentLayerRenderer, glint stays), held items
(ItemInHandLayer) and the enderman's block (CarriedBlockLayer). The TNT
minecart flash uses the white overlay on the cart's light
(TntMinecartRenderer.submitWhiteSolidBlock) instead of plain full-bright white.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon searched its own spiral of 48-block steps from 0 0 for a dry,
non-leaf top block, so seed 1891623539983467248 spawned at -48 -48 where
vanilla puts the world spawn at 144 71 144 (the player then lands within
the respawn radius, 152 145 in the report).
Vanilla (MinecraftServer.setInitialSpawn): NoiseBasedChunkGenerator.
getOrigin runs NoiseSpawnFinder over the noise settings' spawn_target
points (SpawnTargetPoint.sampleFitness, a 2048/512 then 512/32 radial
search biased towards 0 0); an 11 by 11 chunk spiral around that chunk
takes the first column PlayerSpawnFinder.getSpawnPosInChunk accepts
(getLevelRespawnPos: MOTION_BLOCKING, WORLD_SURFACE and OCEAN_FLOOR
heights, no liquid, a full-topped collision shape); without one, the
origin chunk's centre at the spawn height. The first player is then
placed by PlayerSpawnFinder.findSpawn: the respawn-radius square in a
random coprime order, a free player box with liquids colliding, else the
spawn with fixupSpawnHeight; adventure mode uses the spawn as is.
horizon-world gains worldgen::spawn_finder and OverworldGenerator::origin;
the client's world_spawn module and game/initial_spawn.rs follow
setInitialSpawn and PlayerSpawnFinder, for single player and the
dedicated server alike. Flat worlds use FlatLevelSource's origin and
spawn height.
tools/test_world_spawn_parity.py compares, for the report's seed, the
chunk harness's seeds and 50 random ones, the origins
(VanillaWorldSpawnOracle against the spawn_origin example) and the
level.dat spawn of worlds created by the vanilla dedicated server and by
Horizon's. Vanilla is not deterministic on chunk corners where trees of
neighbouring chunks overlap (decoration order follows its worker
threads), so a mismatching seed reruns vanilla up to three times.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A right click with food that is also a block item tried to eat it before
placing it: in creative always, in survival whenever the player was
hungry. BlockItem.useOn places first and only eats a consumable that could
not be placed (counting the use, as ItemStack.useOn does). Picking a sweet
berry bush or cave vines dropped nothing: the harvest loot
(Block.dropFromBlockInteractLootTable) is now rolled and popped. Bone meal
on a bush that can still grow grows it (SweetBerryBushBlock.useItemOn
passes it on) instead of picking it, and counts the use with its
item_interact_finish vibration. Every block placement played its sound
twice; BlockItem.place's level event is now the only one.
tools/test_use_item_on_parity.py drives vanilla's real
ServerPlayerGameMode.useItemOn (and Minecraft.startUseItem's useItem
fallback) against use_item_as_player in a headless game.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mouse-move DX DY (MCP mouse_move) adds relative motion to MouseHandler's
accumulated movement, so the next frame turns the player with
turnPlayer's sensitivity curve ((s*0.6F+0.2F)^3*8, no 8 while scoping),
the inverted axes and the cinematic camera, then Entity.turn. The motion
counts as a grabbed pointer in an active window until the real pointer
is grabbed or released again.
turn and look-at (and look-at-block/look-at-entity) take --over SECONDS:
the turn becomes that motion fed over the time with ease-in-out (the
shorter way round for look-at), replying once the last of it has been
turned. Without --over they still jump. turn is now an MCP tool. stop
cancels a turn in progress.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The debug crash from #90 printed a stand-in report; it now ends the game
with a DebugCrash error that main reports as "Manually triggered debug
crash" with the stack where it was raised. The game keeps the error that
ends it as an error, not a string, so main can tell. Leading std backtrace
frames are dropped (macOS keeps them).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.applyEffectsFromBlocks now runs WebBlock.entityInside and
SweetBerryBushBlock.entityInside for the player through
Player.makeStuckInBlock (no slowdown while flying, impulse context reset),
with Weaving's lighter cobweb hold and the sweet_berry_bush damage of a
grown bush while the known movement is at least 0.003 on X or Z. The scene
motion harness gains cobweb and berry-bush fixtures run through vanilla's
real applyEffectsFromBlocks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game writes logs/latest.log in its home like vanilla's log4j config
(client-1.21.2.xml): [HH:mm:ss] [thread/LEVEL]: message, rolled at start and
at midnight to logs/<date>-<n>.log.gz, seven a day. Messages go through the
log facade and still print their old text on stderr.
A main-thread panic or a fatal error writes
crash-reports/crash-<date>_<time>-client.txt (error, stack, Horizon and
Minecraft versions, OS, CPU, memory, GPU adapter, render settings, resource
packs, world, last 50 log lines). Development builds crash on purpose with
HORIZON_DEBUG_CRASH.
The launcher shows "Horizon crashed" when the game it started ends badly,
with Open report, Copy report, Open logs folder and Report on Discord;
Settings gets a Logs row. horizon-launcher logs and horizon-ctl logs
(--lines N, --crash) print the log's end or the newest report, and both MCP
servers have a logs tool.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fullscreen: applying settings (any option button, joining a world,
travelling, reloading packs) compared the Fullscreen option with what the
window reported and forced the window back to the option. A window made
fullscreen by the compositor (a Hyprland keybind or window rule) left
fullscreen on the next button click. The window now follows blaze3d
Window's fullscreenRequested/fullscreen pair: it is only asked to change
when the option changes (updateFullscreenIfChanged), a change the window
system makes is adopted by the option (updateFullscreenState ->
Minecraft.fullscreenStateChanged), and the window is created fullscreen
when options.txt says so.
Clipboard: Ctrl+A/C/V/X (Cmd on macOS) in the chat, signs, book and quill
(page and title), anvil, creative and recipe-book searches, the language
and pack searches, the LAN port, world name and seed, and the server name
and address boxes, through the arboard clipboard (Wayland data-control,
falling back to X11). Shortcuts follow InputWithModifiers.isCopy/isPaste/
isCut/isSelectAll on the modifier state and the layout's letter; pasted
text goes through each field's own vanilla rules: StringUtil.filterText
and setMaxLength for EditBox, line breaks kept and the 1024-character and
14-line limits for MultiLineEditBox, formatting stripped and whole-or-
nothing width checks for TextFieldHelper on signs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new particle types now come from the events that spawn them in
vanilla.
- Level events 3003 (wax on), 3004 (wax off), 3005 (scrape):
ParticleUtils.spawnParticlesOnBlockFaces, three to five particles on
each face, from the client level's random. They are sent by honeycomb
on a block (HoneycombItem.useOn), axe scraping and wax removal
(BlockTransformer's TransformParticle.send), dispensed honeycomb, and
copper golems being waxed or scraped.
- Level event 3002: a struck lightning rod sends sparks along its axis
(ParticleUtils.spawnParticlesAlongAxis); without an axis they cover
the block's faces.
- Eating: LivingEntity.spawnItemParticles, five crumbs every
four ticks and sixteen when consumed (Consumable.onConsume), placed in
front of the eyes and thrown along the look, as item particles in full
colour. They were greyed terrain crumbs above the eyes. They now
follow Consumable.shouldEmitParticlesAndSounds with the remaining ticks
before the countdown, as LivingEntity does: six times over 32 ticks,
not seven, each one a tick earlier.
- Snowballs (Snowball.handleEntityEvent(3)): eight item crumbs at rest.
Eggs (ThrownEgg): eight scattered by up to 0.04 a tick.
- Slime, magma cube and sulfur cube landings (AbstractCubeMob.tick):
size × 16 item_slime, flame or sulfur_cube_goo particles around the
body, from draws the mob's random also makes on the server.
New harness tools/test_emitter_particles_parity.py: the real
ParticleUtils emitters and LivingEntity.spawnItemParticles.
Part of #63
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On Linux, once the launcher's file has been replaced under the running
process (an update from the command line or another window), current_exe()
is "<path> (deleted)" and self_replace failed with "No such file or
directory". The new binary now goes to the path the launcher started from
(copy beside it, rename over it; Windows still uses self_replace), and an
update the file already has is not downloaded again. The message reads
"Updated to X. Restart the launcher to use it."; Restart execs the new
binary with the same arguments (spawn and exit on Windows) and says so if
it cannot start.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher prepares a version in-process when it ships the preparer sources
the launcher was built with, with the version's own horizon-prepare when they
differ, and with the Python script only for versions made before it. Release
folders ship horizon-prepare and its sources; resources are reused between
versions with the same preparer. The game's messages, PLAY.txt, the docs and
release builds name horizon-prepare, and tools/prepare_native_client.py is a
wrapper of it for the development tools.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A port of tools/prepare_native_client.py and the helpers it imported from
lab.py and the test_*_parity.py modules: the player's pinned game files are
imported and checked against research-lock.json, the client's assets and data
copied, the vanilla exporters compiled and run with the JDK (JAVA_HOME, the
PATH, or java_home on macOS, at the pinned java_major), and the resources and
their manifest written byte for byte as the script wrote them, with its
sha256-keyed skips. tools/compare_prepare.py checks that against the Python
preparer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon's engine knew 91 of vanilla's 128 particle types. These come from
their real providers, constructor to tick:
- sweep_attack (AttackSweepParticle)
- glow, wax_on, wax_off, electric_spark, scrape (GlowParticle and its
providers)
- squid_ink, glow_squid_ink (SquidInkParticle: sinks through air)
- item, item_slime, item_snowball, item_cobweb, sulfur_cube_goo
(BreakingItemParticle: a quarter of the item's ground-model sprite in
full colour, unlike terrain crumbs)
- flash (FireworkParticles.OverlayParticle, its sine pulse and fading
alpha), which firework explosions now show as vanilla's Starter does
- firefly (FireflyParticle: wanders, fades in and out, vanishes inside
blocks)
- trial_spawner_detection(_ominous) (TrialSpawnerDetectionParticle)
- trial_omen, raid_omen (SpellParticle)
- ominous_spawning (FlyStraightTowardsParticle, colour lerped like
ARGB.srgbLerp)
- spit (ExplodeParticle falling at 0.5), fishing (WakeParticle),
dust_plume (DustPlumeParticle)
- red/orange/yellow_poplar_leaves (FallingLeavesParticle.PoplarProvider)
- pause_mob_growth, reset_mob_growth (SimpleVerticalParticle)
- block_marker (BlockMarker)
ParticleType.getOverrideLimiter now lists every type vanilla registers
with it (gust, sonic_boom, sculk charges, campfire smoke, vault_connection
and the new ones were missing), so they show beyond 32 blocks.
The /particle commands take the new types; item particles resolve the
stack's ground model.
The particle_providers harness now builds all 120 types Horizon implements
through the real providers (1428 cases).
Part of #63
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villagers showed one angry, happy or heart particle where vanilla's
AbstractVillager.addParticlesAroundSelf shows five, spread over the body
with Gaussian motion. Taming and love bursts were spread about the middle
of the body rather than over it, and drew from the world random.
particles::around_self places a burst as the client handlers do: per
particle three Gaussian speeds, then getRandomX(1), getRandomY() + y,
getRandomZ(1). Burst holds the counts, speeds and heights vanilla uses,
and entity_event_burst maps entity events to them. The client side uses
the particle engine's random, so no server draw moves.
- villager hurt by a player, failed breeding (13), breeding hearts and
the child's (12), trades, job sites and beds (14): five particles about
the villager, not one 1.8 above its feet.
- wolf, cat, parrot, nautilus and horse taming (7/6): seven hearts or
smoke about the body.
- allay duplication (18): three hearts about the allay.
- llama growth from food: one particle at rest about the body.
- animals in love: the heart moves at the Gaussian speeds already drawn,
exactly where vanilla puts it; forced growth's particle sits where it
was placed (sendParticles with no spread) instead of being jittered.
New harness tools/test_entity_particles_parity.py: the real client
handleEntityEvent of Villager, Animal, TamableAnimal, AbstractHorse,
Ocelot, Dolphin and Allay against entity_event_burst and around_self.
Part of #63
Co-Authored-By: Claude Opus 5.5 <[email protected]>
F3 only toggled the debug overlay. 26.3 makes the debug keys rebindable
mappings (key.debug.*): the modifier (F3) held with another key runs
handleDebugKeys, released alone toggles the overlay unless it was used as
a modifier; F3+Escape pauses without the menu; F3+C held ten seconds
crashes the game.
- debug_keys: KeyboardHandler.keyPress's state (usedDebugKeyAsModifier,
the modifier's down state, debugCrashKeyTime) and tick, with the actions
in handleDebugKeys' order; unit tests replay key sequences.
- gizmos: 26.3's gizmo lines (colour, width in pixels, near-plane clip as
GizmoFeatureRenderer.buildLines), ChunkBorderRenderer's geometry and
EntityHitboxDebugRenderer's boxes, eye lines and view arrows; a coloured
line pipeline draws them, depth tested or always on top.
- game/debug_keys: the actions. F3+A rebuilds every section, B hitboxes,
C copies the location (/execute in ... run tp @s, %.2f half up), D
clears chat, G chunk borders, H advanced tooltips (options.txt), I copies
/setblock or /summon with data (Shift: client side), N toggles spectator
and the previous mode with /gamemode's feedback, P pause on lost focus,
T reloads resource packs, V dumps the version, X improved transparency;
F4 without permission says debug.gamemodes.error. Messages carry the bold
[Debug]: prefix in yellow (red for the crash countdown).
- PauseScreen(false): only "Game Paused" at the top; the
pauseOnLostFocus option now decides whether losing focus pauses.
- The clipboard through arboard; `horizon-ctl status` reports the debug
state and the text last copied.
Fixes#73
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every vanilla Particle owns a RandomSource.create() of its own; providers
draw from ParticleEngine.random, a few from ClientLevel.random, and spells
from SpellParticle.RANDOM. Horizon drew everything from one generator, so
draws landed on the wrong stream. Particles now carry their own random,
and provider, level and spell draws use theirs, as in vanilla.
Fixed against the real providers:
- gust / small_gust: SingleQuadParticle(x, y, z) constructor, so the
12..15 tick lifetime comes out of the right draws.
- explosion_emitter, gust emitters: NoRenderParticle draws no quad size.
- effect, instant_effect, entity_effect, witch, infested: x/z motion from
SpellParticle.RANDOM; the witch's brightness from the provider.
- landing_honey, landing_obsidian_tear, falling_nectar, spore_blossom_air,
crimson_spore, warped_spore, white_ash, sculk_charge(_pop), dolphin:
provider draws (lifetimes, motion, alpha) from the provider random, after
or before the sprite as the provider orders them; crimson spore motion
scaled by the float constants; composter lifetime from the level random.
- enchanted_hit: MagicProvider's 0.3/0.8 tint after the constructor tick;
damage_indicator lives 20 ticks (DamageIndicatorProvider).
- note: colour from Mth.sin's table.
- shriek: one random sprite, not animated.
- trail: Particle(x, y, z, xa, ya, za) constructor, ARGB.scaleRGB on the
8-bit channels, Mth.lerp towards the target including the last tick.
- firework sparks: full bright (SimpleAnimatedParticle.getLightCoords).
New parity harness tools/test_particle_providers_parity.py: the real
ParticleResources providers build every type Horizon implements in a
stand-in ClientLevel (air, optionally a stone floor); the oracle recovers
each particle's clock-seeded random and the probe replays it; state,
requested particles and sounds are compared tick by tick.
Part of #63
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Powder snow was inert: the player's collision context never carried its
fall distance or boots, block-inside effects never ran for the player,
frozen ticks stayed 0 and the empty bucket only took fluids.
Player (PlayerMotion, LivingEntity.aiStep order):
- PowderSnowBlock.getCollisionShape gets the player's fall distance and
leather boots: it sinks through, walks on top in leather boots (sinks
when crouching), lands on the 0.9-high shape after a fall of more
than 2.5 blocks.
- Entity.applyEffectsFromBlocks over this tick's movement path (the
ported block sweep, StepBasedCollector steps): entityInside makes it
stuck (0.9, 1.5, 0.9) while its feet are in powder snow and sends the
server's snowflake from the level random; FREEZE sets isInPowderSnow
and adds a frozen tick per step; EXTINGUISH destroys the snow if the
player burns and puts the fire out. ServerPlayer.onInsideBlock fires
enter_block for every block entered (which also unlocks the boat
recipes in water, and the end gateway advancement).
- baseTick's wasInPowderSnow lets leather boots climb out (living_air).
- Server freezing: thawing 2 a tick, the powder_snow frost modifier
(-0.05 x percent frozen, not over air), freeze damage every 40 ticks
when fully frozen; leather armor (#freeze_immune_wearables) and
spectators don't freeze.
- HUD: the powder snow outline overlay and frozen hearts.
Items and blocks:
- BucketItem.use on powder snow: pickupBlock (2001), used statistic,
fill sound, FLUID_PICKUP, createFilledResult, filled_bucket trigger.
- SolidBucketItem: the empty_powder_snow place sound and an empty bucket
back in survival.
- Falling blocks land on powder snow, or end up inside it and break
after a fall of more than 2.5 blocks.
test_native_scene_motion gains powder snow fixtures ("snow_tick": the
oracle runs vanilla's real applyEffectsFromBlocks and freezing) and
prints frozen ticks, the snow flags and the stuck multiplier.
Fixes#62
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Taming any wolf also granted The Whole Pack and A Complete Catalogue: entity
predicates treated minecraft:components as always true, so every
tame_animal criterion of those advancements (one per wolf or cat variant)
matched the first animal tamed. EntityPredicate tests them with
DataComponentExactPredicate against what the entity reports through
Entity.get (Wolf.get(WOLF_VARIANT) and so on): mobs now report their variant
component (wolf, cat, frog, pig, cow, chicken, zombie nautilus) and the
predicate compares it, an absent component never matching.
Checked against the real EntityPredicate of every criterion of the variant
advancements on real entities of every variant by
tools/test_entity_components_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Choosing packs in the Resource Packs screen only reloaded the GUI atlas
(and only when it kept its size), item models and languages; block
textures and models changed after a restart, and sounds, clouds and the
world border never did. Block textures were also squeezed to 16x16, and
the pack overlay took every file from the top pack, so a pack's own
en_us.json, sounds.json, font or atlas definition replaced vanilla's
instead of adding to it (all other text, or every vanilla sound, gone).
- Minecraft.reloadResourcePacks: rebuild block sprites and models, mesh
tables, the shared atlas, item models, sounds, languages, GUI
metadata, clouds.png and forcefield.png from the new overlay; swap the
mesher's context (workers read it per job), mesh every loaded section
again (LevelRenderer.allChanged) and build a renderer with the new
textures. Nothing switches if loading fails.
- The block sprite array takes the largest texture's resolution (power
of two, at most 256 MiB), so 32x/64x packs keep their detail;
animations upload at that size.
- Files vanilla reads as resource stacks are merged in the overlay:
ClientLanguage (keys override), SoundManager (sounds add up unless an
event replaces; a sounds.json Gson rejects is skipped whole),
FontManager (top pack's providers first), SpriteSourceList (sources
bottom up). Languages are read from every namespace, not only
minecraft.
- Missing or broken block textures use vanilla's missing texture; packs
that still break startup fall back to the default resources
(Minecraft.rollbackResourcePacks).
- Open Pack Folder opens the folder on macOS and Windows too.
tools/test_resource_pack_parity.py checks pack discovery, overlays,
filters, which pack provides a file and the merged languages, sounds,
fonts and atlases against vanilla's FolderRepositorySource,
MultiPackResourceManager and reload listeners.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Quick move, the recipe book's place-all, its search box and the hold-Shift
tooltips read self.keys, which holds remapped and toggled keys: with toggle
sneak, a Left Shift press behind a screen is dropped by the sneak mapping and
sync_toggle then removes ShiftLeft. Vanilla reads the event's own modifiers
(InputWithModifiers.hasShiftDown) or Screen.hasShiftDown: the physical keys.
Track them in physical_keys and read them through has_shift_down.
Clicking outside the window did nothing. AbstractContainerScreen.mouseClicked
sends slot -999 when hasClickedOutside (AbstractRecipeBookScreen's rule when
the screen has a book): a held stack starts a quick craft drag whose release
sends PICKUP -999, which AbstractContainerMenu.doClick turns into a drop of
the stack (left) or one item (right), thrown from the hand with a swing. The
mouse handling is now horizon_client::container_screen, checked against the
real screens by tools/test_container_screen_parity.py; servers get the same
ContainerClick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla keeps xBob and yBob on the LocalPlayer: applyInput eases them
halfway to the player's rotation every tick, and
FirstPersonHandsAndItemsRenderer turns both hands by
(viewXRot - xBob) * 0.1 degrees about X, then (viewYRot - yBob) * 0.1 about
Y, before drawing either (FirstPersonHandsAndItems.extractRenderState
interpolates the bobs). Horizon drew the hands fixed to the view.
- first_person::HandSway: the bobs, applyInput's tick and the rotation.
- The game ticks it with the hands and resets it where vanilla makes a new
LocalPlayer (joining, respawning, changing dimension, leaving the End);
hand_geometry turns the arm, held items and maps by it.
The first-person harness's steps can turn the player: the oracle sets the
LocalPlayer's rotation and calls the real applyInput, and both sides print
xBob, yBob, xBobO and yBobO every tick (turning scenarios plus random turns
in the random cases).
Fixes#66
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Placing fire with a fire charge played item.firecharge.use at the flint and
steel's pitch (nextFloat * 0.4 + 0.8, one draw). FireChargeItem.playSound
uses (nextFloat - nextFloat) * 0.2 + 1.0, two draws from the level random.
The pitch is now horizon_client::items::fire_starter_pitch, checked against
the real FireChargeItem.playSound by tools/test_fire_starter_sound_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Game rules lived twice: Level (loaded when the world opened, written to
data/minecraft/game_rules.dat by Level::write) and the rules the game and
/gamerule use. Every save wrote the file first with the rules loaded at
open, then again with the current ones; a failed second write (on Windows a
file just renamed into place can be held briefly by the indexer or an
antivirus) was only logged, leaving the old rules on disk, so the world
opened with them again.
- sync_level copies the current rules into Level, and Level::write is the
only writer of game_rules.dat (written as vanilla writes saved data:
`_new`, then renamed over the old file).
- The world is saved when the application ends without a window close
(ApplicationHandler::exiting): on macOS, Quit (Cmd+Q) ends the
application through applicationWillTerminate, while vanilla's GLFW window
turns it into a window close and Minecraft.stop saves the world. Window
close, `horizon-ctl quit` and exiting share save_for_exit, which saves
once.
Tests: a unit test writes changed rules through Level::write and reads them
back (GameRules::load and Level::read); the save round-trip harness has
Horizon's player change a rule with /gamerule before saving and requires the
new value in the world vanilla then loads.
Fixes#74
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Animal.mobInteract plays playEatingSound(), which is empty in Animal and
overridden only by armadillos, cats, foxes, frogs, goats, pigs, sniffers and
sulfur cubes (plus nautili). Horizon fell back to entity.generic.eat for every
other animal (cows, sheep, chickens, rabbits, turtles, bees...), with a level
random pitch instead of each class's volume and pitch.
- mobs::ai::eating_sound: the overrides with their exact volume and pitch
(makeSound with the voice pitch, playSound(1, 1), or Level.playSound with
Mth.randomBetween(0.8, 1.2)); silent otherwise.
- mobs::ai::feed_animal: Animal.mobInteract's food part, shared by the game
and the mob AI probe; Strider.mobInteract adds entity.strider.eat.
- AgeableMob.ageUp(seconds, true): forcedAge and the forced-age particle
timer, whose positions draw from the mob's random on the server.
- Tame wolves and cats healed with food: TamableAnimal.feed (wolves silent).
- Parrots: Parrot.isFood is false; wild ones take parrot food with
entity.parrot.eat and a one-in-ten taming chance.
- Baby pigs and chickens use their sound variant's baby_sounds.
The mob AI harness gains a `feed TICK MOB ITEM` event (the oracle calls the
real Animal.mobInteract) and feeding scenarios for adult and baby cows, pigs,
sheep and chickens.
Fixes#68
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Right-clicking a composter with wheat (or any compostable item) did
nothing: ComposterBlock.useItemOn wasn't implemented, only emptying a
full composter. Hoppers and droppers didn't see the composter as a
container either, and a hopper under one sucked items through it.
- useItemOn: below level 7 a compostable item adds a layer with the
chance of its compostable component's provider (evaluated in the
block-interaction loot context, drawing from the level random), then
level event 1500, the used statistic and one item consumed (none in
creative); level 7 still consumes the action; a full composter falls
through to useWithoutItem and empties.
- Level event 1500 (handleFill): composter.fill(_success) and ten
composter particles above the compost.
- getContainer: the input container takes one compostable item from
above (hoppers, droppers, crafters), the output container gives a
hopper below the bone meal of a full composter, emptying it twice as
vanilla's setChanged calls do (and losing it when the hopper is full);
levels 7 and other faces refuse.
tools/test_composter_parity.py runs right-clicks, hoppers above and
below, and droppers into composters at every level through
VanillaDispenserOracle (new "use" and "hopper" actions) and the game's
dispense fixture.
Fixes#70
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cloud mesh radius came from the render distance, the far plane was
capped at max(4 x render distance, 1024) and the cloud fog end was a
constant 2048, so lowering the render distance cut the clouds short.
Vanilla ties all three to options.cloudRange: CloudRenderer.prepare
builds ceil(range * 16 / 12) cells, Camera.depthFar is
max(4 x render distance, range * 16) (also the hand projection, the
frustum and the world border), and AtmosphericFogEnvironment ends
cloud fog at min(range * 16, visual/cloud_fog_end_distance).
The cloud render harness now checks the radius for Cloud Distance
2..128 against the shipped mesh builder, and the environment harness
covers visual/cloud_fog_end_distance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A click in the furnace menu reset the cooking timer whatever it did.
AbstractFurnaceBlockEntity.setItem restarts cooking only when the input
slot gets something other than the same item with the same components,
and menu clicks that take part of the input or merge into it never call
it. Furnace::input_clicked applies that after each click, with the new
input's total cook time.
test_menu_click_parity gains furnace_be, blast_furnace_be and smoker_be:
the server's furnace menus over real block entities, comparing the
cooking data slots after every click.
Fixes#69
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon stretched a quarter of rain.png over each whole column, up to
20 blocks tall, so streaks were about five times too long. Vanilla
advances v by a quarter per block and lets the sampler repeat, so the
texture repeats every 4 blocks. Port createRainColumnInstance,
createSnowColumnInstance and prepareInstances exactly (seeded column
speed, game-time scroll, snow drift and brightening, camera-block size
table, square radius from the Weather Radius option, alpha falloff,
precipitation at the camera's y), and split each quad at the texture
seams so the atlas sprite repeats.
tools/test_weather_render_parity.py compares the columns and quads
with the shipped renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bind resident environment kernels and real fire clocks, fix sprite uploads and flame material, restore swim speed, throttle item lava audio, and emit fire extinguish events. Reset world-entry progress state without deleting saved data. Exact axolotl replay parity remains deferred to issue #64.
The README keeps the parity table, and its known differences move to a
Known issues section. The website drops the What's next section and its
nav links, and no longer fetches or requires a Roadmap section. The
launcher drops its What's next page, `next` subcommand and `roadmap`
MCP tool.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Lists the GitHub contributors of ludovic111/horizon (bots left out, the
owner first) with avatar, login, commit count and a link to the profile.
Built in by build.mjs like the releases (the build still succeeds when
GitHub does not answer, and the block stays hidden until the page loads
it) and refreshed by the page from the API. Adds a nav and menu entry,
lets avatars.githubusercontent.com through img-src, and updates the
README, LEGAL.md and the privacy note.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A chunk in the decoration range whose diagonal neighbour falls outside the
generation range never gets its 3x3 protos, so it must not hold back the
higher-ranked chunks within two of it. Render distances 2, 5 and 12 load and
keep loading while flying without stalling.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new harness decorates a 13x13-chunk area around each site, on both sides,
in the order vanilla's chunk scheduler uses around a player standing there
(chunk_dump HORIZON_LIST_ORDER prints decoration_order::vanilla_order), and
compares every block state, quart biome, block entity and scheduled tick:
the two reported spots on seed 1891623539983467248 (taiga trees at
204 -239; a stronghold with sculk and a geode at 4870 401), eleven tree-heavy
biomes and the nearest mineshaft, village, ancient city and trial chambers of
two seeds. 21 sites, 3,549 chunks, 0 differences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
StructurePiece.placeBlock schedules a tick for the fluid of every block it
sets (the lava of a stronghold's portal room, a fortress's lava wells, water
in a monument or a mineshaft); in a WorldGenRegion it lands in the proto
chunk and runs once the chunk is live. The port treated it as a no-op, so
those fluids sat still until a neighbour changed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Biome decoration is order dependent: a chunk's features write into its
neighbours, so a tree, ore blob or sculk patch placed first changes where the
next chunk's features fit and how many random draws they take. Horizon's
streamer decorated whichever chunk had its 3x3 protos ready, nearest first but
spread over non-overlapping regions for parallelism; around a teleported
player that swapped 45% of the overlapping chunk pairs against vanilla (whose
own runs swap 6%), and grew visibly different forests (seed
1891623539983467248 at 204 108 -239), ore veins, sculk and geodes.
worldgen::decoration_order replays vanilla's chunk scheduler for a standing
player: loading tickets ring by ring in fastutil hash order, four in flight,
FULL tasks decorating their 3x3 squares x-outer by level, INITIALIZE_LIGHT
tasks themselves, a ticket released once its 5x5 square is full. The streamer
ranks chunks with it whenever the player's chunk or the render distance
changes and decorates a chunk only when no lower-ranked chunk within two of it
is still waiting, so chunks further apart still decorate in parallel. Around
that spot the decorated blocks that differ from a vanilla server fall from
about 9000 to about 1000 (two vanilla runs differ by about 200).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaTerrain::finish dropped the proto chunk's block and fluid ticks; vanilla's
LevelChunk takes them over from the ProtoChunk (unpackBlockTicks), so the leaf
re-measurements and the fluid ticks of lakes and springs run once the chunk is
live. They now travel in the chunk's ChunkExtras, like a saved chunk's ticks,
and register with the level's scheduler on arrival; block ticks are translated
from worldgen's default state ids to the game's block indices.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both dumps list each proto chunk's scheduled block and fluid ticks in
scheduling order (`t block|fluid NAME X Y Z PRIORITY`) and the harness
compares them: they run once the chunk is live, and the leaf ticks of
updateShapeAtEdge were missing without any block differing at FEATURES.
HORIZON_ORDER names a file of `x z` lines both sides decorate in, instead of
row-major: decoration is order dependent (a chunk's features write into its
neighbours), and vanilla's chunk system decorates around a player in its own
order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
StructureTemplate.updateShapeAtEdge (every tree, structure pieces) and
Block.updateFromNeighbourShapes run updateShape on a WorldGenRegion, where
LeavesBlock.updateShape schedules a one-tick block tick to re-measure its
distance and waterlogged blocks schedule fluid ticks: vanilla keeps them in
ProtoChunkTicks and runs them once the chunk is live. Horizon ran updateShape
against a throwaway tick scheduler and lost them, so leaves where trees
overlap kept the distance their own tree gave them (one more than vanilla's
for thousands of leaves in a taiga), and could decay where vanilla's don't.
level::Ticks gains a capture mode; the decoration world schedules what a
shape update captured, in order, before the update's own setBlock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts with #51: entity.wgsl keeps the crumbling sprite UVs and
samples with the per-face colour; gfx.rs keeps crumbling_sprite and
outline_params and puts the Nether flag in time.y; game.rs keeps #51's
breaking and cloud geometry. #51's new vertices (breaking decals,
cloud faces, the batching test) get the new zero normal, so they stay
unshaded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test_dedicated_server_parity summons a librarian beside the spawn and the
vanilla protocol client right-clicks it: the OpenScreen packet (title as
Entity.getDisplayName, UUIDs left out) and the MerchantOffers packet must
match vanilla's server. Villagers keep Xp, LastRestock and RestocksToday
before their offers are made (vanilla's null offers).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host hands a trading screen a guest's interaction opens to that guest:
OpenScreen (minecraft:merchant), MerchantOffers when the offers change,
the select trade packet, container clicks run on the merchant menu engine,
the TRADE reputation event and prices for the guest's UUID, the payment back
on close. As a client, Horizon shows a server's trading window from its
MerchantOffers and sends select trade and clicks. Stored enchantments, dyed
colours and map ids now travel in item stacks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The trading screen runs MerchantMenu on the menu engine with the villager's
merchant::Session: level-ups add offers while it is open, prices follow the
TRADE reputation event a tick later, orbs drop at the villager with its own
random, the yes/no voice and regeneration as vanilla; special prices stay
after closing. Offers use the world's RandomSequences (now saved as
random_sequences.dat), the villager's position, its namespaced type (desert,
snow... trades) and exploration maps found through /locate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the gfx.rs conflict with #42 and keep the shared Globals uniform
consistent: Rust, terrain.wgsl and entity.wgsl now all end with
time, crumbling_sprite, outline_params. The automatic merge left
terrain.wgsl reading outline_params at crumbling_sprite's offset. A test
compares the three declarations.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaTradingOracle runs villagers, wandering traders and MerchantMenu on a
real flat server; trading_fixture runs horizon_client::merchant and a new
merchant kind of the menu engine (MerchantContainer, MerchantResultSlot,
tryMoveItems, quick move) over a merchant::Session. Generation now covers
random dyes, random potions, stew effects, exploration maps (a MapMaker hook),
sum/binomial providers and the trade's max uses / xp / discount providers;
getRecipeFor honours the selection hint as vanilla, Offers leave defaults out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Choosing Classic in Options > Interface left no way back. The classic
Video Settings page, which already starts with Horizon's graphics row,
gets a Horizon-only "Menu Style: Classic" button below it: it switches to
Horizon's menus, saves horizon.txt and opens Options > Interface. The
classic main Options screen keeps vanilla's exact layout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client's MotionWorld.apply_movement_emission pushed a footstep event
on every Entity.move, and the game played the block's step sound for
each. Gravity moves a grounded player every tick, so standing on grass
played block.grass.step 20 times a second, overlapping into an endless
rustle (19 instances alive at once). The audio stream had been muted by a
saved WirePlumber setting until v2026.10.13.1, which hid it.
Route the player through horizon_world::movement_emission (vanilla's
applyMovementEmissionAndPlaySound: moveDist, nextStep, the supporting
block checks) with Player.getMovementEmission (nothing while flying or
sneaking on the ground). The status sound census now lists the events
holding channels, which is how the loop was found.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The creative-tabs.json export failed to build the functional blocks tab (no
data-pack registries: banner patterns), and its fallback read decompiled
sources from a path that no longer exists, so the tab was empty and anvils,
crafting and smithing tables were missing from the tabs and the search. The
search also matched only an approximate display name.
VanillaCreativeTabsExporter now builds every tab with vanilla's own
CreativeModeTabs.tryRebuildTabContents over the vanilla data pack's
registries and writes creative-inventory.json: stacks with components, and
the search tab's creative tooltip lines and tags. The client searches them
as SessionSearchTrees does (FullTextSearchTree, IdSearchTree after '#').
Changing tab empties the search box, as in selectTab.
The scrollbar only jumped to the pressed point; holding it now scrolls as
the mouse moves, anywhere, until release (mouseClicked/mouseDragged), and
the wheel and the scroller follow ItemPickerMenu.
tools/test_creative_parity.py compares every tab and 6,793 searches with
the vanilla client's own search trees. horizon-ctl `type` also fills the
creative search box.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Hardware LineList forced one-pixel strokes, while the guessed 0.002 endpoint growth did not separate the edges from block surfaces. Render screen-space strips with vanilla framebuffer width scaling and perspective/view depth layering, leaving the shape edges unchanged. Apply normal outline fog and add three regression tests.
Offline gate passed on this main-based branch. Actual vanilla GLSL vs Horizon WGSL GPU comparison covers 3,072 vertices; maximum projected difference is 0.0036621 pixels. No code from PR #42 is included.
README gets an Agent control section (out of the roadmap: it's done), the
native client guide describes every command group, the blocking semantics,
exit codes, launch discovery, MCP and the two transports, and PLAY.txt tells
release players about horizon-ctl.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
release.yml builds horizon-ctl beside the client (checked for system-only
imports like it), packaging/package.sh copies it from beside the client binary
and refuses to package without it (the macOS folders made by hand go through
the same script), and package_native_client.py adds it to private archives.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a virtual screen: create a world through the title menus, walk to a tree,
break a log, craft planks in the inventory screen, place one and read it
back, once with horizon-ctl commands and once over horizon-ctl mcp.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One command table drives horizon-ctl help, COMMAND --help (with examples)
and the MCP tools. Replies stay one JSON line; errors are error objects with
exit status 1 (game), 2 (usage) or 3 (unreachable). script runs a file or
stdin with shell-like quoting. launch finds the game beside horizon-ctl or
the newest launcher install (--version, --game), with its prepared resources
and the game home, and --wait waits for the world. horizon-ctl mcp serves
MCP over stdio in the launcher's style, one tool per command, with the
screenshot returned as an image. Existing commands and replies are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
game/agent.rs adds commands an agent can play with: state, looking-at,
entities, block, blocks, find-block, inventory, screen and messages read the
game as JSON; move, jump, goto (A* over standable blocks), look-at, attack,
break, place, interact, use-item, select, drop, open-inventory, click-button,
click-at, command, respawn, pause, wait and stop act like a player. Actions
that take time run as a task advanced every frame and reply when done, with
what happened (break reports drops and pickups, place the block placed).
Buttons are clicked by label: the classic and Horizon button drawers record
what they draw each frame (widgets.rs), and a click is a synthesized pointer
move and press, so it behaves exactly like the mouse on every screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Windows has no Unix domain sockets, so the control endpoint there is a random
127.0.0.1 port described by a JSON file (port and a 128-bit token, readable by
the user only); a client's first line is auth TOKEN. Linux and macOS keep the
Unix socket, and any endpoint path ending in .json picks the TCP transport, so
it is tested on Linux too. control::connect and Stream serve both.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Launcher 0.1.3 auto-installed v2026.10.13.1 unprepared; after updating to 0.1.4 nothing installs
it again, so install's reuse never ran for it. backend::reuse_prepared_everywhere() applies the
same reuse (same compatibility checks, copied then renamed) to every installed version that is
not prepared, oldest first. It runs when the window starts (on a thread; the page refreshes when
it is done), in the CLI's `versions` and `update`, and in the MCP list_versions and check_updates
tools. The partial folder is now named per copy, and a version prepared meanwhile is left as it is,
since the window's start and an install can copy at the same time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
After installing a version (CLI install and update, the window's automatic install, MCP),
when it is not prepared and another installed version with the same Minecraft version is,
the launcher copies that version's prepared resources. Players who auto-updated to
v2026.10.13.1 no longer prepare again, and can play it without their Minecraft folder.
The prepared output depends on the Horizon version as well as Minecraft's: the manifest
records the hashes of the vanilla exporter sources it was made with, and
prepare_native_client.py writes every file and the manifest's schema. So resources are reused
only when both versions have the same pinned jars in research-lock.json, the same
prepare_native_client.py and the same exporter sources the manifest lists. They are copied
(prepare rewrites files in place, so hard links would tie the versions together), into a
partial folder renamed when complete. install and the MCP tool say when it is prepared already.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A banner in the website's design (Instrument Serif wordmark over a dimmed
sunset, purple accent), rendered from docs/media/banner.html with Playwright.
- Screenshots of Horizon's own menus and graphics settings, the in-game scenes
as WebP (from the website's media), and the launcher 0.1.3 Play and Patch
notes pages; the JPEGs the README no longer uses are removed.
- A "What changed" section matching the v2026.10.13.1 release notes.
- Download only through the launcher (playhorizon.dev or launcher-v releases).
- Roadmap: performance optimisation, mod support and agent control.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The crack overlay and particle emitters assumed a unit cube. Use selected baked model quads and projected decal UVs for cracks, outline bounds for mining crumbs, and per-box density for the destruction burst. Keep translucent overlays after translucent terrain.
Distinguish model offsets from outline offsets and preserve double-precision shape coordinates. Add regression tests and actual-client differential oracles: 2,740 model cases and 2,740 particle cases, each passing three runs after the upstream merge. Particle enqueue positions match exactly; model max error is 1.40395e-7. Offline gate, camera/first-person/cloud comparisons and Vulkan smoke checks pass.
Shared CI runners hit GitHub's anonymous rate limit in the smoke test (launcher 0.1.2's
macOS build failed twice on it). Players never set the variable, so nothing changes for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The launcher's palette, violet accent, Instrument Serif and Geist, without its sidebar
and settings rows: a full-viewport hero over Horizon's own sunset (slow drift,
parallax, grain), then sections for what it is (a statement that lights up as it
scrolls), a screenshot mosaic, the parity count and a grid of the 14 subsystems, the
launcher beside Horizon's own menus, the latest patch notes, the roadmap, community
and the footer. Reveals, staggered letters and magnetic buttons stop under
prefers-reduced-motion; a translucent top bar and a full-screen phone menu replace
the sidebar.
playhorizon.dev: canonical, og:url, absolute og/twitter images, sitemap.xml and its
robots.txt line, and a 301 from *.up.railway.app (Railway's healthcheck host is still
served).
The full-page screenshots are no longer committed (about 17 MB): `npm run shots`
writes them to the gitignored shots/. The unused blurred background is gone; LEGAL.md
lists the hero and the settings screenshot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
links.json gains a website key; the launcher shows a quiet Website link under
Join Discord and lists it in the links CLI command and MCP tool.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
lab.prepare imported every client library in the version JSON, ignoring
its OS rules, so a Linux install failed on the macOS-only
java-objc-bridge the official launcher never downloads there. The rule
evaluation moves from runtime.py into lab.py and one selector,
client_library_artifacts, now serves preparation, the runtime and the
model baking harness: OS rules as the launcher applies them, and the
natives for this architecture (classifier suffixes, plain natives being
the x64 build only when an ARM variant is listed). Required libraries
are still checksummed and a missing one still errors.
Mojang republished 26.3.json twice (702fe591, then 11f1aefe, the current
one) with only assetIndex changed: non-English translations, same jars
and libraries. The lock pins the current file and accepts both earlier
ones; preparation takes the launcher's copy so it matches the asset
index installed with it, and an unknown republish errors clearly.
Fixes#41
Co-Authored-By: Claude Opus 5.5 <[email protected]>
website/ holds Horizon's site: the horizon-theme tokens and bundled fonts, the
launcher's sidebar, rows, buttons and segmented control, over Horizon's own frozen
landscape. The parity table, harness count and roadmap are generated from the README
at build time; releases, patch notes, the roadmap and links.json are refreshed in the
browser from GitHub, with the build's copy as fallback. The download button picks the
newest launcher-v* asset for the visitor's OS.
Legal: the Minecraft disclaimer in the hero, footer and 404; no Minecraft logo or
wordmark; no game files; the launcher's background (licence unconfirmed) and the
screenshots showing it are left out; LEGAL.md lists every asset and its licence. No
cookies, analytics or third-party assets.
Railway: root directory website, Dockerfile (Node build, Caddy on $PORT with a strict
CSP). Checked with Playwright at 360-1920 px and 1250x700 (docs/widths/); Lighthouse
96/100/96/92 on mobile, 100/100/96/92 on desktop.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villager POI search: main's per-search state classification replaces this
branch's state table (same sections skipped, same positions in order).
status keeps main's mob and block-entity counts and this branch's pending
uploads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- links.json on main gives the Discord and sponsor links (built-in fallback offline,
empty hides the button); 'links' command and MCP tool.
- Install new versions automatically is on unless explicitly turned off (new settings
key, so 0.1.1's written default does not count as a choice).
- Settings: Horizon / Classic menu style, the horizon.txt menuStyle line the game reads;
'menu-style' command and MCP tool.
- launcher workflow: install latest into a temp home, check imports (Windows, Linux client)
and that the client starts, on all three platforms; also runs on launcher-* branches.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Horizon options shell's Video section now holds the graphics settings:
a Simple / Advanced switch (remembered in horizon-graphics.txt), then the
level's rows, each with its frame-time cost and one-line description
(ui_theme::described_row). Horizon settings draw as switches, sliders or
value buttons like vanilla options. The Classic style keeps the Video
Settings buttons to the same screens. Main's menu blur and the world's
resolution-scale pass are separate (post.wgsl, world_post.wgsl).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
At render distance 32 the single section arena grew past the 4 GiB buffer
limit and the renderer panicked. Sections now live in pages of at most
256 MiB (or the device's buffer limit); draws keep their order and bind each
page once per run of consecutive draws in it, so translucent sections stay
back to front. docs/graphics.md describes the settings, the benchmark and
what changed; graphics_bench.py --compare prints a before/after table and
keeps each game's log.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AcquirePoi scanned every block within 48 of the villager (about 460,000),
naming each with string matches, every 20 to 40 ticks for each villager missing
a home, job site or meeting point. Villages gain such villagers over time
(babies, more villagers than beds), so the game thread's work grew with play
time: in a village, 18% of samples at the start and 30-42% eight minutes on,
frame rate dropping and staying low.
Block states are classified once per search, and 16^3 sections whose palette
holds no POI block are skipped whole (PoiManager only visits sections with
records). The same positions are visited in the same order, so the result,
ties in the distance sort included, is unchanged. Same session after: 18%
throughout, frames at the vsync cap.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a resolution scale other than 100% or FXAA on, the world renders into
its own target at the scaled size and one full-screen pass copies it
(bilinear) or anti-aliases it (FXAA 3.11) onto the screen; the hand, screen
effects and HUD stay at full resolution. Off, the world renders straight to
the screen as before. Vanilla's Mipmap Levels option now limits the mip
levels block textures sample (the default 4 is the full chain, as before).
Both are in the Advanced screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Video Settings opens two Horizon screens (graphics_ui): Simple has the
Performance / Balanced / Quality / Ultra presets with render distance, FOV,
max framerate, VSync and GUI scale; Advanced groups every knob under
Rendering, Quality, Display, Performance and Overlay. Each row shows a
one-line description and its frame-time cost, and applies live. Layout lives
in graphics_ui, every colour, size and widget call in graphics_style, so a
menu theme can restyle them in one place.
Horizon-only knobs (horizon-graphics.txt, never options.txt): preset, cave
culling, chunk updates and upload budget per frame (nearest sections first),
sky, fog, weather, animated textures, present mode, frame pacing, worker
threads and the performance overlay (FPS, frame/CPU/GPU times and a
frame-time graph). Defaults keep vanilla's rendering; vanilla options keep
their options.txt keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Unloaded chunks and entity chunks are encoded and written by a background
region writer, in order; loads see queued records first, and synchronous
saves wait for the queue. Deleting entity chunks a region doesn't hold no
longer rewrites the region file.
- Mobs render within Entity.shouldRenderAtSqrDistance (64 x bounding-box size
x entity distance) and inside the view frustum (EntityRenderer.shouldRender);
block entities outside the frustum are skipped, beams and structure blocks
excepted.
- Minecarts and dropped items tick only in entity-ticking chunks, like mobs.
- Villager POI searches skip sections whose palette has no POI block.
- Streaming bookkeeping uses the Fx hash and checks a column's light
readiness once per frame instead of once per dirty section.
- HORIZON_SPIKES=MS prints the timed spans of slow frames; frame summaries
give the phase split of the slowest 1%.
RD 32 village flight on the 4080: 81 -> 230 fps; 1% low 5.8 -> 18.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A column that finished loading or generating after the player moved out of
range was dropped, but its block entities and saved entities were added to the
world first. The entities spawned in a column that never became resident, so
nothing saved or removed them: travelling, mobs piled up (141 to 5,028 in an
8-minute flight), and when the column loaded later its entities spawned again,
so villages near the view edge doubled their villagers. Such a column is now
dropped with everything in it. Mesh results for a column unloaded meanwhile are
discarded too, rather than uploaded with nothing left to remove them.
Decoded static sounds were kept for the whole session: every sound heard, up to
1.4 GiB of samples, and a village plays hundreds. They now stay within 64 MiB,
least recently used out, and are decoded on a thread of their own instead of the
game loop (vanilla's buffers load asynchronously too). Stopping a source that
is still waiting for its sound stops it, as alSourceStop does, so a late decode
cannot start it.
status reports mobs, block entities, resident GPU sections and the sound
engine's tables, for soak tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The --sprint-input scene drives the real LocalPlayer.aiStep sprint and
jump path from keyboard state. Add sprint-jump runs: sprint held or
started by double-tapping forward, jump tapped, held through landings or
pressed first, at five headings, with too little food to sprint, under a
two-block ceiling and up onto a slab. All 293,175 states match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Terrain sections live in one vertex arena (first-fit free list, grown on the
GPU) and draw with one multi_draw_indexed_indirect per pass instead of a
buffer bind and draw per section; per-frame scratch vectors are reused.
- The SectionOcclusionGraph search runs over dense arrays around the camera
instead of hash sets, with per-column visibility.
- Mobs outside the entity-ticking chunks (Chebyshev distance of the
simulation distance, DistanceManager.inEntityTickingRange) only check
despawning, as in ServerLevel.tick; before, every loaded mob ticked.
- World chunks use the Fx hash; sky darkening is sampled once per time and
weather instead of once per mob; creaking-heart scans compare state ids.
- Horizon-only graphics settings (horizon-graphics.txt): frame pacing without
spinning, present mode, animated textures, worker threads.
RD 32 village flight on the 4080: 3.1 -> 81 fps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The local player's clicks now go through a port of Minecraft.startAttack,
continueAttack and handleKeybinds' attack part with MultiPlayerGameMode's
block breaking (first_person.rs): attacking the air swings (and sets the
miss time), creative breaking swings every tick through its destroy delay,
survival mining keeps swinging after a block breaks, swords cannot break
blocks in creative, and attacking an entity in creative sets the delay.
The swing is LivingEntity.SwingState (restart rules, durations under haste
and mining fatigue, forward wrap on restart); the equip height follows
FirstPersonHandsAndItems with the item swap ticker, and placing or
starting to use an item drops the hand (itemUsed). Mining fatigue scales
by Math.pow(0.3, amplifier + 1) as in 26.3.
test_first_person_parity drives the real startAttack, continueAttack,
startUseItem, MultiPlayerGameMode breaking, SwingState, the tickers and
FirstPersonHandsAndItems over 4,192 scripted click sequences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options open as the launcher's Settings page: a sidebar with the sliding
selection (Video, Audio, Controls, Accessibility, Language, Chat, Interface)
and rows with a switch, slider or value button per option
(options_horizon.rs; new sections or rows slot into SECTIONS). Key binds,
the language list and resource packs open vanilla's pages. Interface holds
Menu style (Horizon / Classic). The pause menu hides the in-game HUD over the
blurred world. Scripted and frame-limited runs default to Classic, so the
harnesses see vanilla's screens.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
One theme for the launcher and the game: crates/horizon-theme holds the
palette, type scale, spacing, radii, control sizes, motion and the bundled
fonts; launcher/src/theme.rs and the game's ui_theme both read it. ui_theme
ports the launcher's components (button with hover layer and press state,
sidebar list with the sliding selection, segmented control, switch, rows)
and draws Geist and Instrument Serif through the HUD atlas.
The title is laid out like the launcher (sidebar, page header, rows) over a
live backdrop: Horizon generates an ice-spikes landscape for a fixed seed,
orbits a camera over it with falling snow, then blurs and cold-grades the
frame (post.rs). The launcher's opening (wordmark, then the interface) plays
once per session and a click or key skips it. Menu style Horizon / Classic
lives in horizon.txt, never options.txt; Classic keeps vanilla's drawing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GameRenderer.render3dHud sets up the hand's projection at Camera's hudFov
(70, changed only by death and fluids), never the FOV option or the
sprint, speed and bow modifier. The hand pass now has its own globals
with that projection (a port of JOML setPerspective, as Projection does).
The camera harness compares the world and hand projection matrices at FOV
options 30, 70, 110 and others with vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-ctl gains 'fly' (a constant-speed camera path that replies with the
frame summary); frame summaries add CPU time per frame (waits for the GPU,
display and limiter excluded), per-phase averages and GPU time from timestamp
queries. --offscreen renders into a texture on the discrete GPU so the 4080
can be measured under the headless compositor. Baselines: lavapipe on vscreen
and the RTX 4080 offscreen, seed 1234.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Windows launcher was built without crt-static and imported VCRUNTIME140.dll,
which only the Visual C++ redistributable installs: players without it got a
missing-DLL dialog before anything started. The game itself links only system
DLLs (vulkan-1.dll is loaded at run time).
- launcher.yml links the C runtime on Windows.
- tools/check_imports.py reads PE import and delay-import tables (and ELF
loader, needed libraries and run paths); release.yml and launcher.yml fail
when a Windows binary imports a non-system DLL.
- release.yml builds Linux on Ubuntu 22.04: the NixOS-made Linux release asked
for a /nix/store loader, so it ran nowhere but NixOS.
- No adapter: the game says the Vulkan driver is missing and what to install,
and on Windows keeps the console open until Enter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PipeWire remembers mute per application name. A test run's stream, muted once
in the mixer, left "PipeWire ALSA [horizon-client]" muted for every later game,
the release played from the launcher included: no sound at all, with the device,
the stream and the mixer all working.
- The launcher starts the game as "Horizon" (media.role Game), its own entry.
- The game opens no output on a vscreen, where agents run it (HORIZON_AUDIO=1
forces it, for a null sink), so nobody needs to mute it again.
- Any device sample format is accepted (the mixer converts from f32): a plain
ALSA default reporting i16 used to fail.
- Other output devices are tried when the default does not open, and the log
names the output opened or every reason none did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Removed (each unreferenced by code, workflows, tests or living docs; no harness
dependency record names any of them, and parity.py --list still finds all 281):
- 22 one-off write-ups of the Java-pack experiments in docs/: adaptive-terrain,
client-graphics-smoke, compact-headers, core-performance, culling-empty,
culling-rays, debounce-queue, dispatch-retry, foreground-first,
graphics-api-comparison, heap-budget, mac-first-test, optimization-plan,
playable-validation, quad-reuse, server-graphics-context, shader-world-smoke,
terrain-measurements, terrain-position, terrain-read-wait, tick-membership,
worker-pacing. Their findings are folded into one paragraph of java-packs.md.
- 24 benchmarks/ observation dumps only those write-ups linked (client graphics,
culling, queues, heap, Mac Vulkan, shader worlds, software identity, and the
unlinked rust-core, rust-worldgen-2026-10-05, rust-native-client and
rust-cuboid-model-decoding runs), plus 12 unlinked files inside kept
report folders. The scoped-parity reports the living docs cite stay.
- debug/: one server log from a chunk investigation.
- The manual macos-shader-research and macos-vulkan-research workflows.
- tools/logs/latest.log, an empty log committed by accident (now ignored).
Kept: research-lock.json (parity.py, packaging, the launcher and datafix read
it), the Java-pack how-tos (vulkan, collectors, profiling, process-memory,
tick-state) and tools/test_client_graphics.py (ci.yml runs it).
README: leads with what Horizon is and "Download the launcher" (the
launcher-v* releases; the launcher updates itself), then build from source
for developers. Direct game downloads and PLAY.txt steps for players are gone;
the launcher has its own section with two screenshots. The roadmap merges the
known differences, the final checks and the plans after parity, under the
"Roadmap" heading the launcher's What's next page reads. The parity table and
its 100%-of-what-the-harnesses-compare note are unchanged.
docs/README.md indexes what remains: build and run, parity (status and
workflow), world generation, world data and simulation, the old Java track.
Links to the removed pages and README anchors are fixed.
packaging/PLAY.txt now speaks to players who came through the launcher and
points anyone with a bare archive to it, keeping the manual steps short.
The launcher's README link points at main instead of the launcher branch.
.gitignore: tools/logs/ and .DS_Store. No code behaviour changes; the compiler
and clippy flag no unused code and the tree has no stale TODOs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client ran in the version's own folder, so saves and options.txt would have
landed there and been deleted with the version, and the saves in
~/.local/share/horizon (where the release folders' horizon.sh keeps them) were
not seen. It now runs in HORIZON_HOME, else horizon in the platform's data
folder, with --resources pointing at the version.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_client::chat::ChatScreen is now a port of vanilla's ChatScreen,
EditBox and CommandSuggestions: the input's scrolling, cursor, selection,
word moves and select all, history up and down, argument colouring, usage
lines and parse errors, the suggestion list (sorting, hover, Tab and
Shift+Tab cycling, clicks, wheel) and its tooltips, drawn as GUI calls the
game paints. tools/test_chat_screen_parity.py drives a real ChatScreen on
vanilla's client command tree (custom suggestions answered by the server
dispatcher): 3000/3000 cases match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EmptyItemInHotbarFix was missing from the data fixers. horizon_client::hotbar loads the nine rows of nine stacks of an older file as vanilla does: lenient counts, unregistered items empty, stray entries dropped from their row, a row that is no list ending the reading, short rows lost and long ones cut.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The options' Packs page is horizon_client::pack_screen: vanilla's layout,
lists, focus and selection, search box typing, the mouse wheel per list and
the confirmation of an incompatible pack (Escape answers no). Packs carry
their raw description, compatibility (too old, too new, unknown) and icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_client::pack_screen ports PackSelectionScreen, its two
TransferableSelectionLists and the ConfirmScreen of an incompatible pack as
vanilla's state machine (clicks, wheel, search typing, focus, selection,
scroll) and gives its GUI calls; screen_draw holds the widgets (StringWidget,
MultiLineTextWidget, Button, a font-aware EditBox port) and StringSplitter's
line splitting with legacy codes. Pack components keep their legacy codes and
style fields. tools/test_pack_screen_parity.py runs a real
PackSelectionScreen through the same actions: 1500/1500 cases match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- ClientboundUpdateRecipesPacket at join: the recipe property sets and the
stonecutter's recipes, from the data pack (lan/recipes.rs).
- On a player's first tick the minecraft:tick criteria are met: the recipes
their advancements grant go out in ClientboundRecipeBookAddPacket, then
ClientboundUpdateAdvancementsPacket with what the player may see
(lan/advancements.rs).
- Command replies reach remote players as the components vanilla sends:
Text keeps the Component it was built from (literal, translatable,
siblings, style), sendFailure wraps the message as vanilla does, and the
NBT wraps the elements of mixed lists.
The dedicated-server harness drops its known gaps: it compares these
packets (sorted where vanilla's hash maps order them per run) and the
replies to three failing commands, keeps only the first set_time among the
joining packets and turns random ticks off in the generated world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Book::ghost is the one place that places a recipe's ingredients into the grid or furnace slots; the harness runs fillGhostRecipe against the real menus. Fixes found: the result and the dyed, potion and map demo ingredients cycle through every stack (not the first), a furnace's fuels follow the registry's component lookup, and items keep their registry order.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The recipe book view harness now searches: names, parts of names, ids with a colon, odd texts.
Horizon's recipe tooltips lacked attribute modifiers, smithing template, potion, dyed, pot, rocket and disc lines, and the results a display cycles through (map copies, tipped arrows, dyed armour).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The LAN and dedicated servers built the tree from the data generator's
commands report, which sorts each node's children. lan/command_order.rs,
generated from vanilla's Commands(CommandSelection.ALL) by
tools/generate_lan_command_order.py, gives the registration order, and the
dedicated-server harness now compares the tree in order (the game rules,
registered in an identity-hashed order, aside).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon_client::packs::SelectionModel ports PackRepository's selection and
PackSelectionModel (default positions, fixed and required packs, moving up
and down, commit), and the pack screen uses it. The new pack_selection
harness drives vanilla's model and Horizon's through 4,000 random action
sequences; every state matches.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a busy machine the /list reply sometimes came after the client had
left. Each step now waits for a new chat line, up to 15 s.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon generated noise terrain in minecraft:flat worlds. Since chunk
streaming finishes the corners of the spawn neighbourhood, save_roundtrip's
flat test world got four noise chunks where vanilla had saved flat proto
chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The HUD harness's chat cases now also draw the chat as the open chat
screen does: foreground, at the focused height, scrolled, with the scroll
bar.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The world was saved 20 wall-clock seconds after force-loading the area, so
on a busy machine chunks were still generating or their fluids flowing,
and random ticks changed blocks while the client was being sent its
chunks: which chunks differed varied from run to run. Wait until every
force-loaded chunk is loaded and 600 game ticks have passed, with
random_tick_speed 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The golem's goals are replaced by vanilla's brain (core and idle activities,
the transport cooldown, look-at-player-sometimes, stroll or wait), and
TransportItemsBetweenContainers moves into the library: nearest wanted
chest in chunk and block entity map order, queueing behind golems with a
chest open, the 60-tick interaction with its states, sounds and opener
counts, picking up and putting down (with vanilla's overcounting), visited
and unreachable positions. Weathering draws from the level random after the
living tick; the spin sound is client-local. mob_ai gains seven copper golem
scenarios that print each golem's state, hand and opened chest and every
chest's contents and openers; all match vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Profiles' textures properties are unpacked as authlib does and looked up
as SkinManager does (signatures, the local player, allowed texture URLs,
downloads), skins are fetched and processed as SkinTextureDownloader does
and shown from atlas slots. The tab_list harness now runs a real
SkinManager and compares processLegacySkin over 400 PNGs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DataFixerUpper reads the data once with its own schema: an entity tree of an id that
schema lacks stays untyped for good, and a list of trees is typed only if all of them
are. Spawner potentials follow that, the spawner wrapping skips untyped data, and a stack
without a string id is an empty equipment slot. The raw maps entities remember are taken
again where the fixes of before 1.14 end.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Mac mini no longer excludes options, chunk_generation,
world_border_render and mob_ai: their float maths now matches vanilla's
on Apple silicon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Beardifier, density pow/log, Marsaglia's Gaussian, dripstone, ores, fancy
trunks, icebergs, end spikes, strongholds, note pitches, sculk charge
particles, the warden's and brains' weighted shuffles.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rust's float functions use the platform libm, and vanilla's own Math
results differ between x86-64 (Intel stubs, close to glibc) and AArch64
(fdlibm, with sin/cos evaluated using fused multiply-adds). jmath picks
the same implementation HotSpot uses on the machine, so options, the
world border's alpha and RandomPos match vanilla on an Apple silicon Mac
as well as on Linux.
The vanilla protocol client no longer records the game-time sync sent
every 20 ticks: whether one arrives before the first chunk batch depends
on when the client joined.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
datafix_scene_12.py builds one scene for 1.8 to 1.12 from per-era tables (blocks, entity ids and merged types, riding, equipment, potions, items probed on the server, achievements by command instead of advancements).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
horizon_client::recipe_book_screen lays out RecipeBookComponent's drawing
(background, search EditBox, tab buttons, the filter CycleButton, the page
of RecipeButtons with its number and arrows, the alternatives overlay,
tooltips) and GhostSlots as vanilla's GUI calls with their poses, and the
game paints them. The new harness builds vanilla's widgets for 600 random
states (VanillaRecipeBookScreenOracle) and compares every call: 600/600.
Ingredients keep their HolderSet order (tags in TagLoader order), which is
how the overlay and ghost slots cycle; the book centres over the menu on
narrow screens as widthTooNarrow does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
GuiBannerResultRenderer's flag model and pattern layers, lit with
Lighting.Entry.ITEMS_FLAT and clipped to the 20x40 rectangle, through the
GUI item pipeline. The block entity render harness gains loom result cases
(the oracle submits vanilla's renderToTexture under the PiP pose): 407/407.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/datafix_scene_12.py builds it in 1.12 command syntax: the whole block id/data space, block entities, items, entities, player, scoreboard, advancements, maps and statistics (real ones from a bot, plus a few hand-written fixtures).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The anim harness now runs vanilla's TheEndGatewayBlockEntity on a client and
a server stand-in (triggerCooldown every 2400 ticks and on entry, block event
1) and compares the beam render state; 40 cases match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle's player stays 5000 blocks east between scenarios, and
Mob.checkDespawn draws for it once a mob idles past 600 ticks
(warden_alone).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Warden.nextStep: a step every 0.55 walked; mob steps raise a step
game event, which the mob AI probe delivers to wardens through the
listener the game uses (warden::listen).
- Warden.tick runs the vibration ticker before super.tick.
- The probe applies melee hits on mobs, reads a warden's target from its
brain, and plays the player's death sound for a lethal hit; warden_player
and warden_cow stop before the killing blow.
- getViewVector(0) uses the head's previous yaw, and AirAndWaterRandomPos
the float quarter turn: tempted ghastlings stroll as vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Animal babies and dropped items stay out of the comparison, as before the
merge: the probe does not model them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A zombie villager's profession comes from its constructor's random, not
the one finalizeSpawn draws from. Allays and withers are no longer marked
persistent to escape despawning: they are kept as removeWhenFarAway and the
wither's checkDespawn keep them, and canPickUpLoot follows the allay's and
the sulfur cube's overrides. A sulfur cube's size sets its attribute
bases, and strength and speed effects count in attribute values. The
oracle's stand-in level gets the sound seed random Level.playSound draws
from.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The parent's half of VillagerMakeLove.tryToGiveBirth (eating, the child's
bed and type, resting) runs in its own tick, before it ages; the partner's
follows as an event. A baby's NearestBedSensor keeps its bed when the
reused path heads elsewhere, as vanilla checks for a POI there. Footsteps
come from the block under the feet (Entity.mainSupportingBlockPos), not
the column below the centre. The oracle faces newborns' heads with their
bodies (LivingEntity draws the head yaw from Math.random), and the breeding
scenarios leave one free bed per section (PoiManager hands them out in hash
order).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Gossip and gifts between villagers (TradeWithVillager) move into the
library so the mob_ai probe runs them as the game does; both villagers now
lock their gaze on each other, as lockGazeAndWalkToEachOther does. The
probe reads the oracle's villager flags, prints villager data (food,
inventory, gossip, reputation) and adds babies as they are born. The
oracle seeds TriggerGate's shuffles too, which were left to the clock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaProtocolClient joins a server with vanilla's own codecs;
test_dedicated_server_parity.py compares a vanilla and a Horizon dedicated
server serving the same world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The shrieker summons the warden during the block ticks, so it ticks in the
same tick's entity phase: the oracle and the fixture now seed it (its
random, sensor delays and RunOne shuffles) before that phase, not after.
The heartbeat is a client sound: its pitch no longer draws from the
level random, and the fixture leaves it out as vanilla's server never
sends it. sculk: 435/435.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps both sides' oracle scenarios: the creaking hearts, hurts, leads and
animal flags of parity-base on this branch's entity list, with a heart's
creakings adopted under their creation seed. Restores the villager meeting,
birth and golem-check events the unfinished WIP had begun to replace.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Streaming kept, decorated and generated chunks within circles a fixed
margin wider than the render distance, but finishing a chunk needs its
3x3 neighbours decorated and theirs generated: squares whose corners
fall outside those circles. At render distance 2 the corner chunks around
the player never finished, so their neighbours never lit and the player
was never placed. Each step now also keeps what a finished chunk's
neighbourhood needs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Mac mini and ThinkPad no longer keep the shared vanilla data, so the
other machines are opt-in again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The control socket reports itself unavailable on Windows, which has no
Unix domain sockets, and horizon-ctl uses the same stream type. A release
workflow builds the Windows client by hand; packaging/package.sh assembles
the binary, the resource preparation tool and PLAY.txt, without any of
Minecraft's files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Number the server files cases, whose folders eula-true and eula-TRUE collide
on case-insensitive macOS file systems; keep world_border_render and mob_ai
off the Mac, where platform float math changes their results; give the local
executor the dead flag the flake check reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped on
2026-10-12; may not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped on
2026-10-12; may not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped on
2026-10-12; may not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped on
2026-10-12; may not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped on
2026-10-12; may not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A lead gives its mob a home at the holder (PathfinderMob.whenLeashedTo)
and every random position helper honours homes (generateRandomPos-
TowardDirection's lean back and GoalUtils.isRestricted), as the happy
ghast's float area now does too. LlamaFollowCaravanGoal keeps 2.1F, a
llama's baby and the breeding experience are drawn from the parent's
random, wolves run from llamas by strength (WolfAvoidEntityGoal), a
growing mob refits its box (fudgePositionAfterSizeChange) and a happy
ghast's new controls want nothing. Scenarios can lead mobs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelChunk.removeBlockEntity calls setRemoved: a jukebox reports its song
stopping (the jukebox_stop_play game event and level event 1011) and a
beacon plays its deactivation, whatever the flags. A record popped from a
replaced jukebox stops a playing song first and updates the neighbours as
JukeboxBlockEntity.onSongChanged does (a redstone wire beside it can pop).
The block update oracle removes block entities the same way.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChunkHeightAndBiomeFix, ChunkProtoTickListFix, ChunkRenamesFix,
OverreachingTickFix, SpawnerDataFix, the world generation settings fixes
and the Caves & Cliffs biome and advancement renames, with the chunk's
Level tag and UNTAGGED_SPAWNER in the schema walker. A 1.17.1 sample
world upgrades as vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mobs::creaking_heart is CreakingHeartBlockEntity: the ticker drawn from
the level random, the state following the night and the logs, the
protector spawned with SpawnUtil.trySpawnMob (five tries within 16
blocks, on top of a collider, then EntityType.create's yaw and
Mob.finalizeSpawn's follow range bonus), its sounds, letting it go by
day, beyond 34 blocks or with a player stuck in it, and when a player
hurts it the trails and the resin spread over the logs. The game ticks
its hearts through it, and its level random now carries on between
ticks.
The mob AI harness gains hearts: the oracle places them when a scenario
starts, ticks block entities after the mobs, seeds the creakings the
level creates and adds them to the scenario, prints each heart's state
and block entity, and lets the player hit a mob (hurt T I). Five heart
scenarios (night, day, uprooted, no player near, hit twice) match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LoomScreen.extractBackground (slot icons, scroller, pattern buttons
with their swatches and tooltip, the result banner or the six-layer
error) and CrafterScreen's background, disabled slots and redstone
arrow become screen_layout calls the game paints. A new harness runs
vanilla's screens on stand-in menus and compares 1,600 call lists.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scheduled and random block ticks now run after the player's digging
and before any entity, so items a dispenser shoots or a block pops get
their first tick on the tick they appear, as vanilla's do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The subtitle overlay now lays out GuiDraw calls in hud_overlays and the
game paints them like the other overlays. Hover events that show an entity
save its UUID as an int array, as UUIDUtil.CODEC does. The save round trip
builds a vanilla scoreboard with every saved field (objectives, number
formats, display slots, locked and displayed scores, teams with every
option) and requires Horizon to save scoreboard.dat as vanilla did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DedicatedServerProperties with every key, default, parser and serializer,
java.util.Properties load/store, Eula and the four StoredUserList files,
checked against the pinned server JAR by test_server_files_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The copper golem takes its weathering's texture and glowing eyes, saves
and loads next_weather_age and weather_state, starts with its weathering
unset instead of weathering on its first tick, and throws what it carries
to the player as BehaviorUtils.throwItem does. Iron golems offer their
poppy to copper golems too (#candidate_for_iron_golem_gift) and put it on
an empty antenna when the offer runs out beside one.
The entity model harness now poses the copper golem with its idle and
chest interaction animations (44 cases), and the entity death loot harness
includes it (1,595 cases over 88 types); both match vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Old server jars run on Mojang's own Java runtimes, pinned in
research-lock.json and installed beside the shared vanilla data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the integration branch's StringSplitter port and HUD overlays: the tab
list now emits hud_layout::GuiDraw calls and wraps with its split_lines, and
the vignette's border warning comes from WorldBorder::warning_strength
through hud_overlays::border_warning.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The raid bars now draw through hud_overlays. Villagers' raid field is
what getRaidAt finds where they stand, with lost raids and the wave flag
kept apart, so the brain's SetRaidStatus, ResetRaidStatus (which resets on
a lost raid), ReactToBell and raid packages read vanilla's conditions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Explosion drops keep their random sequences and the pot's dynamic drops;
/setblock destroy and /fill destroy run Level.destroyBlock through the
behaviour library, so the block entity spills as vanilla's does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs push through the new pushEntities with the teams' collision rules;
lang keeps the integration branch's deprecated.json handling; the
scoreboard keeps this branch's vanilla hash orders.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs set each other's brain memories within the tick (the breed target
and lockGazeAndWalkToEachOther), the partner rests and forgets at the
birth, and an animal in love draws its heart particle randoms. The adult
happy ghast's TemptGoal.ForNonPathfinders ticks every other tick and
follows creative players. The mob AI scenarios can give the player an
item and a mob love, taming, an age or a llama strength.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sample of 27 harnesses (the 20 quickest, options, datafix's six shards)
passed there; it builds against the same ALSA headers as the desktop.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The copper golem code (its goals, chest transport, weathering, statues and
interactions) was in the tree but not compiled; it is now a species with
its model, keyframe animations, sounds and game hooks. Its antenna flower
sits in the saddle slot as vanilla's EQUIPMENT_SLOT_ANTENNA, sheared by
players and dispensers with the unequip event and the 1.5-up drop, and
dropped with the carried stack as preserved equipment. A pumpkin on a
copper block builds it after the snow and iron golem patterns: both blocks
clear, the block becomes the matching copper chest (CopperChestBlock
getFromCopperBlock with the least oxidized of connected chests), and the
golem takes the block's weathering and plays its spawn sound. Weathering
draws from the level random, the statue pose from the golem's own.
The dispenser harness now builds copper golems and shears and equips them:
6000/6000 cases match.
Block.popResource of tall flowers and flower beds and the composter's
bone meal pop when the behaviour asks, as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientRecipeBook keeps the displays it was sent in a HashMap of display
ids, whose iteration order (buckets, then arrival) orders the
collections; the game now keeps such a client book (on the verified
Java HashMap port) synced with the server book, with display ids
numbered as RecipeManager does. A new harness runs the client's
ClientRecipeBook, RecipeCollection selection and the tab and page logic
of RecipeBookComponent over 1,200 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Eight scenarios: a breeze alone and fighting a player near, inside and
beyond its jump circle; a creaking alone, watched, unwatched, and woken
then hunting once the player turns away (a new player turn= option).
The oracle now faces the player +z with its head too (it kept a random
head yaw from its creation, which the breeze's jump target reads).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
monster_brain runs BreezeAi and CreakingAi as vanilla's Brain does:
the sensors with their random first delays, memories that expire, the
core, idle and fight activities, and each behaviour (Shoot, LongJump,
ShootWhenStuck, Slide, SlideToTargetSink, StartAttacking, MeleeAttack,
RunOne...) started and ticked in vanilla's order, with durations drawn
from the level random. It reuses the villager brain's memory slots.
Breeze.tick draws its particle count and whirl clock before the entity
tick; its whirl and idle sounds are local (client) sounds; it is immune
to fall damage and plays its landing sound. Entity.lookAt and the long
jump's atan2 (an fdlibm port) are bit-exact. A frozen creaking keeps
thinking while its navigation and controls wait, stopInPlace zeroes its
motion, and its attack sound follows the hit. Mobs update their ground
flag on every move, as a server's authoritative entities do.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brain behaviours draw from the level random (DoNothing, POI scans): the
game lends it to the mobs and keeps what they drew.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
parity.py --repeat N runs each harness N times; its runs must agree, else it
is reported FLAKY and recorded as failing. Later rounds run on the machine
that ran the first, reusing its oracle results, and never two at once there.
With --flake-check REF (passed by gate.py, merge base with parity-base; by
supervise.py integrate, the integration branch before the merge), harnesses
whose script, oracle or probe is new or changed must pass three runs in a
row; a pass records its rounds so it is not repeated again. gate.py matches
shards of a known failure by the harness's name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The overlay listens to the sound engine from the frame that renders it with
Show Subtitles on until one renders with it off (keeping its captions),
forgets plays as it renders, picks the latest play in range (the first of
full ties), fades in float arithmetic, lays the boxes out in integers under
the pose translation and takes its background from the text background
opacity options. Translations now apply the game's deprecated.json renames,
as ClientLanguage does ("Chick hurts"). tools/test_subtitle_parity.py runs
vanilla's SubtitleOverlay on 1500 scripted cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entity model harness now sets the warden's render state (tendril and
heart animation, the roar, sniff, emerge, dig, attack and sonic boom
animation states) and bakes its four emissive layers (tendrils, heart,
bioluminescent skin, pulsating spots) over WardenModel: 430 cases match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The known and highlighted recipes, the book settings and the saved
recipeBook compound now live in one place (recipe_book::Settings with
a RecipeIndex of learnability and display counts): advancement rewards,
/recipe give and take (counting displays, never special recipes) and
crafting a recipe award through it, and loading drops unknown recipes
instead of rebuilding the set from done advancements. A new harness
compares 1,500 operation sequences with vanilla's ServerRecipeBook and
its codec.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs die (LivingEntity.die: the death event, catalysts, loot and
experience) when they are killed, not when their death animation ends.
Catalysts take a death in the dispatcher's order (nearest by exact
distance, then visiting order); experience orbs draw their merge id from
the level random and their motion from their own. The sculk harness
gains catalyst scenarios: kills by the player near catalysts over
random terrain, with blooms, charge cursors and the sculk they spread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs on the player's team never target the player; a creeper's blast
spares its team mates when its team has no friendly fire; mobs push each
other only as their collision rules allow; name tags follow the team's
name tag visibility, show the team's prefix, suffix and colour and the
score of the objective displayed below names; the player's own death
message stays out of chat when the team hides death messages. The
criteria that follow the player compare health as a float, round armor
down and give auto-updated displays the player's team-formatted name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerPlaceRecipe and StackedContents' RecipePicker run on the menu
engine (horizon_client::menu::place) for the inventory, crafting table
and furnaces: testClearGrid, clearGrid with placeItemBackInInventory,
the biggest craftable stack, clamping to stack sizes and moveItemToGrid.
A new harness compares 6,000 placements with vanilla; vanilla's item
order follows identity hash codes, so the oracle hands its order to the
Rust side. The game's recipe book now places through the engine, and
its craftable filter uses the same counts and placement ingredients.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The adult's careful GhastMoveControl (the blocks around the destination
and swept on the way, #happy_ghast_avoids, fluids), HappyGhastLookControl,
HappyGhastFloatGoal, the still timeout of a player standing on top, and
getSuitableFlyToPosition returning its first good point. The ghastling
flies by its brain with getWalkTargetValue's preference for air just
above the ground. Flying travel drags by 0.91F and 0.8F as floats, and a
happy ghast swims as fast as it flies.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelChunk.setBlockState now runs BlockEntity.preRemoveSideEffects and
removes the block entity when a block with one is replaced by another
block (copper chests and copper golem statues keep theirs across
weathering; UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS skips the side effects).
Containers spill every slot through Containers.dropItemStack (spot and
pieces of 10 to 30 with triangular motions from the level random, empty
slots still drawing their spot), jukeboxes pop their record, lecterns
their book; shulker boxes keep their contents for the dropped box.
Campfires, decorated pots, chiseled bookshelves and shelves, which Horizon
keeps as loaded, spill too.
Block.dropResources now draws its loot and pop spots when the behaviour
calls it, not after it returns, and spawnAfterBreak awards ore, sculk and
spawner experience. Item entities and orbs the behaviour makes are added
before it returns to the game, in the same tick. Player mining follows
ServerPlayerGameMode.destroyBlock's order (removal and spill, tool wear,
then the drops); explosions let the removal spill the contents.
ExperienceOrb.award draws the merge id from the level random and moves
the orb with its own random. Decorated pots copy their decorations and
drop their sherds when cracked (loot tables' dynamic entries).
The block update oracle replays preRemoveSideEffects too, and the new
block removal harness (tools/test_block_removal_parity.py) compares
setBlock/destroyBlock/removeBlock on every block entity holder with
contents against vanilla: 2000/2000 cases match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
supervise.py check trial-merges every candidate branch into the integration
branch with git merge-tree, without touching a worktree: each alone, every
pair, then the largest set that merges cleanly together, in about a second.
integrate starts from that set (or every parity/* branch with new commits,
with --ready), gates it once, and if the gate fails tries each half, so the
branches that pass still land.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command registries never received the effect ids, so every /effect
and mob_effect argument failed to resolve.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every harness gave the same result on the Mac mini as on Linux, except
options and chunk_generation, where Rust's float functions use the platform
libm and round differently on macOS: those stay off the Mac. gate.py and
supervise.py no longer force 8 local harnesses, so parity.py keeps the
desktop to 3. The ThinkPad is listed (2 harnesses, nice 19) but not used by
default. Oracle runs get a 15-minute hang guard instead of 5, since one oracle
on a fully loaded machine can take more than 5 minutes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle spent 58 of every world's 62 seconds waiting for the idle
threads of the game's IO pool to time out; it now exits once its outputs are
written (same outputs on all 22 worlds). The harness runs in 6 shards, keeps
the vanilla outputs so the oracle's results are reused, and declares its Rust
scope (datafix, NBT, region files, compression and horizon-core positions):
other horizon-world changes no longer rerun it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
parity.py:
- A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1..NAME#N,
each told its share through PARITY_SHARD; NAME selects them all.
- PARITY_RUST_SCOPE lists the Rust sources a harness's probes can reach.
It applies only if closed (nothing in it names a module, crate-root
re-export or macro outside it, and nothing outside implements anything for
its types); crate roots then count without their other module declarations.
- Bytecode caches no longer count as dependencies.
- With other hosts, this machine runs 3 harnesses at once and only those
under 800 MB, admitted against the memory free at each start; hosts can
exclude harnesses and run at a nice level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The level random is passed down to the mob AI step as the integration
branch does, the brain runs in customServerAiStep's place, and the mob AI
probe builds the brain after seeding the mob and prints its activities and
memories. The oracle seeds every GateBehavior's shuffle with the mob's seed
(the same change the entity-ai branch makes) so RunOne picks are
reproducible.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Friendly fire (doTeamsAllowDamage), alliances, collision rules
(EntitySelector.pushableBy), seeing invisible team mates (isInvisibleTo),
name tag visibility (LivingEntityRenderer.shouldShowName) and who sees a
player's own death message. tools/test_team_rules_parity.py runs vanilla's
methods on stand-in entities and a stand-in client over 6000 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The searching sound and the digging block hit sound are the client's
(playSearchingSound, emitDiggingParticles), the digging sound starts with
the dig (SnifferSoundInstance), and the egg plop and the breeding
experience come from the parent's random in its brain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LoomMenu (selectable patterns, clickMenuButton, the six-layer limit,
quick moves, the result's onTake) and the server's CrafterMenu over the
block entity (the result following the grid, slot state packets, filled
disabled slots enabling) join the menu click harness, now 8,000 click
sequences over 19 menus with pattern button and slot state ops. Slot
clicks end with setChanged and container slots are limited to their
stack size as SimpleContainer does. The game's loom and crafter screens
now click through Game::menu_session on the same engine.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractHorse.aiStep draws a tail swish every tick before the goals run.
Mobs now see the mobs ticked before them as they are after their tick
(ServerLevel ticks entities one after another), in the game and in the
mob AI probe; a baby llama following its parent depended on it. The
llama's strength is read raw, as getStrength does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaSculkOracle runs sensor, calibrated sensor and shrieker scenarios
(scripted game events and block changes, the player's warning level) in
a real headless level ticked in ServerLevel.tick's order; the game runs
the same scenarios through its own sculk code (HORIZON_SCULK_FIXTURE).
Fixes found on the way: sculk block entities tick in the order they were
added, the entity phase of sculk runs before the block entities, client
side effects of level events draw from the client random, scheduled
ticks run one at a time so a shrieker summons inline, wardens are
summoned as SpawnUtil does (obstruction by entities, the yaw and
finalizeSpawn draws, agitated sounds of discarded attempts), and an
empty level no longer draws the level random every tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The objectives, holders and teams now iterate as vanilla's fastutil maps and
each team's members as its HashSet; criteria resolve to their canonical
names as ObjectiveCriteria.byName does; scoreboard.dat leaves out the
fields equal to the codec defaults and writes components as
ComponentSerialization does. tools/test_scoreboard_parity.py runs vanilla's
own Scoreboard against the model on 500 scripted cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The patrol spawner, the raid tick (countdown, spawn positions, wave
composition, bonus wave, leaders, riders, boss bar, horn seed, raiders
leaving, victory, loss, celebration) and the raids.dat codec with
DataFixerUpper's partial results and fastutil's map order now follow
vanilla line by line. tools/test_raid_parity.py runs vanilla's own
PatrolSpawner, Raids and Raids.CODEC on a stand-in level: 1,900 cases
match.
In game, raiders join, are loaded, hurt and killed through addWaveMob,
updateBossbar and removeFromRaid as vanilla calls them, the wave leader
map lives in the raid, recruits take the raid's current wave, raid and
patrol mobs keep their spawn rotation, and each dimension keeps its own
raids.dat. Bad Omen and Raid Omen act from inside the effect tick, with
the ConcurrentModificationException vanilla swallows when Raid Omen is
first added, and the omen position is saved with the player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SnifferAi and HappyGhastAi as vanilla's Brain runs them: the sensors and
their scan rates, the activities and their requirements, and every
behaviour (AnimalPanic, MoveToTargetSink, FollowTemptation, AnimalMakeLove,
LookAtTargetSink, RunOne and the sniffer's scenting, sniffing, searching,
digging, rising and happy states) in availableBehaviorsByPriority order.
Adds AirAndWaterRandomPos and the mob AI scenarios for llamas, sniffers
and happy ghasts. The brain is not ticked yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Holding the player-list key draws tab_list's layout: in single player the
local player with the list objective (vanilla shows nothing otherwise), on
a server the listed players from PlayerInfoUpdate (game mode, latency,
display name, list order, hat), the tab list header and footer, online
mode for heads and the server's objectives, scores, display slots and
teams, which now also feed the sidebar. The renderer draws the border wall
with RenderPipelines.WORLD_BORDER's overlay blend and the forcefield
texture, from border_render's buffer and transforms; the client border
follows the server's border packets and ticks between them. The vignette
reddens near the border.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CrafterBlock.dispenseFrom and CrafterBlockEntity.serverTick over 4,000
cases: recipes from the datapack with remainders and specials, disabled
slots, containers and other crafters in front. The crafter now reports
its level events (1049, 1050, white smoke 2010) through the client
random, hoppers merge stacks only with equal components, brewing stands
take items as a WorldlyContainer, and empty written book pages are not
written.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A worker started over ssh (parity.py --worker) runs harnesses on a mirror of
the worktree on another machine, here the Mac mini. One longest-first queue
feeds every machine, remote ones first; passes come back with the content
hash of each dependency, so they are keyed on exactly what ran. Harnesses
that need this machine are listed in tools/parity_hosts.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entity model harness now drives the sniffer's five keyframe animation
states and its search walk, the happy ghast body item, riding and leash
holder on its body, harness and ropes layers, and LlamaDecorLayer's model.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entity model harness now poses the breeze's six animation states and
bakes BreezeWindLayer's and BreezeEyesLayer's models and the creaking's
emissive eyes model (141 and 64 cases, no differences).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tab_list ports PlayerTabOverlay: the comparator (list order, spectators,
team, name ignoring case), the 80-player limit and 20-row columns, heads
with the default skin by UUID and upside-down names, team and spectator
name styling, ping bars, the list objective as integer scores in their
number format or hearts with their change blink and the hp text fallback,
and the wrapped header and footer. Font::split_lines now follows
StringSplitter.splitLines and Text::resolve_legacy applies legacy codes
inside components. test_tab_list_parity runs vanilla's overlay against
examples/tab_list_fixture: 1,000 cases, 3,476 frames, 844,758 calls match.
The protocol gains the scoreboard, team and world border packets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Client-ticked animation state now drives the renderer alongside the End
portal and gateway rendering; gateways, whose ages and cooldowns the client
does not track yet, keep rendering settled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the nbt contents hook and stack_display_name from parity/hud with
rendering-hud's object_style and first-component sibling style.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client border now wraps horizon-core's vanilla-checked BorderGeometry
(previous size for partial ticks, status, lerp speed, absolute limit) and
border_render ports WorldBorderRenderer: extract's fade and tint, the cached
wall buffer, the scrolling texture transform and which walls draw. The
vignette takes vanilla's int colour and the warning strength from the
border. test_world_border_render_parity runs vanilla's renderer against
examples/world_border_render_fixture: 1,500 cases, 16,859 frames match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
JigsawPlacement.generateJigsaw builds the pieces with the GenerationContext's
WorldgenRandom (large feature seed from the level seed and the position's
chunk) and only places them with the level random. Horizon assembled them from
the level random, so the village layout, and the plants /fill later broke
around it, changed from run to run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villagers ran an approximation built from goals. They now run vanilla's
Brain: expiring memories, the nine sensors with their randomly delayed
scan clocks, activities from the villager_activity timeline, and every
package's behaviours (core, work, play, meet, rest, idle, panic, pre-raid,
raid, hide), started and ticked in Brain.availableBehaviorsByPriority's
order with RunOne's shuffles, the level random for behaviour timing and
POI scans, and Villager.refreshBrain when the profession changes.
The mob AI probe prints the activities and memories as the oracle does,
and the harness's three villager scenarios now match: 34/34.
Sleeping villagers take LivingEntity.SLEEPING_DIMENSIONS instead of being
frozen in travel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShulkerBoxBlock.canOpen: a closed box opens only when the half-open lid
box is free of collisions. Bells gather and test entities by their real
bounding boxes and liveness. Client block-entity tickers run with
ClientLevel.tickBlockEntities, which Minecraft.tick skips while paused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Player.attackStrengthTicker and itemSwapTicker: both advance each tick, a
different main-hand item resets them, and getAttackStrengthScale divides
by the ATTACK_SPEED delay. The game does not drive it yet; the attack
indicator still shows full strength until combat uses the cooldown.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The chat cases add messages (with the system and error tags) at given Hud
ticks and draw ChatComponent in background mode: wrapping, the 200-tick
fade, scale, width, height, spacing and opacity options. Horizon's chat now
wraps like StringSplitter.splitLines and ComponentRenderUtils.wrapComponents
(the indent on wrapped lines, breaks at the last space, a dropped newline
or space), keeps the options as doubles and draws through a layout with
vanilla's fills and text offsets. A component list whose first part has
empty text now still passes its style to the rest. The harness runs 200
cases per kind.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
save_entities forgot the UUID and compound of entities Horizon keeps but
does not simulate, so the save on quit gave them new UUIDs. The player's
equipment is written as LivingEntity does: omitted when empty, where
PlayerEquipment.isEmpty also counts the selected hotbar item.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The overlays cases run Hud.extractCameraOverlays and extractSleepOverlay:
the vignette with its brightness and world border warning, the spyglass
scope and its scopeScale easing (read back and compared), equipment
camera overlays, powder snow, the nether portal sprite and the nausea
swirl. The game feeds its full-screen camera overlays from the same
layout (the vignette option and first person now count), draws the
spyglass with vanilla's unclamped easing and the sleep fade at vanilla's
opacity under the sidebar, titles and chat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The entity shader gains rendertype_end_portal as layers 9 (15 portal
layers) and 10 (16 for gateways): end_sky and the rotating, scrolling
layers of end_portal.png projected from the screen, repeated inside their
atlas sprites, with vanilla's GameTime uniform. The world-space
approximation and its portal block scan are gone; the block-entity
renderer draws both blocks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every GateBehavior (RunOne, and the gates of TradeWithVillager and
VillagerMakeLove) shuffles its behaviours with a ShufflingList random that
RandomSource.create() seeds from the clock, so two oracle runs of the same
villager scenario diverged after the first RunOne. The oracle now gives
each gate a LegacyRandomSource seeded with the mob's seed, and seeds the
gates of a brain rebuilt by Villager.refreshBrain before its next tick.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The decor cases run vanilla's itemDecorations on stacks with counts,
durability (Item.getBarWidth/getBarColor), unbreakable stacks and item
cooldowns ticked through ItemCooldowns. Horizon's item drawing now uses
the same layout: the bar rounds and colours like vanilla's and the count
sits at vanilla's offsets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
block_entity_anim now also ports ChestLidController (chests, trapped and
copper chests, ender chests: triggerEvent(1, openCount), tickLid,
getOpenness), ChestBlock.opennessCombiner and ChestRenderer's lid curve,
and EnchantingTableBlockEntity.bookAnimationTick with its render state
(nearest non-spectator player by feet position within 3 blocks, Mth.atan2,
the class's shared RANDOM). The renderer pairs a double chest half with its
neighbour only as DoubleBlockCombiner does (same block, other half, same
facing) and reads the book from BlockAnim instead of ticking its own copy.
The game keeps a BlockEntityAnims and runs the client tickers of every
loaded animated block entity each tick: chest lids, books, shulker boxes,
bells (resonance sound, entity-effect particles, raider glow), conduits
(nautilus particles) and dragon/piglin heads. Opening and closing chests,
ender chests and shulker boxes sends the opener-count block event (both
halves of a double chest); a ring's block event reaches the bell through a
new LevelEvent::BlockEntityEvent. The renderer now gets this state instead
of BlockAnim::default(), so shulker lids, bells, conduits and heads move.
block_entity_anim harness: 256/256 cases (new: 48 chest, 36 book).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle fills a BossHealthOverlay with LerpingBossEvents, runs
JumpableVehicleBar on a jumpable mount and displayScoreboardSidebar on a
real Scoreboard with teams, display names and number formats. The game
collects the dragon, wither, raid and custom bars into one
BossHealthOverlay layout (the backgrounds and notches now draw in vanilla's
order) and draws the sidebar with the text background options and
String.CASE_INSENSITIVE_ORDER.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
TheEndPortalRenderer and TheEndGatewayRenderer: the position-only portal
cube in FaceInfo order (only the vertical faces for portals, the faces
Block.shouldRenderFace leaves for gateways) and the gateway beam through a
shared submitBeaconBeam. The beacon beam now goes through the same
function. The client does not track gateway ages and cooldowns yet, so
gateways in game render settled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Campfires and brushable blocks resolve their items (empty here) through the
context's ItemModelResolver, which was null. End gateways read the level for
their beam (max y, game time) and visible faces, so they get a stub level
with the case's neighbours, light, build height and game time. New cases
cover gateways spawning, cooling down and settled against neighbours, and
End portals beside the same neighbours.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The HUD harness now draws cases by kind. The oracle runs Hud.extractEffects,
extractTitle, extractOverlayMessage and extractSelectedItemName after
replaying setTitle/setTimes/setOverlayMessage and Hud.tick, and
hud_overlays lays out the same state: MobEffectInstance ordering through
Java's small-array TimSort, the title and action bar fades, the backdrop
from the text background options and the tool highlight timer. The game
draws its effect icons, titles and action bar through these layouts and
now shows the selected item name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An original logo (a sun setting behind a blocky horizon), a banner and a social
preview card, and six screenshots captured in the Rust client. The README opens
with badges, a short pitch and a gallery; a test keeps the parity badge in step
with the subsystem table. Packaged builds use the logo as their menu icon.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
F1 only hid the chat. Hud.extractRenderState hides the camera overlays,
crosshair, hotbar, effects, bars, sidebar, action bar, titles and chat, and
GameRenderer.renderItemInHand skips the hand; the sleep overlay, debug screen
and open screens still draw.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Restructure the README around what Horizon is, its status, how to build it
from your own copy of the game, and its parity table. The Java performance
pack material and the research lab notes move to docs/java-packs.md and
docs/research-lab.md. The target platform is stated generically instead of
naming a machine, the stale list of missing mobs is dropped in favour of the
audited table, and the legal notice follows Mojang's usage wording.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon no longer fetches anything of Minecraft from Mojang's servers. The
client jar, version JSON, libraries and assets come from the user's official
launcher install, and the server jar is one they downloaded themselves
(--server-jar or HORIZON_SERVER_JAR). Every file is checked against checksums
pinned in research-lock.json, and the prepared .local/ layout is byte-identical
to the old downloads, so the harnesses and the native client are unchanged.
CI keeps the offline checks on GitHub's runners; the vanilla comparisons and
the mod build need the game, so they move to a manual job on a self-hosted
runner that holds the maintainer's own copy.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The old server jars and the 22 worlds made from them are inputs, but they
lived in each worktree's .local and were lost when its outputs were
cleared. They now sit beside the shared vanilla data, written atomically,
and the harness generates any that are missing (under a lock, since
several worktrees may run it at once).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The wither's customServerAiStep (charging, side heads, block breaking,
healing) now runs inside serverAiStep, and aiStep draws the heads' smoke
particle offsets after travel as vanilla does; its steering uses the 0.6F
and Mth.atan2 float maths, and its level-event sounds (shoot, spawn, block
break) no longer draw from its random: MobEvent::LevelEventSound lets the
client pick their pitch. Entity.getGravity is zero while isNoGravity, which
the flying move control sets. Drowned swim with isUnderWater (the previous
tick's eye state, Entity.wasEyeInWater), and the eyes count as in water per
26.3's EntityFluidInteraction (between a water block's bottom and its camera
height) rather than the older 0.11 offset.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Projectiles shot by mobs (arrows, splash potions, snowballs, llama spit,
wind charges, crossbow bolts, tridents) take their spread, and arrows their
base damage, from the new projectile's own random as vanilla does, leaving
the shooter's random alone. The witch's player target goal reports as
NearestAttackableWitchTargetGoal (NearestTarget gains a subclass label), and
the probe names the enderman goals as vanilla's classes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Block loot keeps one random-sequence path: the blocks side's block_loot
with the generic loot evaluator, and /loot uses the shared loot_sequence.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Drowned register Zombie's goals and then Drowned.addBehaviourGoals (go to
water, trident and melee attacks with okTarget, go to beach, swim up,
RandomStrollGoal) with DrownedMoveControl, Drowned.travelInWater and
AmphibiousPathNavigation. Skeletons get their bow from finalizeSpawn and
pick the bow goal or the AbstractSkeleton$1 melee goal from the held item,
as reassessWeaponGoal does. Monster.aiStep adds 2 to noActionTime in bright
light (raiders always), which stops RandomStrollGoal; LookAtPlayerGoal
supports Mob.class targets and InteractGoal, used by the wandering trader
(with vanilla's AvoidEntityGoal order), illagers, ravagers and vexes.
Entities below the world take 4 damage a tick instead of vanishing, water
travel uses the 0.8F/0.6F/0.3F float constants, and MobWorld carries the
level's sea level.
The oracle refreshes the player's water state after placing it (it is never
ticked, so isInWater kept the previous scenario's value) and keeps its world
under the harness build directory, deleting it before halting instead of
leaving /tmp/horizon-mob-ai* behind. The probe reports KILLED removals.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/gate.py took a crashed tools/parity.py for a pass with no new
failures. The runner now records each completed run, and the gate fails
unless its own run finished. A missing or empty trace (the disk was full)
no longer crashes the runner.
After a harness passes, the runner deletes its large regenerated outputs
(64 MB or more), keeping the vanilla oracle outputs that reusing an oracle
run needs. Full suites left 2-4 GB files in every worktree and filled the
disk.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Zombies (and husks, drowned, zombie villagers, zombified piglins) now
register ZombieAttackTurtleEggGoal first, as vanilla does, so their goal
selector draws the same random numbers. LivingEntity.pushEntities runs in
aiStep against the player and other mobs, with pushes to other mobs applied
at once as Entity.push does (replacing the post-tick push_apart). Spider
target goals check the light before their random interval, LookAtPlayerGoal
uses the non-combat targeting conditions, raiders register the
PatrollingMonster/Raider goals before their own, and the witch runs its
aiStep (heal cooldown, drinking, particle roll) before super.aiStep with
NearestHealableRaiderTargetGoal's nextBoolean.
The mob AI oracle's player is now a fully loaded, hurtable client: its
connection starts with a 60-tick load timer and 26.3 moved the hurt cooldown
to damageCooldownTime, both of which only ServerPlayer.tick counts down, so
whether mobs could hurt the player depended on the scenario order. The probe
reports the player's hurt sound, and an rng-trace feature records the call
sites of Horizon's mob random draws next to the oracle's trace.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Clippy and rustfmt clean, and the scoreboard test now expects vanilla's
removeObjective, which keeps holders tracked without scores.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Encrypted bytes that arrived in the same read as the last plain packet
stayed in the buffer undecrypted, and the connection then waited forever
for a frame it could not parse. The loopback test hit this under load.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/locate biome samples the biome source like BiomeSource.findClosestBiome3d,
/locate poi scans loaded chunks for PoiTypes block states within 256 blocks,
/place feature places the configured feature with the level random,
/place jigsaw assembles pieces with JigsawPlacement.addPieces and places
them into the level, and unknown structures or pools report vanilla's
errors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Context int and float providers (inline or registered) evaluate with
Java's arithmetic, exceptions and rounding, in the default, block or
entity loot context; their codecs report missing fields as
DataFixerUpper does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PlayerTrigger.TICK fires every tick and InventoryChangeTrigger as the
inventory changes; /recipe counts recipe book displays and fires
RecipeUnlockedTrigger; block drops draw from their loot table's random
sequence.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bone meal and saplings reach for getChunkSource().getGenerator(); the
shared oracle level now answers it, as the dispenser oracle did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dispensed spawn egg's mob gets the riders finalizeSpawn adds (chicken
jockeys, spider jockeys, strider jockeys). The harness leaves which mob
comes out a baby and rides to the spawning port's finalization draws, as
it already does for the level random of spawning cases. Cases no longer
grow plants out of a dispenser or build a door from mismatched halves,
and the oracle runs every case three times so an unseeded value is
marked even when two runs happen to agree.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Model TestCommand's release tree (ResourceSelectorArgument patterns
included) and its finder, locate, pos, stop and clear outcomes when no
test instance block exists. Compare /help's gamerule alternatives as a
set: GameRuleCommand registers them in identity-hash order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pass is keyed by the content of the harness's dependencies, so it is
also valid in any worktree with the same files. Passes are published to
parity-shared/ beside the shared vanilla data, and a worktree adopts one
whose key matches its own files instead of running the harness: an
agent's gate only runs what its own changes affect, instead of the whole
suite from scratch. Every runner on the machine also shares
PARITY_MACHINE_JOBS slots, so several agents' gates can't overload it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brigadier keeps children in registration order and /help prints them
that way; tools/generate_command_order.py records vanilla's order where
Horizon's tree differs. The game now runs the release integrated
server's tree; HORIZON_DEV_COMMANDS restores the development commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Invalid allocations are dropped rather than refused, overlaps are not
checked, and an empty or truncated file is a region without chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/tellraw resolves nbt components; execute in another dimension sees its
chunks unloaded; the built-in pack's predicates load; holders stay
tracked after their objective is removed; new worlds use the offline
profile id of the player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture's chunks are plains (empty chunks read as badlands, which has
no bone meal flowers) and carry the case's sky and block light, as the
oracle level does, so grass bone meal places flowers and crops check
their light like vanilla's. The oracle's client sounds include the lava
fizz (1501). Cases build two-block plants whole: a lone lower half is not
a world vanilla makes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/spreadplayers follows SpreadPlayersCommand (safe positions, failure
distance, float centre); /datapack models the built-in, feature and
world packs with their chat links; unknown function tags are empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Desert, snowy, swamp and jungle nights, lush, sulfur and dripstone caves,
warm and frozen oceans, crimson forests and slime chunks. A cube mob grown
by finalizeSpawn takes its constructor's head rotation from the entity's
unseeded random (AbstractCubeMob.onSyncedDataUpdated), so that yaw alone is
not compared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A dispenser places a shulker box in the block in front or nowhere, and
only when that block's canBeReplaced accepts it (a snow layer only when
it is a single layer).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/test_natural_spawn_parity.py compares whole chunk spawn passes,
spawnCategoryForPosition from chosen sites and chunk generation packs
against vanilla's NaturalSpawner on generated box worlds.
Divergences fixed on the way: the reduced water ambient tag is
reduce_water_ambient_spawns, and the room check uses each type's spawn
dimensions scale (4 for slimes and magma cubes, 2 for sulfur cubes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A block entity read inside an item stack remembers the map it was read from;
that memory is not part of the value, so an item's block entity tag with
only its id still counts as empty and is dropped, as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villagers, piglins, piglin brutes, allays, dolphins, foxes and pandas can
pick up loot from construction (their constructors call
setCanPickUpLoot), so they take dispensed equipment without spawn
finalization. Nautilus saddles play item.nautilus_saddle_equip. The
oracle level is the overworld, so fire in an obsidian frame lights a
portal there too, and it answers getEntity(id) and level-wide entity
queries, which leash cutting needs. Unseeded item motion is compared
within its full spread (item motion plus a shearing sheep's push).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LootTable.getRandomItems uses the table's random_sequence, so /loot
results are deterministic per world seed; inline loot tables and tool
components now work too. The oracle ticks its player like the network
listener does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Golems and the wither come from EntityType.create (no spawn finalization,
no level random), after clearPatternBlocks sets every pattern cell to air
with flags 2 and a destroy event, and updatePatternBlocks follows. A
dispensed pumpkin that completes a golem skips setBlock's neighbour
updates as vanilla's does, and pumpkins and skulls post block_place.
Dispensed skulls face RotationSegment.convertToSegment(facing). Destroy
events for air play no break sound. The dispenser fixture plays on normal
difficulty like the oracle level.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Inline and registered item modifiers now apply (26.3's type-dispatched
functions, set_lore, set_custom_data, set_components, toggle_tooltips,
filtered), counting every set slot like CountingModifier. Item stacks
keep components Horizon does not model, display names follow
ItemStack.getDisplayName, /data prints compounds in CompoundTag's
HashMap order, summoned passengers snap to the position, and living
entities other than the player get vanilla attribute maps (waypoint
transmit range included, tracked as ServerWaypointManager does).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaNaturalSpawnOracle runs NaturalSpawner (createState, the filtered
categories, spawnForChunk's loop, spawnCategoryForPosition and
spawnMobsForChunkGeneration) on a stand-in level built from boxes of blocks,
light and biomes with structure pieces, entities, players, the world spawn
and unloaded chunks; natural_spawn_fixture runs mobs::spawn_selection on the
same world.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixes from 3076 to 3459 (game event, cat, frog, painting, goat, goat
horn, filtered text, POI, blending, spawn egg, effect duration, archaeology,
double-sided signs, the 1.20 experiment and chunk statuses), the block
entities their schemas add, and block entities' remainders (a decorated
pot's shards stay next to its sherds). The sample world generator logs in
to 1.18.2 to 1.19.4 servers and gates the scene by version.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening a world saved by an older version runs the file fixes, level.dat and
the DataFixers upgrades of every file instead of refusing it. The save
round-trip harness treats a lit campfire's cooking progress as a timer in the
vanilla views: Horizon saves it as loaded (stage 3 checks it exactly), and it
moves on by however many ticks each vanilla server ran before data get.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity_shim.py stands in for java and javac in harness runs: it
records each oracle run's outputs and the content of every file under the
paths its arguments name, and replays the run while those are unchanged.
After a Rust-only change a harness only pays for its Rust side
(block_update 53 s -> 5 s). A harness that fails while reusing oracle
results is rerun fresh, and stops reusing them if that passes; --all and
--fresh never reuse them.
Harness probes built in the dev profile are optimised (overflow checks
and debug assertions kept), and the chunk generation harness runs its
vanilla and Horizon sides concurrently.
HORIZON_NO_AUDIO keeps the game off the speakers; tools/parity.py sets it,
since the save round-trip harness launches the game on a virtual screen.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Only the collision shapes overlapping the spawn block stop the fall, so a
wide mob beside the dispenser no longer lands a block high. Spawn egg
cases compare that the new mob speaks and where, not which of its voices
its own unseeded random picked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
DataFixerUpper reads typed fields without taking them out of a record's
remainder, so a typed field a fix drops comes back on write with its value
from the record's last read: entities keep their old ArmorItems, HandItems,
body_armor_item, saddle, Trident and Particle next to the new fields, as
vanilla 26.3 writes them. Fixes that write and read entities refresh that
map. The area effect cloud particle moves to custom_particle (4420), boats
keep Type (BoatSplitFix's remainder update misses), and region files with a
short header read as empty, as vanilla's RegionFile does. The sample worlds
now use the item formats each version accepts and enable its experiments.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mobs::spawn_selection ports NaturalSpawner over a NaturalLevel trait: the
spawn state (category counts, spawn potential charges, local caps), the
filtered categories, spawnForChunk, spawnCategoryForPosition with mobsAt,
isValidSpawnPostitionForType and isValidPositionForMob, and
spawnMobsForChunkGeneration. The game's spawning goes through it. Biomes
now load their spawn costs and creature_world_gen_spawn_probability.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BoatDispenseItemBehavior spawns the chest boat or chest raft itself: boats
now carry ChestBoat's 27-slot chest, drawn with its model, saved with its
Items, opened by sneaking, spilt when broken. MinecartDispenseItemBehavior
spawns command block minecarts (an empty command). The dispenser harness
now draws fronts per item (fire, portal frames, anchors, crops, copper,
hives, golem and wither builds), every boat wood, more equipment, spawn
eggs, sulfur cube swallowables and leashed targets, over 6000 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Flint and steel uses BaseFireBlock.canBePlacedAt and getState (a portal
frame draws the level random for vertical dispensers), posts block_place
and block_change, and lights only what CampfireBlock, CandleBlock and
CandleCakeBlock.canLight accept. Fire placed inside an empty obsidian
frame now lights the portal (BaseFireBlock.onPlace). Glowstone posts the
anchor's block_change. Shears cut leads (shearOffAllLeashConnections) and
empty a sulfur cube. Dropped leads draw motion from the item's own
random, not the level's. The oracle gives bone meal a chunk generator so
sapling, grass and moss cases are compared instead of erroring.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Markers, displays and other unsimulated entity types are now kept as
their saved data at a position, so selectors, /tag, /kill, /tp and
/data reach them. Widen the command execution corpus to every command
the integrated server registers, and keep the oracle's terrain tickets
out of /forceload's view.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity.py traces each harness (its Python, Java oracle sources and
the Rust sources of the probes it runs, from Cargo's dep-info) and reruns
it only when one of those, the vanilla artifacts or the toolchains change.
Harnesses run in parallel within the available memory, and their probes
build without LTO and with 16 codegen units into .local/parity-target: a
one-line change rebuilds every horizon-world probe in 3 seconds instead of
122. The full suite takes about 20 minutes instead of an hour.
tools/gate.py is the check a branch passes before integration, and
tools/supervise.py manages the per-area worktrees: status, new worktrees,
saving unfinished work, and integrating a branch only when the gate passes.
Harnesses find probes through lab.cargo_target() instead of a hard-coded
target/, the save round-trip harness builds the binaries it launches, and
the mob AI harness now fails on divergences like every other harness
(--report-only keeps the old behaviour). Debug builds keep line tables
only, so worktrees stay small.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may
not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may
not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may
not build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecraftServer.forceGameTimeSynchronization broadcasts SetTime with no
clocks every 20 ticks, and it can reach a joining bot before
PlayerList.sendLevelInfo's full sync. Wait for a sync with clocks instead
of failing on the first packet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Formats the overnight parity work, fixes the lints it introduced, and
updates the LAN bot test and mob AI probe for fields added on the other
side of the merges (the bot test also set a field that never existed).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/rea_vanilla.py runs REA's JADX provider over the pinned client and
server JARs: class search, class members, method bodies and callers
(including lambdas). REA and the JADX engine are pinned in
research-lock.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
block_entity_anim ports ShulkerBoxBlockEntity.updateAnimation and its
open-count events, BellBlockEntity triggerEvent/tick with resonation,
ConduitBlockEntity.clientTick (shape check, hunting, rotation, nautilus
particles), SkullBlockEntity.animation and DecoratedPotBlockEntity wobbles,
and maps the state to the renderer's BlockAnim.
tools/test_block_entity_anim_parity.py runs the real vanilla block entities
on a stand-in client level (VanillaBlockEntityAnimOracle) against
examples/block_entity_anim_fixture: 172/172 cases, 12720 ticked steps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Uncommitted changes the agent had in progress when it was stopped; may not
build or pass its harnesses yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mth.atan2 builds its tables with StrictMath.asin (fdlibm, ported) and
HotSpot's cos; the controls multiply by the folded RAD_TO_DEG float
constant. Mob.checkDespawn runs before the tick and, in 26.3, persistent
mobs only reset their idle time near a player. NearestAttackableTargetGoal
always requires a line of sight and ignores mustReach when searching.
MeleeAttackGoal starts its canUse clock at game time 0. The harness now
records every random draw of the vanilla mobs with its caller, the random
state per tick, and starts each scenario at game time 1000.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Send the join packets in vanilla's order with the permission level entity
event, recipe book, server data, ticking state and post effects; announce
the newcomer only to the others; list players with every info action;
use vanilla's level key order and border warning time; sync the game time
every 20 ticks without clocks and the overworld clock only when it jumps.
Build the command tree from the commands report for the player's
permission level and server kind, with vanilla's suggestion providers
(VanillaCommandSuggestionsExporter), and answer command suggestions.
The tree matches what a vanilla integrated server sends node for node.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mobs::spawn_rules ports SpawnPlacements over a SpawnLevel trait the game's
MobWorld implements: the placement types with per-type isValidSpawn
(leaves, ice, magma for fire-immune types) and isValidEmptySpawnBlock with
each type's dangerous and immune blocks, and every registered predicate in
vanilla's draw order: the dimension's monster light test and block light
limit, thunder, peaceful, spawn reasons, guardians' sky from below water,
drowned, bats, ghasts, ocelots, slimes (surface chance and slime chunks),
strays under powder snow, striders, turtles, polar bears, tropical fish,
nautiluses, silverfish and endermites near players, patrols and the
spawnable-on tags.
Divergences fixed on the way: nether monsters ignored light, hoglins and
piglins had extra checks, slimes spawned outside slime chunks, ocelots and
ghasts skipped their odds, bats ignored the surface and their draw order,
and several predicates read the wrong blocks.
tools/test_spawn_rules_parity.py: 18,669 cases over 87 entity types match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client now resolves stacks with horizon-assets' ItemModelResolver into
an ItemStackRenderState equivalent (item_render): baked cuboid layers with
their display and local transforms, tints and foil, and the special model
renderers (chests, shulker boxes, banners, heads, shields, tridents,
decorated pots, conduits, bells, books, copper golem statues) posed from
the entity model layers. GUI icons are submitted under the GuiItemAtlas
pose with ITEMS_3D/ITEMS_FLAT lighting, back faces culled and clipped to
the slot; dropped items follow ItemEntityRenderer (bounding box hover,
spin and seeded cluster copies); item frames, first-person and mob,
player and armour stand hands, worn skulls and head items submit the
same state in the world.
The client atlas also runs the atlas sprite sources (directory, single,
filter, unstitch, paletted_permutations), so armour trim sprites exist.
The ad-hoc icon, held-banner, shield, potion-layer and glint paths are
gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Recipe book widgets play SimpleSoundInstance.forUI(UI_BUTTON_CLICK, 1.0F),
whose volume is 0.25. Unit tests cover the mixer distance gain and the
stopped/looping source states.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ToggleKeys holds the sneak, sprint, attack and use ToggleKeyMappings with
the dispatch of KeyboardHandler.keyPress (releases always reach them;
presses and SDL key repeats only without a screen), MouseHandler.onButton,
Gui.setScreen (releaseAll / restoreToggleStatesOnScreenClosed) and
LocalPlayer.respawn (resetToggleKeys, which Horizon lacked). Presses behind
a screen no longer flip a toggled key. tools/test_toggle_keys_parity.py
drives the real Options mappings through the real static KeyMapping
functions (8,000 sequences).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Situation captures the screen music, the menu without a player, End boss
music, creative abilities and underwater selection and the music volume;
the harness calls the real getSituationalMusic/getMusicVolume with a real
camera attribute probe for every dimension and biome.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon had the Auto-Jump option but never jumped. MotionWorld::move_self
now runs LocalPlayer.updateAutoJump after the player's own move (behind
canAutoJump: option, autoJumpTime, ground, shift, input, block jump
factor), the next aiStep presses jump for one tick (input.makeJump) without
toggling creative flight, and the game feeds the option each tick.
tools/test_auto_jump_parity.py runs the real updateAutoJump on a LocalPlayer
shell in an independently built copy of the resident test scene with real
block states (10,568 cases, 1,200 jumps).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Move the beam geometry into block_entity_render::beacon_beam so the
block entity render harness can replay it against vanilla submit calls
(positions, UVs and colours of the solid beam and glow per section).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle runs the real updateIsUnderwater, UnderwaterAmbientSoundHandler
and BiomeAmbientSoundsHandler over the real SoundEngine, comparing every
level random draw, loop fade, mood sample and playing instance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity loot tables now run on the faithful loot engine with an entity
context: the victim, the killer and direct attacker, the damage type and
its tags, the last player to hurt it and their luck, entity predicates
(flags, equipment, components, sheep, cube size, raider captains,
vehicles, entity type tags), damage source predicates, killed_by_player,
random_chance_with_enchanted_bonus, enchanted_count_increase, furnace_smelt
and tag entries. Draws come from the world's random sequence for the table.
mobs::drops ports dropAllDeathLoot: Monster loot for babies, equipment
drops in EquipmentSlot order with Looting's equipment_drops effect and
durability, the fox's mouth item, the wither's star, the charged creeper
head hook, horse inventories and chests, and getBaseExperienceReward with
the equipment bonus and baby zombie reward, split into orbs.
Fixes found on the way: sheep dropped their wool twice, baby monsters
dropped nothing, armour bonus experience and the equipment slot order were
wrong, hoglins were fire immune, and fishing treasure was unreachable (the
hook predicate format changed).
tools/test_entity_death_loot_parity.py: 1,578 cases over 87 entity types
match vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add horizon_client::hud_layout (crosshair and attack indicator, hotbar
with selection, off hand by main arm, item slots with the pop squeeze,
armour, hearts including frozen and absorbing kinds, food, air bubbles,
mount hearts capped at 30, experience bar and level) and the
test_hud_parity harness whose oracle runs vanilla Hud methods on a
stand-in client. The game now draws these through hud_layout; the
experience level is centred with integer division like vanilla,
spectators get no crosshair and mount hearts show in creative too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The game now ends each frame with FramerateLimiter.limitDisplayFPS below
260 fps (sleep, overshoot averaging, spin-wait) instead of waking the event
loop at a computed deadline, and the limiter honours Thread.interrupted.
tools/test_frame_limiter_parity.py runs the real limiter bytecode with its
clock calls rewritten to a scripted virtual clock (5,288 cases).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horizon spawned mobs whatever the spawn_mobs and spawn_monsters game rules
said; natural spawning now follows ServerChunkCache.tickChunks (the chunks
are still shuffled every tick) and the custom spawners tick only with
spawn_mobs. The slimes this put in the test world were what woke its sculk
sensor.
Falling blocks keep their UUID and loaded compound while they move, read
NoGravity, DropItem and a signed Time, and drop themselves as an item
after 600 ticks off the ground instead of landing. A crafter's saved
triggered flag only follows the block state when the state changes
(CrafterBlock.neighborChanged), and a horse saves the attribute instances
it was loaded with, as AttributeMap.pack does.
The harness compares vanilla log lines without their time stamps, accepts
extra entities only in chunks vanilla never generated, treats a falling
block's Time as a timer, and keeps the lit furnace's slots out of the
vanilla view. Its scene starts the falling block far below the 600-tick
limit and keeps the campfire from finishing during the run. The known-gap
list is empty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle drives MusicManager over the real SoundEngine with the situation
as input; the script covers delays, replacements, frequencies, fades,
loading screens, pauses, silent starts, toasts and unplayable events.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a decoding capture proxy (tools/net_capture.py), an offline vanilla
client launcher (tools/vanilla_client.py) and a vanilla codec oracle that
decodes every captured clientbound packet with vanilla's StreamCodecs.
Fix the entity data serializer ids (vector3 onwards were off by one), the
sheep wool and piglin baby indices, send mob variants, villager data and
equipment, and send entity movement at each type's update interval and
tracking range with position syncs, like ServerEntity.sendChanges.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaMobAiOracle runs vanilla mobs in a real headless flat server level,
ticking them as ServerLevel does while the rest of the level stands
still; mob_ai_probe replays the same scenarios on Horizon. Both print each
mob's AI-visible state every tick (position, motion, rotations, goals,
targets, navigation, controls, input, brain activities and memories,
sounds) and tools/test_mob_ai_parity.py reports the first divergence.
Fixes found: float entity dimensions, Mth.sin/cos in moveRelative and the
strafe/swim controls, Mth.atan2 and the float thresholds of the controls,
RandomPos float constants, the ambient sound clock starting at zero, hurt
and death sounds drawn from the mob's own random (also when burning),
knockback for every damage type not tagged no_knockback, Entity.move's
collision flags, restitution and footstep/swim sounds, and mob sound
volumes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Camera.tick, tickFov, alignWithEntity with getMaxZoom over visual-shape
clipping, setRotation and move with JOML quaternion arithmetic,
calculateFov/calculateHudFov, getFieldOfViewModifier (now including
flight), ClientAvatarState walk/bob history (the backwards-extrapolated
walk distance and velocity-based bob) and GameRenderer.bobHurt/bobView as
the JOML pose matrix with property flags. The game renders through them,
adding hurt tilt, the camera distance and scale attributes, the mirrored
front camera and the sleeping camera. tools/test_camera_parity.py compares
18,502 cases against the real classes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Audio now keeps vanilla SoundEngine bookkeeping over a mixer that stands in
for OpenAL sources (gain, pitch, linear distance attenuation, looping,
relative, source states, the 247/8 channel pools), ticks once per client
tick with pauseAllExcept/resume and stops everything when the world closes.
MusicManager holds a forMusic instance and checks isActive as vanilla does;
the ambient handlers play LoopSoundInstance, UnderLiquid* and mood instances,
the enter/exit sounds seeded from the level random; jukebox songs use
forJukeboxSong instead of a hand-made volume ramp.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a differential harness (test_block_entity_render_parity) whose Java
oracle runs each vanilla BlockEntityRenderer on a real block entity and
captures the submitted model vertices, textures, light and text calls.
Horizon now animates shulker lids, bell shakes, active conduits (cage,
wind and the camera-facing eye), skull jaws and ears, pot wobble and
sherd sides; signs wrap lines like StringSplitter and measure spaces
with the font; books use the Mth sine table and player heads the slim
default skin.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle runs vanilla SoundManager/SoundEngine on OpenAL Soft null device
(inline executor, silent buffers) and replays scripts of plays, stops,
category gains, option volumes, channel ends, pauses, queued, delayed and
looping sounds and tickable ambient instances, printing every AL source
parameter, isActive, subtitle notification and pool count per step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
World generation and interaction now have every covering harness passing and
score 1. The other rows keep 0.75, with their gaps rewritten to name what is
still missing now that the block, command, entity model and item model
harnesses pass; the next steps follow the open gaps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The item model harness ran out of memory, so it now streams both outputs and
looks Horizon's rows up by offset. With the vanilla side no longer crashing on
swap-animation lookups (properties now come from the baked set), on armour
(trim sprites are generated by the items atlas's paletted_permutations source
once SpriteSources is bootstrapped) or on compasses (item tags are bound from
the data pack), 197,000 more stacks were compared and 121,841 differed.
Horizon now composes transformations as JOML 1.10.9 does: rotationXYZ and
axis-angle quaternions with cosFromSin, and Matrix4f operations that take the
identity, translation, affine or generic path by matrix properties, with
unfused a * b + c. Float RGB tint constants are scaled by 255, the shulker box
renderer always encodes its texture, and a new palette module ports
PalettedPermutations and PaletteMapping so trim sprites exist on the Horizon
side too.
196,290 resolved stacks match; 2,582 cases are still rejected by vanilla's
stand-in client and not compared.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The chunk generation harness found 70 differing chunks across three seeds in
the overworld, Nether and End. Proto chunks, biomes and decoration now span
the dimension type's build range rather than the noise height, so features
above y=128 in the Nether and End city upper floors survive; the decoration
level reports no sky light, as unlit vanilla chunks do, so mushrooms and crops
pass or fail their light checks the same way; beehive decorators store their
bees; iceberg ellipses skip NaN distances; double slabs never waterlog;
mineshaft spawners save an empty SpawnPotentials list; and the dump example
forgets structure starts between rectangles, as the oracle starts each with
fresh chunks.
The oracle prints absent block entity keys as vanilla loads them from an empty
tag, and the harness treats vanilla's save-format equivalences (plain sign
text, absent empty banner patterns, unfiltered sign messages) as equal. All 2,352
chunks over 225 structure sites match with zero block, biome or block entity
differences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add SoundRegistry (sounds.json with replace, subtitles, file validation and
event references weighing as much as their event) and a SoundEngine model,
with a differential harness resolving every sounds.json event against the
real SoundManager.prepare and SimpleSoundInstance.getOrResolve.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ports vanilla's packrat parser with SNBT and component-predicate grammars, the
entity selector, block state and item parsers, resource-or-inline codec
arguments, and the remaining argument types, suggestion providers and error
cursors. The tree gains compute, data modify compute, place, locate poi,
datapack create, item modifiers and the parse-only commands (dialog, kick,
posteffect, fetchprofile, spectate, publish, waypoint, debug, jfr), with
release-dispatcher permissions. /particle takes the 26.3 option syntax.
The oracle's stand-in server now holds a fresh world's real state (data pack
managers, pack repository, empty storages, clocks, templates, an overworld),
so vanilla answers every input instead of crashing on about 1,100 per level.
The command parse harness matches on 39,685 of 39,685 commands at permission
levels 4, 2 and 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ports AbstractNautilus, Nautilus, ZombieNautilus and their brains (taming
with pufferfish, saddles and nautilus armour, riding with a dash, breeding,
spawning below sea level, drowned riders), SulfurCube on the slime code
(swallowed blocks and their archetypes, shears, TNT cubes, splitting) and
CamelHusk on the camel (its own saddle and sounds, husk jockeys). Camels now
save their saddle and stay tame after loading, and horse armour layers load.
The entity model harness compares 150 layers, all matching, including the new
mobs and previously unmapped armour, harness, fish and villager layers; mob
attributes pass with the new types.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client random-ticked every section with a non-air block, drawing
getBlockRandomPos positions vanilla never draws. It now ticks a section only
when its palette holds a randomly ticking state, as
LevelChunkSection.isRandomlyTicking does (lava counts through LiquidBlock).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The warden's walk, idle wobble and pulsating spots now use Mth's sine table
as WardenModel and WardenRenderer do, and baby sniffers no longer apply
SnifferAnimation.BABY_TRANSFORM: SnifferRenderer wraps the baby geometry in a
plain SnifferModel, so that animation never runs in 26.3. The harness now
drives the illusioner through explicit arm poses like the other illagers, and
the oracle shows its hat as IllusionerRenderer does. All 132 compared layers
match over 3,876 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The block oracle cut chained neighbour updates at 512, but a server level
uses max-chained-neighbor-updates (1,000,000 by default), as Horizon does, so
long redstone wire cascades stopped early on the vanilla side only. Redstone
now matches on 4,000 of 4,000 cases; block updates and random ticks still
match on every compared case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The oracle now gives comparators and lecterns real block entities, so a
lectern whose state has a book reads page progress 1 of an empty book and
signals 14. The fixture's stand-in block entities now do the same, and the
redstone harness matches on 8,000 of 8,000 cases with a second seed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The random tick oracle now stands on a real overworld: the data pack's
dimension type (for fire's infiniburn tag), its attributes and day timeline
on a clock at the fixture's day time, level data for difficulty, and block
scans that walk positions in BlockScanUtils' section order, since 26.3 reads
chunk sections the map-backed level does not have.
Horizon now rolls the nether portal's zombified piglin spawn with the level
random (the client spawns it near a player), rechecks chest and ender chest
openers through a new block entity hook and broadcasts the lid event, gives
melting ice and placed sniffer eggs their game events, and reads the weather
age of copper golem statues, which name the field weatheringState. The
fixture prints empty dispensers, crafters, ghastling spawns and fresh
shriekers as vanilla resolves them.
Random ticks match on 39,998 of 39,998 compared cases (two propagules cannot
grow trees in the oracle), block updates on 40,000 of 40,000 and redstone on
3,999 of 4,000.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Taking a brewed potion fires BrewedPotionTrigger, which crashed the
stand-in player without PlayerAdvancements. With an empty tracker the menu
click harness matches on 6,000 of 6,000 cases.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The block update oracle crashed on 625 cases because its constructor-less
level had no sound seed generator, attribute system or chunk source; it now
supplies them, keeps cave and void air apart from air, and creates block
entities on demand as a real level does. With those cases compared, Horizon
now primes TNT with the level random, sound and prime_fuse event at the
moment of priming, links powered wooden shelves into side chains, breaks
nether portals whose frame is no longer complete, reacts to power at command
blocks through a new block entity hook, rings bells with their block event,
uses the straw bed's own destroy particles and gives big dripleaf tilt changes
their vibration. Empty dispensers also emit block_activate in the client.
Block updates match on 40,000 of 40,000 cases and redstone on 3,999 of 4,000.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The overnight work landed LAN hosting, the remaining mobs, client paletted
storage and 18 new differential harnesses, so the table was scored again from a
full run of all 232 harnesses: 223 pass, 8 fail and the item model harness runs
out of memory. Chunk storage, section meshing and lighting now match vanilla in
their harnesses and score 1, and multiplayer rises to 0.75 now that worlds open
to LAN. The other subsystems stay at 0.75 because a harness covering them still
fails or part of them is not yet compared with vanilla; their gaps and the next
steps now name what is left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds a differential harness that instantiates every vanilla entity type on a detached server level and compares default attributes, standing dimensions and baby dimensions with Horizon's mob table. Baby donkeys and mules now use the vanilla half-size hitbox instead of 0.7 scale.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The sequence loot function folds over an Option accumulator, which clippy flags as a manual try_fold. The fold is intentional, so allow the lint there.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Merges local main into this branch. The only conflict was in the illager
animation: this branch's anim_pose (with the passenger sitting pose) is kept,
and main's change that leaves the illusioner's hat visible is applied to it.
fmt, clippy with -D warnings and the full workspace tests pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings main's sync_level()/save_game() split and the world ticks inside
`if running {}` into the typed block entity NBT, save roundtrip harness and
session.lock work. The only textual conflict was in recipe_book.rs, where
the player file writer keeps main's toBeDisplayed list alongside the
recipes list. fmt, clippy -D warnings and the whole workspace test suite
pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
After merging the overnight feature branches, the only remaining problem was a clippy lint in the illusioner mob code, where a manual modulo check had to become is_multiple_of. With that change, formatting, clippy with warnings denied and the full workspace test suite all pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds differential harnesses for random/scheduled ticks and redstone circuits on top of the block update oracle, and makes the oracle report pending block events as the deduplicated set vanilla keeps. The harness found that detector rails never sensed anything, so they now check for minecarts on place, tick and entity contact, and the client reports the player and minecarts to pressure plates, tripwires and detector rails each tick.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Finishes the in-progress merge of main into this worktree's branch, keeping both sides: main's sync_level()/save_game() split and the world ticks inside the running block alongside this branch's item model baking, new mob brains and command parsing work. cargo fmt, clippy with warnings denied and the full workspace tests all pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Work in progress on commands, functions and scoreboard handling carried over from the interrupted run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The illusioner is an illager spellcaster with a bow. It casts a mirror spell that makes it invisible for a minute while four mirror images wobble around it and reshuffle when it is hurt, and, on Hard-ish local difficulty, a blindness spell on a new target. It shoots arrows like a skeleton, flees from creakings, joins raids and illager target rules, scares villagers and traders, and shows its hat and bow arm pose as in vanilla. Other illagers no longer draw the hidden hat layer.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Merge local main into the options, timer and mouse branch. The only conflict was the module list in horizon-client's lib.rs, where this branch adds keymapping and main adds lan; both modules are kept. main's sync_level()/save_game() split and the world ticks inside `if running {}` merged cleanly. cargo fmt, clippy with -D warnings and the whole workspace test suite pass.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A monster room's chest took its loot table and seed from the level random in vanilla, and its spawner got the chosen mob's spawn data, but the worldgen world ignored both, so the random sequence and the block entities diverged. A block entity also now survives a set of the same kind of block (a waterlogging chest in a ruined portal), as in a vanilla proto chunk. The chunk harness stops reporting default block entities and vault reward tables as differences.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The vanilla differential run showed Horizon writing fields vanilla leaves out: empty scoreboard parts, recipe book settings at their default, an empty warden anger list. These are now omitted, and mobs loaded without saved equipment no longer receive the default bow. Opening a world whose level.dat is older than 26.3 now fails with a message naming tools/upgrade_world.py, which runs vanilla's own forceUpgrade on a copy. The roundtrip harness lists the timing-dependent differences it tolerates, and horizon-ctl waits up to five minutes for the control socket on a loaded machine.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
On the player inventory, crafting table, chest family, hopper, dispenser and furnace screens, holding a stack and dragging across slots now splits it (left even, right one each), a quick second click gathers matching stacks, number keys and F swap with the hotbar or off hand, and Q or Ctrl+Q throws one or the whole stack, all through the same engine the click harness checks against vanilla. Other screens keep their own click handling.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The multiplayer screen listens on the vanilla multicast group 224.0.2.60:4445 and lists LAN worlds announced with the [MOTD]/[AD] format, letting the player join them directly.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Shift-clicking an inventory, hotbar, armour or crafting grid slot now follows InventoryMenu and CraftingMenu quickMoveStack, including moving into the crafting grid, armour and off-hand slots and the repeat-until-unchanged loop.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Shift-clicking in chests, barrels, shulker boxes, hoppers, droppers, dispensers and furnaces now builds the vanilla menu from the open container and the player inventory, applies quickMoveStack with the repeat-until-unchanged loop of doClick, and writes the slots back.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The crafter menu follows CrafterMenu: slots refuse items while their disabled flag is set, and the result slot is non-interactive. Random disabled flags are part of the harness cases.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The brewing stand menu follows BrewingStandMenu: single-item potion slots, the ingredient and fuel slots with their quick move rules, checked against vanilla with potion bottles, ingredients and fuels in the random contents.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Merged local main into the mobs branch. Conflicts were in the mob event
enum (kept both LeashBreak and CureZombieVillager), the LAN and leads
module lists (kept all), horse NBT (chests are written for llamas,
donkeys and mules, with llama data kept), and chunk saving, which now
goes through mob_tree_nbt (main's passenger trees) with write_leash
applied inside it so leashes are still saved.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Right clicking a cauldron with a bucket, bottle, water potion, dyed armour, banner or dyed shulker box now applies the verified cauldron rules to the held stack, inventory and block.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Using a bucket, bottle, water potion, dyed armour, patterned banner or dyed shulker box on an empty, water, lava or powder snow cauldron follows CauldronInteractions, including the creative-mode stack rules of ItemUtils.createFilledResult and the underwater refusal for lava and powder snow. A harness runs every item against every cauldron state in vanilla's own useItemOn and compares the result, block writes, sounds, drops and inventory.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The lightmap now brightens with night vision and conduit water vision and flickers its block light like vanilla's extractor. The hearts, armour and food rows follow Hud.extractPlayerHealth: absorption rows, hardcore sprites, poison and wither tints, the regeneration hop, low-health shaking, the damage blink and hunger jitter, driven by vanilla's java.util.Random.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The environment differential harness now passes for every dimension, biome, time and weather level. Weather colour multipliers are floored to 8-bit channels as vanilla does. A biome without its own fog or sky colour falls back to its dimension's, which fixes the End's dark purple fog, and the missing cloud colour is transparent.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Stripping, scraping, wax removal, tilling and path making now come from the data pack's transformers instead of hard coded block lists, so every copper variant, rooted dirt loot and the face restrictions behave like vanilla, and honeycomb waxes copper blocks.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A new block transformer module evaluates the data pack's rule based state providers the way BlockTransformer.transformBlock does (matching blocks and tags, all-of, copy properties) and derives the waxed twin of a copper block from the wax-off transforms. A harness compares every transformer, block state, face and block above with vanilla's own providers and with HoneycombItem.getWaxed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A pure menu engine now implements every click type (pickup, quick move, swap, clone, throw, the three quick craft drags, pickup all) for the player inventory, crafting table, chests, hopper, dispenser, shulker box and the furnaces, with armour, fuel, output and crafting result slot rules, the crafting result recompute and remainders, and Inventory.add. The new menu click harness runs thousands of randomized click sequences through vanilla's real menu classes and this engine and compares every slot, the carried stack, data slots and drops.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Nether roof and End sky sections are all air in vanilla and Horizon keeps none, and structure templates keep their stored integer width while vanilla re-saves block entities with its own, so neither is a generation difference.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings the llamas, breezes, sniffers, happy ghasts and copper golems on top of the raids, scoreboards, warden, sculk, creaking and golem-construction work already on main. Sniffer eggs now schedule their first hatch stage when placed, boosted on moss.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Brings the options, timer and mouse parity port on top of everything that landed on main (raids, scoreboards, warden, subtitles and more), keeping both sides in game.rs, clock.rs and settings.rs. The subtitle option test now expects vanilla's behaviour of keeping the default when notificationDisplayTime is out of range.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Rewrote a negated float comparison in the compass wobble check, merged two identical branches in the pack overlay mirroring, and replaced the empty item model test stub with small unit tests so the workspace passes clippy and tests.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Work in progress on the block update differential harness fixtures and redstone/piston update fixes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds the stopwatches store saved in data/minecraft/stopwatches.dat, the /stopwatch create, query, restart and remove commands, and the execute if stopwatch condition, with the parse tree and parse fixture updated.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Resource packs now honour pack.mcmeta overlay directories and filter block patterns when layering files over the vanilla resources.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Work in progress on the environment rendering differential harness: a Rust fixture example, a vanilla oracle and the driver script.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds the vanilla updateShape/neighborChanged oracle, the Rust fixture example and the Python driver, plus a handle_neighbor_changed entry point on Behaviours.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Work in progress on the vanilla menu oracle adapter used by the upcoming menu click parity harness.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Work in progress on vanilla's item model pipeline in horizon-assets: definition parsing, resolution, generated model baking, special renderers and a differential harness against a Java oracle.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Remote players can now open and click containers, and the host gets a LAN menu and action hooks, building on the first LAN server.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A carved pumpkin or jack o'lantern placed on two snow blocks, on an iron T, or on a copper block now brings a golem to life: the pattern blocks vanish with break particles and a snow or iron golem appears; on copper the block turns into the matching copper chest facing the way the pumpkin did.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A new Java oracle bakes every vanilla model layer, builds the model's own render state and runs the real setupAnim; tools/test_entity_model_parity.py replays the same states through Horizon's animation code and compares every part pose, visibility and the baked polygon sums (about 3700 cases over 125 layers). Fixes the harness found: model sine and cosine now use vanilla's quantised table, the creeper's swapped leg bindings, humanoid arm poses in vanilla order (held items no longer tilt mob arms, bows pose both arms last, crossbow charge, riding, crouching), swing weapon down, piglin ears and dancing, illager hats, arms and bow pose, zombified piglin ears, baby donkey head and legs, hoglin baby head, ravager neck, vex charging arms, feline sitting and lying poses including babies, camel and zombie/skeleton horse baby models, and new ports of the goat, polar bear, panda, llama, turtle, strider, snow golem, silverfish, endermite, parrot, bee, bat, armadillo, axolotl, frog, rabbit and fox models. Hidden parts are now an explicit flag instead of a zero scale, and skipDraw is supported.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A Java oracle builds vanilla's real Brigadier command tree with the loaded registries and data-pack tags, exports the tree, and parses a corpus of command strings. The corpus is generated by walking that tree with valid and invalid samples for every argument type plus hand-written edge cases. A Rust example parses the same corpus with Horizon's dispatcher and the Python driver compares success, exact error text and cursor, and end-of-input completion suggestions, with tree-diff and mismatch summary helpers. Currently 79 percent of 39685 inputs match at permission level 4. Entity selector errors now rewind to the start of the input like vanilla.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Containers now remember a custom name: it is read and saved as CustomName, taken from the item when placed (including by dispensers), shown as the screen title of chests, furnaces, the brewing stand, hoppers, dispensers and the crafter, and given back to the dropped item when the block is broken. Saddles on pigs, striders and horses are kept as full item stacks instead of a flag, so their components survive taming, the horse menu, dispensers, saving and death drops. The recipe book lists dye, transmute (shulker box, bundle, map cloning) and tipped arrow recipes, searches the tooltip lines and ids of a collection's results the way the vanilla search tree does, and squeezes new recipes' tabs and buttons once, saving the pending recipes as toBeDisplayed.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A world can now be opened to the local network from World Options in the pause menu (LAN on or off, port, command access, forcing the game mode, plus the default and personal game mode, cheats and difficulty), or with horizon-ctl lan open. The hosted world runs a server inside the game that speaks the vanilla 26.3 protocol: server list status, offline-mode login, the configuration phase with the synchronized registries and tags (sent as known-pack references to vanilla clients and in full to clients that know no packs), then the play state with chunks and light, time, weather, the player list, the host and other guests as visible players, mobs and dropped items, block changes, chat and keep-alives. Network I/O runs on its own threads and a world that is not open pays one check per tick. A vanilla 26.3 client and a second Horizon client were verified joining and seeing each other and the terrain.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Happy ghast: an adult floats about, drifts toward a player holding snowballs or a harness, stays within 32 to 64 blocks of where it is, heals slowly (a point a second in rain) and takes a harness by interaction (shears take it off again, and it drops with the ghast); a harnessed adult can be ridden and flown with the movement keys, look direction and jump, and the rider lands on top when getting off. Ghastlings are smaller, follow players and adults, panic fast and grow up in 24000 ticks or faster with snowballs; a dried ghast block now hatches one. Harness and body models, tentacle animation, sounds and saving of the harness are in.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The crafting module now matches shapeless recipes with a full bipartite assignment, shrinks shaped patterns like ShapedRecipePattern, keeps result components (suspicious stew effects), and implements the transmute, dye, imbue (tipped arrow), book cloning, firework rocket/star/fade, decorated pot, repair item, map extending, banner duplicate and shield decoration recipes with vanilla's matching and assembly. Crafting remainders now follow Item.craftRemainder only and are placed on the grid by recipe, so cloned books and duplicated banners stay. Stacks gain dyed colour, firework explosion, pot decorations, bundle contents, stew effects and map post-processing components, and their NBT now matches the codecs (defaults omitted, templates without count 1). A banner whose first layer was a rhombus was mistaken for the raid banner; that is fixed.
tools/test_recipe_parity.py drives vanilla's RecipeManager (new VanillaRecipeOracle) and the Rust recipe_fixture over 50k generated crafting, cooking, stonecutting and smithing cases and compares matches, results and remainders as canonical NBT; all match.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The new oracle drives vanilla's own chunk stages (structure starts and references, biomes, terrain with surface and carvers, then biome decoration with every placed feature and structure piece) through real WorldGenRegion instances, and the chunk_dump example runs the same stages in the same order in Horizon. The Python runner compares every block state, quart biome and block entity over terrain sites and structure sites found by scanning each seed, in the overworld, Nether and End, with phases that stop before features or run structure pieces only.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The command clears the player's warden warning level or sets it from 0 to 4. A mob's death is now heard with the facts of the mob that died, so a dying warden stays silent to sensors.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sniffer: wanders, scents and sniffs, walks to diggable ground it has not explored, digs for a few seconds and drops a torchflower seed or pitcher pod, then stands up happy, with a cooldown of eight minutes before it sniffs again; follows torchflower seeds, panics fast, lays a sniffer egg instead of bearing young when bred, and keeps its explored positions in its save. Sniffer eggs now schedule their hatching when placed (faster on moss), crack twice and hatch a snifflet that takes 48000 ticks to grow up; the keyframe animations, the snifflet model and the lower digging body, dust and sounds are in.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The section mesher blended biome colours over quart cells at the section's
mid-height. It now follows ClientLevel.calculateBlockTint: each tinted block
averages the colours of the biomes BiomeManager picks (the eight-corner
zoom with the client's hashed seed) over the biomeBlendRadius square at the
block's own height, computed lazily per layer and column. The ao and
cutoutLeaves options are honoured too: flat lighting, skipped faces between
opaque leaves and a vertex layer flag that makes the terrain shader ignore
leaf alpha.
The section mesh parity gate gained biome-blend scenes (random zoom seeds,
radii 0 to 7, all 67 biomes, real Biome colour resolvers through a
BiomeManager in the Java oracle) and option scenes; all 1.0M quads of the 112
scenes match the client.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Place a saved structure template at a position with rotation, mirroring, integrity, seed and strict mode. Templates come from the world's generated structures first, then the vanilla template files. Block entities and saved entities are placed along with the blocks, and the template ids are offered as suggestions.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Players and mobs now report footsteps, swimming, landings, splashes, damage, deaths, eating, drinking, container use, shots and landed projectiles, blasts, lightning and primed TNT to the vibration system, with sneaking and wool respected. The darkness effect fades in and out over 22 ticks, closes the fog in to 15 blocks, dims the lightmap with its pulse and hides the sun, moon and stars. Active sculk sensors shed redstone dust, charge and shriek level events show their particles, and the player's warning level is saved with the player data.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
New commands: /recipe, /damage (with at, by and from), /rotate (angles or facing), /swing, /playsound and /stopsound (per sound source, with the minimum volume rule), /particle (dust, block and the other particle options), /attribute (get, base set, reset and modifiers on the player; max health, speed and attack damage on mobs), /ride, /spreadplayers (with teams and a height limit), /forceload (force loaded chunks stay generated and simulated), /fillbiome (with a replace filter and biome tags), /worldborder (set, add, center, damage, warning and get, with the shrink and grow animation, the wall that stops the player and border damage, saved in world_border.dat) and /loot (replace, insert, give and spawn from loot tables, killed mobs, mined blocks and fishing). The sound engine learned to play a sound on a chosen sound source and to stop playing sounds by event or source.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Villagers killed by a zombie, husk, drowned, zombie villager or zombified piglin rise as zombie villagers on Normal (half the time) and Hard (always), keeping their type, profession, level, trades, experience, gossip, equipment and baby state. Splash and lingering potions now give their timed effects to nearby mobs, so a weakness potion followed by a golden apple starts a cure of 3600 to 6000 ticks, sped up by nearby iron bars and beds; the zombie villager shakes, plays the cure sound and swaps weakness for strength. When the clock runs out it becomes a villager again with nausea, the curing player gains major and minor positive gossip (so trades get cheaper), the cured_zombie_villager advancement triggers, and the conversion sounds play. ConversionTime, ConversionPlayer, VillagerData, Offers, Gossips and Xp are saved, curing zombie villagers never despawn, and zombie villagers now draw their level badge and baby textures.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Unlocking recipes through advancement rewards now shows the "New Recipe(s) Unlocked!" toast with the recipe's result item and "Check your recipe book" underneath, one toast for a burst of unlocks.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Double chests now open as one 54-slot Large Chest (right half first, as in vanilla) with the 222-pixel screen and the inventory moved down, and both halves update their comparators and saves. Shulker boxes use their own texture and "Shulker Box" title, ender chests and barrels show their names, and the hopper minecart is titled as such.
Shift-click now moves stacks between the player inventory and chests, barrels, shulker boxes (which refuse other shulker boxes), ender chests, hoppers, dispensers and droppers the way the vanilla menus do, and in furnaces it sends smeltable items to the input, fuel to the fuel slot and takes results to the inventory.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
/execute now has every vanilla subcommand: store (result or success into scores, boss bars and entity, block or storage data with a type and scale), positioned over a heightmap, summon, on (owner, vehicle, controller, passengers, target, attacker), and the if/unless conditions score (all comparisons and matches), blocks, biome, predicate, function, data, items and slots on top of block, entity, loaded and dimension, with the vanilla pass, fail and count messages. Data packs in the world's datapacks folder (folders and zips) provide functions with comments, line continuation and $ macros, function tags including minecraft:load and minecraft:tick, and predicates; /function (with arguments or "with" sources), /return (value, fail and run), /schedule, /reload and /datapack list, enable and disable work against them. Also added: /bossbar with a boss bar overlay and saving, /item replace, fill and override over player, mob, armor stand, item frame and container slots, /random and /teammsg, and /tick rate, freeze, step and sprint on the game clock.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Breeze: chooses the nearest player or iron golem (or whoever hurt it), long jumps behind its target after inhaling, shoots a wind charge, slides to new spots and shoots again when stuck, with the vanilla timers, poses, keyframe animations, wind and eyes layers, sounds and block dust at its feet; arrows and other projectiles bounce off it. Wind charges: the item a player throws (half a second between throws), the charge a breeze shoots, and their burst, which does no damage but knocks players, mobs, projectiles and primed TNT away, cancels the thrower's fall, opens wooden doors, trapdoors and gates, presses buttons, flips levers, rings bells and snuffs candles; gust particles for the burst.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sneak-using a tamed adult horse, donkey, mule, skeleton or zombie horse, or pressing the inventory key while riding one, now opens its inventory: the saddle slot, the horse armour slot (horses only), the 5x3 chest of a donkey or mule with a chest, the mount drawn turning toward the cursor, and the player inventory below. Slots only take what the mount can wear (a saddle, or horse armour for horses), shift-click routes items the way the vanilla menu does, and the equip sounds play when the mount is saddled or armoured. The menu closes when the mount dies or is more than four blocks away.
The chest contents are saved with the mob (Items) and drop with the chest when the mount dies. The translation table also gains the entity, recipe book, banner pattern and toggle slot texts used by the new screens.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
During a raid a witch looks for the other raiders, throws them
Regeneration, or Healing once they are almost dead, and the potion cloud
heals every raider it reaches. The ominous banner shows its own name and
uncommon rarity in tooltips.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Villagers remember players through gossip (major and minor, positive and
negative, and trading) that fades by the day and spreads when villagers
meet, saved with each villager. Hitting a villager earns a minor negative
mark, killing one in front of others a major one, and each trade a small
positive one; the reputation changes the prices, as does Hero of the
Village. Villagers that meet swap gossip, pass surplus food to hungry
neighbours (farmers to anyone) and wares to the profession that wants them,
pick up food and seeds from the ground into an eight-slot inventory, and
willing pairs (twelve food points, awake, adult) make a baby after about
fourteen seconds when a free bed is within 48 blocks. Frightened villagers
and gossiping groups that slept lately call for an iron golem, and golems
defend a village against a player whom a villager nearby holds at minus
100. Bottles from structure loot keep their amplifier.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Drinking an ominous bottle gives Bad Omen; inside a village it turns into
Raid Omen, and when that runs out a raid starts, or grows, around the
village points villagers have claimed (the raid_trigger statistic and the
voluntary exile trigger fire on the first omen). Raids are saved in
data/minecraft/raids.dat and send waves of pillagers, vindicators, evokers,
witches and ravagers (ravagers carrying pillagers, evokers or vindicators)
by difficulty and omen level, from a ring that closes in as the countdown
runs down, with the raid horn and a red notched boss bar that shows the
raiders left, Victory or Defeat. Raiders walk to the village, move between
beds, take back a dropped captain banner, celebrate a lost village, are
enchanted with Quick Charge or Sharpness at high omen levels and are
dropped from the raid when idle or far away. Killing one makes the player a
hero; a won raid grants Hero of the Village and the hero_of_the_village
trigger. Villagers hide in their beds during a wave, ring the bell before
one, celebrate a victory with fireworks, and toss gifts to a hero. /raid
start, stop, check, spawnleader, setomen, glow and sound are available.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Every ten minutes or so, in daylight and a fifth of the time, a patrol of
pillagers spawns 24 to 48 blocks from a player who is not near a village,
once the world is five days old and outside mushroom fields. The leader
wears the ominous banner and heads for a distant target in ten block hops
with its companions following; patrols hold their ground, shout to nearby
raiders and attack together once something comes close. Natural spawns lead
a patrol one time in seventeen. A fallen banner always drops, and a captain
pillager drops an ominous bottle that gives Bad Omen when drunk. Patrol
state, the banner's patterns and the bottle's amplifier are saved as vanilla
NBT.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A creaking heart in a pale oak trunk now follows its logs: placed or updated between two logs on its axis it wakes at night (12600 to 23400 in the overworld) or goes dormant by day, and stays uprooted without them. An awake heart with a player within 32 blocks spawns a linked creaking within 16 blocks, keeps its UUID in the saved block entity, reports the distance to it as a comparator signal, and lets it go when the day comes, it strays more than 34 blocks or a player stands inside it. The creaking freezes while any player looks at it, wakes when one looks from within 12 blocks, then hunts the nearest visible player and hits every 40 ticks. Bound to a heart it takes no damage; hits only make it sway and, when a player did it, make the heart send trail particles and sometimes spread resin clumps over nearby logs. Breaking the heart makes its creaking twitch to death, and removing the block any other way tears it down in a burst of crumbling pale oak. It has the vanilla model, animations (walk, attack, invulnerable, death), glowing eyes, sounds and NBT home_pos, and the trail particle exists now.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The inventory, crafting table, furnace, blast furnace and smoker screens get the green recipe book button, which opens the book beside the menu and moves the menu over. The book has the search tab and one tab per category (hidden until it holds a recipe), a search box matching recipe names (or @namespace), the craftable-only toggle, pages of 20 recipe buttons that cycle through the recipes of a group, right-click for the alternatives overlay, and tooltips. Clicking a recipe moves the items from the inventory into the grid (shift fills the biggest stack, clicking again adds one more set), and when items are missing the grid is cleared and the recipe is shown as a ghost over the slots.
Which recipes appear comes from the advancement rewards already unlocked; the open and craftable-only settings of each book and the unlocked recipes are saved in the player's recipeBook compound. Recipes now keep their category and group from the data pack.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Llamas and trader llamas: random coat and strength, tamed by riding with a maximum temper of 30, fed wheat and hay, given a chest and a carpet (shears take the carpet off), spitting at whoever hurt them or wolves close by, hunting zombies and illagers when they belong to a trader, forming caravans behind a leashed llama, and saved with their variant, strength, chest, carpet and lead. Leads: put on and taken off mobs, tied to fence knots, with the elastic pull, the snap past twelve blocks and following their holder, drawn as a rope and saved by holder. The wandering trader gets its own goals (drinking invisibility at night, fleeing monsters, walking to its wander target, staying home, standing still while trading, leaving after two days) and a spawner that brings two leashed trader llamas. Llama spit now moves before it drags and falls, and camels spawn on sand.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
/data now supports get, merge, modify and remove on entities, block entities and command storage, with NBT paths (children, indices, [] and [{...}] matches, root matches), scaling for numeric reads, the append, prepend, insert, set and merge modifiers, and from, string and value sources. Command storage is saved per namespace in data/<namespace>/command_storage.dat. /tag adds, removes and lists entity tags, which selectors' tag=, nbt= and advancements= options now check. /summon accepts a full NBT compound (custom name, tags, health, NoAI, Invulnerable, item contents and so on), and armor stands, item frames, paintings, boats and minecarts can now be selected, teleported and killed like other entities.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Game events now reach sculk sensors, calibrated sensors, shriekers and wardens through the vibration system: frequencies, listener ranges, travel time, wool occlusion, sneaking and dampening rules, and the nearest-first selection. Sensors power redstone and comparators and resonate amethyst, shriekers warn players and summon a warden at the fourth warning, catalysts take the experience of nearby deaths and spread sculk, and the warden has its anger, brain, sonic boom, animations and layers.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Dispensers now follow DispenserBlock's behaviour table: arrows, eggs, snowballs, experience bottles, thrown potions, firework rockets (flying straight until they hit something) and fire charges are shot out with the vanilla power and spread; buckets place or pick up water, lava, powder snow and mobs through the new Behaviours bucket calls; flint and steel, bone meal, TNT, shears (sheep, mooshrooms, bogged, beehives), brushes on armadillos, honeycomb waxing, respawn anchors, glass bottles (water, honey), water potions on mud, boats, minecarts, armor stands, shulker boxes, carved pumpkins (which now build snow and iron golems for players too), wither skulls, spawn eggs and worn equipment on players, armor stands and mobs that may pick up loot. Every dispense plays the vanilla click, fail or launch sound and the smoke puff, and droppers keep the plain drop or container insert. Experience bottles can also be thrown by the player.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The crafter block now works: right-clicking opens its menu with the 3x3 grid, the recipe result preview and the powered arrow, and clicking an empty slot disables or re-enables it (with the button click sound and the lighter disabled slot art), while shift-click moves stacks between the grid and the inventory. When powered, it crafts after four ticks using the grid's recipe, plays the craft or fail sound, shows the crafting state for six ticks, pushes the result and any remainders into the container in front (or shoots them out with white smoke), uses up one of each ingredient and fires the crafters-crafting-crafters advancement.
Its comparator signal counts filled and disabled slots, hoppers fill its smallest stacks first and skip disabled slots, and its items, disabled slots, powered flag and animation ticks are saved with the chunk.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Show Subtitles accessibility option and the notification display time slider now exist and persist in options.txt. Every sound event with a subtitle key in sounds.json that the audio engine plays, even with no audio device, feeds a closed-caption overlay at the bottom right: repeated sounds refresh the same row, rows are only shown while the source is in range, an arrow points left or right when the sound is outside the view, and the text fades from white to grey over three seconds (stretched by the display-time multiplier, with looping sounds staying bright for half a second longer).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Scoreboards now work like vanilla: objectives with every criteria kind (dummy, trigger, health, food, xp, level, armor, deathCount, kill counts, per-colour team kills and any statistic such as minecraft.mined:minecraft.stone), the full /scoreboard command (objectives add, remove, list, modify and setdisplay; players set, add, remove, reset, get, list, enable, operation and display name or number format), /trigger, and /team with colour, prefix, suffix, friendly fire, invisibility, nametag, death message and collision options. Scores, teams and display slots are saved as vanilla NBT in data/minecraft/scoreboard.dat. The sidebar draws with the objective title, team colour slots, number formats and the vanilla layout, and target selectors accept scores={...}, team= and tag=.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The loom now opens its menu: a banner, a dye and an optional pattern item go in, the 32 plain patterns (or the one the pattern item provides) show in a scrolling 4x4 grid with hover names, picking one previews the result banner and the result slot gives a banner with one more layer, using up one banner and one dye. Banners with six layers show the error state and give nothing. Shift-click routes banners, dyes and pattern items to the right slots.
Banner layers are stored in the banner_patterns item component, in a new banner block entity (saved to chunks), and carry over when a banner is placed or dropped. Placed banners draw all their pattern layers, banner and shield icons are drawn as the vanilla 3D models with their patterns, a held banner is shown in the first-person hand, and tooltips list the layers. Banner duplication and shield decoration crafting recipes work, and crafting now returns item remainders such as buckets.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sculk sensors cycle through active, cooldown and inactive (30 ticks, 10 for the calibrated one), power the block below them and click; the shrieker ends its 90 tick shriek and asks to respond; the catalyst blooms for 8 ticks. The level SculkSpreader moves charge cursors across sculk, veins and plain blocks, growing sensors and shriekers, merging cursors and reporting charge particles.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Chunks saved with isLightOn now read their SkyLight and BlockLight arrays back like vanilla instead of being relit: a missing sky layer repeats the bottom layer of the section above it, and a missing block layer is dark. Such chunks skip the light job, mesh as soon as their neighbours are lit, and only relight when a freshly generated neighbour arrives. tools/test_light_parity.py builds a scene of slabs, stairs, snow, trapdoors, lanterns, leaves and lava in a vanilla world, edits it, and requires every saved light value and every incremental relight to match Horizon's engine.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entities and physics move to 0.75 now that the dragon, vehicles,
decorative entities and most mobs are in; the gap lists and next steps
follow the code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chunks gain inhabited time while a player is near (saved as
InhabitedTime), giving vanilla's local difficulty. Zombies, husks,
zombie villagers and skeletons spawn with armour sets by it, piglins with
golden pieces; worn armour protects them, is drawn with the humanoid
armour layers, drops 8.5% of the time and is saved. Zombie, drowned and
zombified piglin weapon odds now follow 26.3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villagers claim a free bed and a bell, work at their job site by day
(work sounds, and up to two restocks a day with demand catch-up), meet
at the bell in the evening and sleep in their bed at night, lying as
vanilla draws sleepers. They wake at dawn, when hurt or when the bed
goes. Memories, restock counts and the last restock are saved.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Villagers claim the nearest free job site within 48 blocks and take its
profession, lose a fresh profession when the site goes, flee zombies and
raiders, and shake their heads when they have nothing to sell.
Right-clicking a villager or wandering trader opens its trades; offers,
experience, level, profession and job site are saved as vanilla NBT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Allay: given an item it keeps the player as its friend, flies to matching drops within 32 blocks, carries up to a stack and throws it to the player (or a liked note block) one item a tick within 3 blocks, pausing pickups for three seconds after each throw; it dances by a playing jukebox, duplicates when given an amethyst shard mid-dance, heals over time, cannot be hurt by its player, and hands its item back to an empty hand; animation and NBT. Mobs now save and load what they hold (equipment mainhand and offhand), and models whose root holds another part named root (allay, vex, frog, baby axolotl) no longer apply that offset twice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Camel: always tame, saddled and ridden like a horse (the rider at the hump, lower while it sits), dashing forward on the jump bar with a 55-tick cooldown and faster while sprinting, sitting down and standing up now and then (no moving meanwhile, up at once in water or when panicking), eating cactus, the keyframe animations (walk, sit, sit pose, stand up, idle, dash) and its NBT. Mobs whose health is above zero no longer keep a death clock from a save, and a dead wither stops healing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Parched: the desert skeleton with 16 health, a 70-tick bow interval and arrows of weakness for 30 seconds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Building a T of soul sand or soul soil under three wither skeleton skulls (upright along X or Z) summons the wither: eleven seconds of invulnerable charging that heal it and end in a power 7 blast, then flight toward its target while the centre head fires skulls on a ranged-attack clock and the side heads pick their own targets, blocks around it broken when hurt, regeneration, immunity to arrows below half health and to the undead, a purple boss bar with boss music, the nether star drop, its animation, texture and scale. Wither skulls fly like fireballs (blue ones keep their speed), hit for 8 with withering and explode. Block items such as heads aimed at a block are now placed instead of equipped. Vexes no longer drop their swords.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ravager: melee charges speeding up while hunting, trampling leaves it runs into, the 10-tick bite with its head lunge, stunned for two seconds by a raised shield half the time (else flinging the defender), then a roar that hurts everything but illagers within 4 blocks and flings it away, and its NBT.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Evoker: the spellcaster goals with their warm-ups and intervals (summoning three vexes for 30 to 120 seconds, fangs in a ring when the target is close or a line of sixteen toward it, turning blue sheep red), fleeing players, the spellcasting arm pose. Vex: flying through blocks around its bound origin, charging its target with an iron sword, copying its evoker's target, wasting away after its life. Evoker fangs rise from the floor after their delay and bite for 6. Guardian beams are only drawn for guardians.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker: attached to a block face (re-attaching or teleporting when it is lost), idle peeks, opening to fire a homing bullet every one to five and a half seconds at a player within 20 blocks, arrows bouncing off the closed shell, a chance to teleport below half health, dyed shells, the lid and head animation and the rotation onto its face, and its NBT. ShulkerBullet steps toward its target along axes and levitates what it hits for 10 seconds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guardian: the swimming move control with its wobble, water travel, stroll and targeting of players, squid and axolotls, the laser charging 80 ticks (60 for the elder) before indirect magic plus a bite, spikes hurting melee attackers while still, flopping on land, the tail, spikes and eye animation, the rendered beam, and the spawn rules. The elder curses players within 50 blocks with mining fatigue every minute.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Structure pieces now spawn their entities: the swamp hut's witch and black cat, the monument's three elder guardians, end city sentry shulkers and the elytra item frame, mansion evokers, vindicators and allays, ocean ruin drowned, and mineshaft chest minecarts with their loot rolled in. Chunks keep their structure references (saved as structures.References), so structures_at answers advancement location predicates, and natural spawning uses structure spawn_overrides (guardians in monuments, witches in huts, fortress mobs on nether bricks).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Structure templates now place their saved entities (StructureTemplate.placeEntities: moved, turned and mirrored with the template, finalized for jigsaw pieces, ignored for mansions and end cities), carried in the proto chunk and spawned with the chunk: villages get their villagers and animals. /locate structure (an id or a #tag) searches ring and random-spread placements like ChunkGenerator.findNearestMapStructure; seed 42 matches vanilla for villages, monuments, swamp huts, igloos, strongholds and outposts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HumanoidArmorLayer for the player's model (in third person and the inventory), the armour parts posed like the body. Shift-clicking armour or an off-hand item in the inventory puts it in its empty slot (InventoryMenu.quickMoveStack), and shift-clicking an armour slot takes it back. The GUI clip handles rectangles with corners in any order.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
F5 cycles first person, third person behind and in front (Camera.setup with getMaxZoom pulling the camera in front of blocks); the hand and crosshair show only in first person. The local player renders as a PlayerModel kept in step each tick (body turning, walk animation, swing, crouching and riding poses), in the world and in the inventory's player box following the cursor. Players now use the player skin instead of the zombie texture.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShieldSpecialRenderer's plate and handle under item/shield's gui display transform, instead of the missing-icon square.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Advancements screen: one tab per displayed root, the tree laid out by a port of TreeNodePosition in AdvancementTree.addAll order, visibility from AdvancementVisibilityEvaluator, connection lines, frames, the progress tooltip with its split title bar, dragging, scrolling and the L key. /advancement grant|revoke with only/from/until/through/everything and criteria, with vanilla's messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
recipe_unlocked matches the criterion's recipes key, so unlocking one
recipe no longer completes every recipe advancement; inventory_changed
honours the occupied, full and empty slot counts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The advancement tree loads from the data packs; criteria are tested
with vanilla's entity, item, block and location predicates and loot
conditions, and triggered by inventory changes, location (every second),
kills, deaths, eating, filling buckets, placing blocks, sleeping,
dimension changes, breeding, taming, riding and totems. Completed
advancements grant their rewards (experience, loot, recipes), show the
advancement toast and are announced in chat; progress is saved to
players/advancements/<uuid>.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Picked up, dropped, crafted, used (placing blocks) and broken items,
damage dealt and taken, jumps, fish caught, animals bred, enchanting,
leaving the game, being killed by a mob, and opening chests, barrels,
shulker boxes, hoppers, droppers, dispensers and every crafting station
now count, as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pause menu's Statistics button opens StatsScreen: the General tab
lists every custom statistic with vanilla's number, distance, time and
tenths formats, the Items tab the mined/broken/crafted/used/picked
up/dropped columns with sorting, and the Mobs tab kills and deaths per
mob. Teleports no longer count as distance travelled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Phantoms circle an anchor high above the player, swoop at them every
eight to twelve seconds, bite on contact and keep away from cats (which
hiss); they spawn over players who have not slept for three days. The
statistics behind that are tracked as vanilla does: play time, the
time-since counters, movement by kind, deaths, kills, blocks mined and
sleeps, saved to players/stats/<uuid>.json.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Saddles fit adult pigs and striders; a saddled one can be ridden and
steered with a carrot or warped fungus on a stick, whose use boosts it
and wears the stick (a worn-out stick becomes a fishing rod). Saddles,
horse armour and donkey chests drop when their wearer dies, and saddled
pigs and striders keep their saddle in the save.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Horses get random health, speed and jump strength, coat colours with
markings, grazing, rearing and AbstractEquineModel's animation. Feeding
heals, grows foals and builds temper; riding an untamed horse ends with
it tamed or bucking the rider off. Saddles, horse armour and donkey
chests fit with a click, a saddled mount follows its rider with the
charged jump bar, its health replaces the food bar, and offspring
inherit their parents' stats (a horse and a donkey have a mule).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Piglins leave gold-wearing players alone, pick up and admire gold, barter
gold ingots for a roll of the bartering table, fight with crossbows or
golden swords, hunt baby hoglins, flee zombified piglins and get angry
when their chests are opened or their gold mined. Brutes always attack.
Hoglins headbutt their targets into the air, babies flee, and both turn
into zombified piglins and zoglins after 15 seconds out of the Nether.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pillagers charge their crossbows, keep their range and fire; vindicators
hunt with iron axes (Johnny attacks everything). IllagerModel's crossed
arms, crossbow hold and charge, and weapon swings are ported, and a calm
vindicator hides its axe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Iron golems hunt monsters (not creepers), close in, swing for a random
half to one and a half times their damage and throw the target up, show
cracks as they weaken, take iron ingots for repairs, offer poppies to
villagers and hold a grudge against a player who hits them. Snow golems
pelt monsters with snowballs, leave a snow trail and melt in hot biomes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
They no longer attack on sight: once hit they stay angry for 20 to 39
seconds, move faster, growl and rally the others nearby. They spawn with
golden swords.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blazes hover toward targets above them, slow their fall, smoke and fire
three-fireball volleys; ghasts float about, face their target, cry and
shoot exploding fireballs credited to them. The blaze rods spin and ghast
tentacles sway. Blazes, endermites, silverfish, experience orbs and the
fishing bobber pointed at texture paths 26.3 moved, so they were
invisible.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Cats get their variants, collars and the feline model's sitting,
crouching, sprinting and lying poses. Wild cats flee players unless
tempted with fish, which may tame them; tame cats sit on command, follow
their owner, lie on beds, sit on chests and lit furnaces, stalk rabbits
and have tame kittens.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Wolves get their biome variants, the tame and angry textures, the collar
layer and WolfModel's sitting, begging and shaking poses. Bones tame them;
tame wolves sit on command, follow and teleport to their owner, defend it
and join its fights, heal on meat, take collar dyes and breed into tame
pups. Mob particle bursts (hearts, smoke, splashes) now show.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Wire the paused witch, enderman and cube-mob modules: witches drink and
throw potions, endermen teleport, stare-aggro and carry blocks, slimes and
magma cubes hop with their size, squish and split. Slime sizes save, the
outer slime layer renders, and mob-applied effects use the player's effect
list (so they show and tick like potions).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A combat tracker records the player's damage, its causing mob and the
fall, and builds CombatTracker's message: falls from ladders or high
places, assisted falls, being slain or shot by a named mob, kill credit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spectators fly through blocks without overlays, a hotbar or a hand, and
do not interact, pick up items or trigger plates; adventure players cannot
build. The game mode is saved and restored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four crystals on the exit portal rebuild the pillars one by one and bring
the dragon back, as DragonRespawnStage does; breaking one of them aborts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The dragon with its phases, path nodes, fireballs and breath, end crystals
that heal it, the boss bar and boss music, and the kill: XP, the exit
portal, the egg and a new gateway. The fight state is saved where 26.3
saves it. Creative players are now invulnerable, as in vanilla.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Map data now lives in data/minecraft/maps/<id>.dat and the id counter in
data/minecraft/maps/last_id.dat (SavedDataStorage resolving the
minecraft:maps/<id> and maps/last_id identifiers), so vanilla and Horizon
read each other's maps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Frogspawn hatches into 2-5 tadpoles that swim, flop on land, wag their
tails and grow into frogs after 24000 ticks. Frogs take the warm, cold or
temperate variant from the biome. AgeableMob ages (babies, breeding
cooldowns) are now saved and loaded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Books and quills open BookEditScreen: pages of 14 lines of 114 pixels and
1024 characters, up to 100 pages, page buttons that add pages, Done and
the signing screen (15-character title, finalize warning) that turns the
book into a written book by the player. Written books open BookViewScreen.
Lecterns take a book, open LecternScreen with Take Book, send a 2-tick
redstone pulse on page turns (with the library's unpowering tick) and drive
comparators from the page. Both book components and the lectern block
entity are saved; open book and sign screens no longer pause on focus loss.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CartographyTableMenu: zooming a map out with paper (a fresh map one scale
up), cloning it with an empty map, and locking it with a glass pane, the
new map ids made when the result is taken, the take sound, and the screen's
previews with the error and lock sprites. Quitting now closes the open
menu first so its items go back to the inventory, and map textures upload
with the drawn frame's HUD batch only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Empty maps create a map centred on the player's grid cell; held maps reveal
the world with MapItem.update (dominant map colour per pixel, height and
water-depth shading, the dithered edge, the ceiling noise in the Nether)
and carried maps track the player marker. Maps save as data/map_<id>.dat
with idcounts.dat, their 128x128 textures live in reserved atlas slots,
and they render held in both hands over the map background and full-face
in item frames (the map=true frame models). Block facts now carry the
MapColor id.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
BeaconBlockEntity: beam sections tinted and blended by stained glass,
pyramid levels from beacon_base_blocks every 80 ticks, primary and
secondary powers for players in range (amplified when doubled), and the
activate, ambient, deactivate and power-select sounds; saved with the
vanilla keys. The 230x219 BeaconScreen has the power buttons, the upgrade
button, payment slot and confirm/cancel. BeaconRenderer's rotating beam and
glow scroll through a new wrapped-sprite mode of the entity shader.
/setblock and /fill now replace block entities with the new block's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Thunderstorms now roll ServerLevel.tickThunder per chunk: the bolt strikes
the top of the column or a living entity under open sky nearby, may bring a
skeleton trap horse, and LightningBolt ticks its flashes with the thunder
and impact sounds, fire, lightning rods and thunderHit (5 damage and
burning, charged creepers with their energy swirl, zombified piglins,
witches, mooshroom colours, turtles, burnt items). The bolt is drawn by
LightningBoltRenderer's layered branches through a new additive entity
pipeline, and /summon accepts lightning bolts and minecarts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Minecarts follow OldMinecartBehavior: rails and slopes, powered and
activator rails, coming off the track, the rider's push, players and carts
pushing each other, placement, breaking and saving, with chest and hopper
containers, furnace fuel and push, and TNT priming and explosions. They
render on the rail between the front and back points with their display
block.
Jukeboxes take jukebox_playable discs, play the song with the rainbow Now
Playing overlay and note particles, emit 15 while playing, drive
comparators, eject the disc and save it.
Name tags name mobs (saved as CustomName); the name shows above the head on
a translucent background when targeted or always visible, and Dinnerbone
and Grumm render upside down.
Blocks are now picked and outlined with their vanilla outline shapes
(exported per state with the edges LevelRenderer draws), so rails, slabs,
torches and plants no longer act as full cubes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rockets launch from a clicked block or boost a gliding player, fly with the
1.15 acceleration and 0.04 lift for 10*(flight+1)+rand(6)+rand(7) ticks, then
explode: 5+2n damage within 5 blocks with line of sight, and the client
starter spawns the shaped spark bursts, trails, twinkles and blast sounds.
/give now applies its [component=value] list through a new SNBT parser.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Jumping in the air with a usable glider starts fall flying (the
FALL_FLYING pose), travelFallFlying applies vanilla's lift, dive
conversion, climb and 0.99/0.98 drag, the elytra wears every 20 ticks
of flight, and gliding stops on landing or when it can no longer glide.
Right-clicking swappable armor equips it (Equippable.swapWithEquipmentSlot),
returning the worn piece, honouring Curse of Binding and playing the
equip sound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Double-tapping jump within seven ticks toggles flight (jumping off the
ground when it starts), jump and sneak move up and down at three times
the 0.05 flying speed, horizontal acceleration uses the flying speed
(doubled while sprinting), Player.travel keeps 0.6 of the vertical speed,
flight ends on landing, and flying players neither crouch nor take fall
damage.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Throwing after ten ticks (one durability, kept in creative), Riptide
launches in water or rain, and the thrown trident's flight, 8 damage plus
Impaling against aquatic mobs, reversed bounce, sticking, Loyalty return,
pickup and ThrownTridentRenderer. horizon-ctl status also reports the
item in use and thrown tridents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Charging over 1.25 s less Quick Charge with the loading sounds, loading
arrows into charged_projectiles (three with Multishot, one arrow used),
and shooting critical arrows at power 3.15 fanned 10 degrees apart with
vanilla's shot pitches and durability use.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FishingRodItem casts and reels in with vanilla's sounds and rod damage.
The hook flies, bobs, runs the lure and bite timers (rain and sky speed,
the Lure enchantment), tracks open water, hooks mobs and pulls them, and
retrieving a bite rolls gameplay/fishing with Luck of the Sea, open-water
treasure, experience and the catch thrown at the player. Loot rolls now
honour luck (entry quality, bonus rolls) and fishing-hook predicates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shulker boxes are 27-slot containers that refuse other boxes, keep their
contents when broken, exploded or dropped (the item's container
component) and restore them when placed. Ender chests show the player's
ender inventory, saved as EnderItems. Both play their open and close
sounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The combat kernel's death-protection hook fires for a totem in either
hand unless the damage bypasses invulnerability; the game then uses up
the totem (main hand first), sets health to 1, clears effects, applies
regeneration II, absorption II and fire resistance, and plays the totem
sound and 30-tick particle emitter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbstractBoat's status (under water, in water, on land with the averaged
block friction, in air), floatBoat buoyancy and drag, controlBoat
paddling with turn and acceleration, collisions, paddle sounds and
animation, riding (seat attachment, 105 degree head clamp, out of
control under water) with dismounting forward onto floor, BoatItem
placement on blocks or the water surface, VehicleEntity damage and
breaking, entity saving and AbstractBoatRenderer with the hurt wobble.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ArmorStandItem placement snapped to 45 degrees, gravity, interactAt
equipment swaps by clicked height, the two-hit break with drops (one hit
in creative), entity saving (equipment, Small, ShowArms, NoBasePlate,
Invisible, Marker) and ArmorStandRenderer with the default pose, hit
wobble and humanoid armor layers from the equipment assets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Frames hang on any face (ItemFrameItem), survive per ItemFrame.survives,
take one item and rotate it in eight steps, give the item back and then
break on attack, save as vanilla entities (Facing, Item, ItemRotation,
ItemDropChance, Fixed, Invisible) and render the baked frame model with
the item at half size; glow frames light their item fully.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Painting items hang on wall faces with Painting.create's choice (the
largest placeable variants that fit, at random), HangingEntity.survives
rechecked every 100 ticks (collision, solid support, no overlap),
breaking by attack with drops and sounds, entity saving (variant,
facing, block_pos) and PaintingRenderer's front, back and edge quads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ComponentArgument reads SNBT or JSON text components (text, translate
with fallback, selector, keybind, colours, formatting, extra, lists);
selector components resolve for the command source. /title drives the
Gui title, subtitle, times and action bar, drawn with vanilla's fades
and 4x / 2x scales over the hotbar.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ExecuteCommand's tree with redirects: as, at, positioned (as), rotated
(as), facing (entity), align, anchored, in, and if/unless block, entity,
loaded and dimension, as conditions or tests. Commands run with a real
command source (entity, position, rotation, anchor): @s, coordinates,
local coordinates from the anchor and /say and /me names follow it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Off hand: the InventoryMenu shield slot, the hotbar's off-hand slot,
swapping with F, saving as slot 150, and death drops (which also missed
armor). Shields and other blocks_attacks items rise from either hand,
block after their delay through the combat kernel's applyItemBlocking
hooks (angle, damage reductions, bypassing types, piercing arrows), lose
durability, play their sounds, push the blocker back on partial blocks
and get disabled by axes. Using any item now slows walking by its
use_effects speed multiplier.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Camera.tickFov / calculateFov: movement speed (sprint, speed effects),
bow draw and spyglass zoom ease in at half a step per tick, with the
death and underwater/lava narrowing. The spyglass zooms to 0.1, hides
the hand, drops the 8x mouse factor and draws the scope overlay; it
stops after 1200 ticks or on release with vanilla's sounds. Inventory
icons of display_context item models now use their gui case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apply clippy's fixes, fix the remaining lints by hand and allow, for the
whole workspace, the lints that one-to-one ports of vanilla's Java
naturally trip (argument counts, index loops, nested types, large enum
variants).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tree fixture world now applies vanilla's edge shape updates for its
obstacles (faceless vines, unsupported short grass), the vanilla fixture
world answers environment attributes with defaults, and features that
need structure templates or the pale moss registry, directly or through
named features, are left to the decoration harnesses. The scene probes
build their scene with World::test_scene since TestWorld was removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
214 of 215 differential harnesses pass and the client joins a vanilla
26.3 server; scores and gaps follow the merged parity work.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EffectCommands (give with seconds/infinite, amplifier, hideParticles;
clear all or one) and EnchantCommand with vanilla's level, support and
compatibility checks and response messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The block placement oracle had replaced VanillaPlacementOracle, which a
dozen feature harnesses call reflectively (context(JsonObject)). It now
lives in VanillaBlockPlacementOracle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MusicManager: menu, game, creative, underwater, biome and End tracks from
the background_music and music_volume environment attributes, with
vanilla's delays, replacement and fades; tracks decode on a worker and
play on a dedicated mixer voice. Ambient handlers: biome loops with
40-tick cross-fades, additions, cave mood from sampled light, and the
underwater loop, additions and enter/exit sounds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Oracles reference shared helper oracles (heightmap, level shape...) that
javac could not find when only the entry files were listed. Also let
chunkgen_bench list unsupported features (HORIZON_LIST_UNSUPPORTED=1).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve the places where union merges kept both sides: the persist
rewrite of level.dat now carries item components, XP seed and effects;
mob ticking uses the creatures module (with Thorns); explosions damage
mobs through the new damage source and emit particles; TNT's white
flash moves to overlay 5 since clouds use 4; End gateway data follows
the new dimensions/ folder layout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EndFlashState: one flash per 600 clock ticks with seeded offset, duration
and direction; the end_flash sprite in the sky, the flash intensity added to
the lightmap sky factor and the delayed weather.end_flash sound.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Options, Video Settings, Music & Sounds, Controls, Key Binds, Mouse, Chat,
Accessibility, Language (with search) and Resource Packs screens in the
HeaderAndFooter/OptionsList layouts. options.txt uses vanilla keys and
encodings (unknown lines kept), including key_<mapping>:key.keyboard.x
binds that the input path honours, per-source volumes, brightness, VSync,
max framerate, fullscreen (F11), chat and font options. Languages reload
live; resource packs (folders, zips, Programmer Art, High Contrast) are
layered over the vanilla resources through an overlay root.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/vanilla_login.py joins an offline 26.3 server (login and
configuration phases) so the server loads players/data/<uuid>.dat;
vanilla_world_check.py gains 'login NAME SECONDS' and 'sleep S' steps and
disables the whitelist. With it, vanilla loads a Horizon player with its
position, rotation, inventory, selected slot and abilities, and Horizon
reads the file vanilla writes back.
A new world's first spawn becomes its world spawn (no bogus respawn
point), the player UUID is kept across saves, the selected hotbar slot is
restored, and game_rules.dat only holds rules vanilla knows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Eyeblossoms follow the day timeline's eyeblossom_open attribute (waking
neighbours in vanilla scan order), turtle eggs crack and hatch on sand with
the timeline hatch chance, and dried ghasts hydrate while waterlogged. Adds
LevelAccess::day_time.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EnderEyeItem.use away from portal frames: the nearest ring position of
minecraft:strongholds (ChunkGenerator.getNearestGeneratedStructure, compared
at the chunk's (8, 32, 8) block, located at its corner), an EyeOfEnder
signalled towards it with vanilla's flight curve, launch and death sounds,
dropped back as an item four times in five after 80 ticks. The generator
exposes its cached concentric ring positions for this.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Java oracle loads the vanilla data pack's enchantment registry, binds
item components and replays EnchantmentMenu's cost, clue and enchantment
selection draws; tools/test_enchanting_parity.py compares it with the Rust
stations module for every enchantable item and the book over 32 seeds and
7 bookshelf counts (51,744 lines match).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Legacy jigsaw elements apply STRUCTURE_AND_AIR last, after jigsaw
replacement (outposts no longer carve air where jigsaws sat).
- Templates without a known shape run StructureTemplate's neighbor shape
updates (updateShapeAtEdge, then updateFromNeighbourShapes) through
level::Behaviours: snowy grass under igloos, stair shapes and fence
connections in shipwrecks.
- The placement oracle keeps going through structure entities (a level
stand-in with no enabled features) and compares brushable-block loot.
With block behavior exports present, 18 chunks of seed 1 covering
villages, igloo, outpost, mineshafts, shipwreck, warm ocean ruin, buried
treasure, ruined portal, trial chambers, monument, mansion, stronghold,
ancient city, trail ruins and a jungle temple match vanilla exactly,
blocks and block-entity loot seeds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answer ServerLevel.tickPrecipitation (Biome.shouldFreeze, shouldSnow,
getPrecipitationAt) and Level.isRainingAt from the worldgen bundle's biome
temperatures, so snow accumulates, water freezes, cauldrons fill, farmland
stays moist and fire is extinguished in rain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
shadows.rs ports EntityRenderer.extractShadow and ShadowFeatureRenderer
with the vanilla per-renderer shadow radii and strengths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
screen_effects.rs ports ScreenEffectRenderer and the Hud camera overlays
as clip-space quads drawn by a new gfx pass (alpha and vignette blend
modes, repeating atlas sprites), with tick-driven vignette brightness
and portal intensity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Worlds now use 26.3's layout and files: dimensions/minecraft/<dim>/{region,
entities}, level.dat with singleplayer_uuid, players/data/<uuid>.dat and the
saved-data files for weather, world clocks (day time), world generation
settings and game rules. Older Horizon saves are migrated on open.
Chunks are written like SerializableChunkData: vanilla's short palette form
for default states, SkyLight/BlockLight layers, the four FULL heightmaps,
block_ticks/fluid_ticks from the tick scheduler, chest/trapped chest/barrel
and furnace block entity IDs, and everything Horizon does not simulate
(unknown block entities, structure references, post-processing) kept from
the loaded chunk. Partially generated vanilla chunks are regenerated
instead of loading as holes.
Mobs, item entities, experience orbs, arrows and falling blocks are saved
per chunk in entities/*.mca with vanilla NBT (entity_nbt.rs), keeping each
loaded entity's UUID and unknown fields; entity types Horizon does not
simulate are written back unchanged. Entities leave the world with their
chunk and come back when it loads.
examples/save_parity re-encodes every chunk of a vanilla-saved world and
compares blocks, biomes, heightmaps, light, block entities and ticks
(identical on three reference worlds apart from vanilla's stale worldgen
light). A Horizon save of a vanilla world opens in the vanilla server with
its mobs, items, arrows and unknown entities.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The chat box reads logical keys so layouts and virtual keyboards type
what they show; container screens hide the chat like vanilla's layering;
HORIZON_NET_DEBUG=1 logs serverbound packets; horizon-ctl respawn; the
probe ends a client tick after teleports (the server accepts one
position per tick).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port HopperBlockEntity (push/pull, item entities, furnace faces, double chests,
transfer cooldowns), DispenserBlock/DropperBlock.dispenseFrom with the default,
bone meal, TNT, flint and steel, bucket and arrow behaviours, and container
comparator signals with setChanged notifications. Hopper and dispenser/dropper
block entities save with their own ids; their screens use the vanilla layouts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Mobs now run vanilla's tick order with a GoalSelector/target selector
(priorities, MOVE/LOOK/JUMP/TARGET flags), PathNavigation over the
walk/swim/fly/amphibious node evaluators, Move/Look/Jump/BodyRotation
controls and LivingEntity travel in air, water and lava. Path types,
spawn surfaces and keyframe animations come from a new exporter
(VanillaMobDataExporter -> mob-data.json). Farm animals, zombies,
skeletons, creepers and spiders get their vanilla goals; projectiles
(arrows, snowballs, eggs, ender pearls, fireballs) share one system;
natural spawning follows NaturalSpawner with category caps.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LevelChunk.postProcessGeneration: positions worldgen marked (aquifer fluids
next to open air, sculk veins, fences, ...) get their fluid tick, LiquidBlock
tick or shape update once the chunk and its neighbours are loaded, so cave
water and lava start to flow and bubble columns rise. Decoration now marks
the BlockBehaviour.Properties.postProcess positions of WorldGenRegion.setBlock
(mushrooms themselves, above soul sand and magma blocks), which vanilla's
bubble columns over underwater magma come from.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Offers come from the trade_set/villager_trade registries through each
set's RandomSequence (world seed + sequence id), with merchant predicates,
enchant_with_levels/enchant_randomly/filtered/set_potion modifiers and the
additional trade cost. MerchantOffer pricing (demand, price multiplier,
special price), payment matching and stock follow vanilla; the trading menu
matches MerchantScreen and levels the merchant up with new offers.
horizon-ctl 'trade PROFESSION [LEVEL]' opens it until villagers exist.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds the all-states catalogue and dense random scenes with fluids, a
--quick mode, fixture meshing time in the report, and clippy fixes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fog.rs implements the fog environments (atmospheric with rain fog,
water with water vision, lava, powder snow, blindness), the camera fog
type and computeFogColor (sunrise tint, sky mix, void darkness). The
sky shader evaluates SkyRenderer's sky disc, sunrise/sunset fan and dark
disc per pixel with sky fog; clouds fade by the cloud fog end.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A network thread logs into offline-mode servers, runs the configuration
phase (registries, tags, known packs, code of conduct) and forwards play
packets; keep-alives and pings are answered there. game/remote.rs drives
the game from the connection: server chunks feed the streamer (remote
mode: no generation, unload on forget), block updates, entities with
interpolation (mobs with models, other players, dropped items), health,
food, experience, time, weather, respawn, dimension changes, container
menus and inventory sync, chat and system messages, plus movement,
mining with prediction sequences, use-item, attacks and container clicks
sent like LocalPlayer and MultiPlayerGameMode. The title gets a
Multiplayer button with a servers.dat list, Add/Edit, Direct Connection
and a Disconnected screen. horizon-ctl gains connect, chat, respawn and
disconnect; --server/--name join at start.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Dim::End (DIM1 region folder, End environment attributes, sky light).
- Eyes of ender fill End portal frames; a complete ring of twelve
(EndPortalFrameBlock portal shape) lights the 3x3 End portal.
- End portals travel to the End (obsidian platform at 100 49 0, facing
west) and back to the world spawn; dying in the End respawns in the
Overworld.
- Without the ender dragon the fight counts as won: the active exit
portal (EndPodiumFeature) and the first gateway of the seed's
shuffled ring are placed once the main island is loaded.
- End gateways: TheEndGatewayBlockEntity exit search (chunk scan,
findValidSpawnInChunk, END_ISLAND fallback, findTallestBlock), exit
gateways linked both ways and kept in DIM1/horizon-end-gateways.json;
worldgen return gateways lead exactly to the End spawn.
- End sky box (end_sky.png tiled, 0x282828), End portal and gateway
surfaces and the spike End crystals (EndCrystalModel animation).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Potions can be drunk, thrown (splash/lingering) and filled from water;
effects tick like MobEffectInstance (hidden effects, periodic heal/poison/
wither/hunger), drive movement (jump boost, slow falling, levitation,
speed/slowness modifiers), damage (resistance, fire resistance) and mining
(haste/fatigue), and show as HUD icons. Enchantments affect gameplay from
their data components: efficiency, aqua affinity, unbreaking, sharpness/
smite/bane, knockback, fire aspect, power, infinity, protection family and
feather falling, thorns, mending, armor attributes, and fortune/silk touch/
looting in loot tables.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ChatComponent/ChatScreen port: wrapped 100-line history with the 200-tick
fade, scrolling, system message indicator, input history, argument
colouring, usage hints, parse errors and tab completion. Commands:
help, gamemode, defaultgamemode, give, teleport/tp, time (26.3 clocks and
markers), weather, kill, summon, setblock, fill, clone, experience/xp,
gamerule, difficulty, seed, spawnpoint, setworldspawn, clear, locate biome,
say, me, msg, list. Game rules gate the clock, weather, random ticks,
keep_inventory, death messages and command feedback, and are saved to
data/minecraft/game_rules.dat. F1 hides the HUD, F2 saves screenshots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt entities (gravity, drag, bounce, fuse) spawn from redstone, fire,
flint and steel / fire charges, unstable TNT broken by players and chain
explosions (random short fuse). TntRenderer swell and white flash via a new
entity overlay. Explosions use vanilla ray marching over a Java HashSet
(matching iteration and shuffle draws), exposure-scaled damage and knockback
for players, mobs, items and TNT, TNT drops without decay, merged drops and
container contents.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Smooth lighting and ambient occlusion now follow BlockModelLighter
(cubic/partial faces, diagonal samples through light-permeable blocks,
light blending with the sky-borrow and zero rules, partial-face weights),
with vanilla's integer colour arithmetic, cardinal shading, quad light
emission, flat lighting for emitting or non-AO blocks, tint source counts
and random block offsets. Fluids follow FluidRenderer: occlusion against
neighbour face shapes, legacy-solid corner heights, flow-rotated UVs with
Mth.atan2 and the sine table, water overlay beside glass and leaves, back
faces and max(self, above) light. Flat model elements skip their
degenerate faces like UnbakedCuboidGeometry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VanillaStructurePlacementOracle generates vanilla terrain with live
structure starts and references for the 3x3 chunks around a target, runs
the structure part of applyBiomeDecoration through a WorldGenRegion-like
level and dumps the target chunk's blocks and block entities. The
structure_blocks example does the same with Horizon's generator
(GeneratorWorker::set_structures_only skips placed features) and
tools/compare_structure_blocks.py reports the differences.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ParticleEngine gains ClientExplosionTracker-style explosion bursts,
TrackingEmitter and ServerLevel.sendParticles spreads; explode() and
Player.attack spawn them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port LightEngine.shapeOccludes / getFaceOcclusionShape (occlusion.nbt) so
slabs, stairs, snow layers, paths etc. block light directionally, and use
ChunkSkyLightSources.isEdgeOccluded for sky sources. Block light now rises
into the section above the highest occupied one.
Block edits on lit chunks are relit in place with vanilla's decrease and
increase passes instead of recomputing the 3x3 chunks (and remeshing every
section of them); edits that race a chunk light job are replayed onto it.
examples/light_parity compares Horizon light with the SkyLight/BlockLight
arrays of worlds saved by the vanilla 26.3 server (0 differences on
villages, outposts and a slab/stair/snow scene; 181 of 113M sky values
differ where vanilla's worldgen light is stale) and checks incremental
relighting against full relights. tools/vanilla_world_check.py runs the
vanilla server headless; tools/anvil_dump.py prints NBT and region chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chests, trapped chests and barrels written by world generation become
client containers; their loot tables are unpacked with the loot module
using the generated LootTableSeed and the container's biome. chunkgen_bench
can list generated block entities (HORIZON_BLOCK_ENTITIES).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Install the live feature registry (sharing the generator's worldgen bundle),
route bone meal through BoneMealItem.useOn, report biome names and the world
seed to block behaviour, play the bone meal sound and pop items from tall
flowers. GameLevel::max_y is now exclusive as LevelAccess documents. New
horizon-ctl commands: setblock, fill, getblock, container.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New horizon_world::loot: pools, weighted and nested entries, the item
modifiers chest tables use (counts, damage, random and level-based
enchanting, potions, stew effects, instruments, filtered/discard),
conditions, the stack splitter and the slot shuffle, all with vanilla's
random draw order. tools/compare_loot_fill.py runs a new vanilla oracle
(VanillaLootFillOracle) against the loot_fill example: all 58 chest
tables match slot for slot over 42 seeds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Workstation menus follow the vanilla screens: slot layouts, sprites, the
enchanting table's seeded offers with clues and galactic names, anvil
rename/repair/combine costs, grindstone disenchanting with experience,
smithing upgrades and trims, stonecutter recipe buttons, and the brewing
stand block entity with data-driven recipes. Item tooltips show rarity,
enchantments, potion effects and trims; enchanted items glint.
horizon-ctl gains menu commands (use, click-slot, click-gui, type, xp, ...).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Font: bitmap glyph ascent/oversampling, unihex glyphs rasterized on demand
into a reserved atlas region, bold/italic/underline/strikethrough and
StringSplitter wrapping. Languages layer lang/<code>.json over en_us.
The command module ports Brigadier parsing, suggestions and smart usage
with the vanilla argument types and the 26.3 command tree; game rules
use the 26.3 registry names and saved-data file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Primitive codecs (VarInt/VarLong, strings, positions, LpVec3, BitSets),
VarInt21 framing, zlib threshold compression, AES/CFB8 encryption, MD5
offline UUIDs, network NBT via horizon-world, item stacks with data
component patches, chunk and light payloads with paletted containers,
synched entity data, and typed packets for every connection state in both
directions with round-trip tests. Packet ids are generated from the data
generator's packets report. The probe example logs into a vanilla 26.3
server and decoded ~8000 packets (329 chunks) without errors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port TreeGrower (all growers incl. 2x2 dark oak, pale oak, mega spruce/jungle,
mangrove, azalea, poplar), BoneMealItem.useOn/growCrop/growWaterPlant and every
BonemealableBlock. Configured and placed features run on the live level via a
FeatureWorld/PlacementContext adapter with vanilla setBlock semantics. Also fix
torchflower crops never maturing and kelp growth into flowing water.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port EndCityPieces' section generators (house towers, towers, bridges,
fat towers and the single ship), the local collision rule of
recursiveChildren and the treasure chest data markers. Seeds 1 and 12345
match vanilla on all 158 End starts over 601x601 chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sections now report their block-entity blocks; a new block_entity_render
module draws them each frame with vanilla's baked layer models, renderer
transforms, animations (chest lids, banner wave, enchanting-table book)
and entity diffuse lighting. Adds setblock/fill control commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The 127 skipped placed features were all skipped because the worldgen bundle
lacked worldgen/blocks/{block-properties,cached-collision,cached-support}.nbt
and the survival definitions, which prepare_native_client.py only copied from
a previous simple-block parity run. Run the vanilla exporter directly when
those files are missing, and say so in the client when they are absent.
With them, every placed feature of the three dimensions runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Port NetherFortressPieces (bridge and castle piece weights, children,
moveInsideHeights, chests and the blaze spawner) and NetherFossilStructure
(noise-column floor search, template piece with the dried ghast). The
structure-start oracle and chunkgen_bench take a dimension; seeds 1 and
12345 match vanilla on 5024 of 5025 Nether starts over 301x301 chunks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
vanilla_diff compares Rust-decorated chunks with region files of a vanilla
world generated for the same seed; chunkgen_bench takes --dimension.
configure_end_gateway stores the exit of end_gateway_return gateways instead
of failing the feature.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Item stacks carry vanilla's extra components, saved in chunk and player NBT.
The enchantment registry loads from the data pack with ordered tags, the
EnchantmentHelper cost/selection rules, value effects and level-based values,
and the anvil, grindstone, smithing and stonecutter result rules.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 15:03:28 +02:00
1693 changed files with 497628 additions and 23616 deletions
<img src="docs/media/banner.webp" alt="Horizon: Minecraft: Java Edition 26.3, rewritten in Rust and checked against vanilla by 281 differential harnesses" width="100%">
Horizon is a Rust rewrite of Minecraft Java **26.3** targeting one-to-one vanilla gameplay parity, alongside client performance research. Not affiliated with Mojang or Microsoft.
A Rust rewrite of Minecraft: Java Edition 26.3 built for one-to-one vanilla parity, checked against the game itself with differential tests.
Every subsystem matches vanilla in all 281 parity harnesses, on Linux and macOS: 100% of what the harnesses compare (see [Project status](#project-status)). The native client is playable on Linux, macOS (Apple Silicon) and Windows.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## What changed in v2026.10.13.1
The latest release ([notes](https://github.com/ludovic111/horizon/releases/tag/v2026.10.13.1)):
- **Horizon's own menus:** a new title screen, world list, options and pause menu in the launcher's style, over a live frozen landscape. The vanilla look is still available: Options → Interface → Menu style → Classic.
- **Graphics settings, Simple and Advanced:** presets (Performance, Balanced, Quality, Ultra), or every rendering, quality, display and performance setting, each with a short description and its cost, plus a performance overlay.
- **Much faster rendering:** for example, at render distance 8 over a village, 363 → 2,409 FPS on an RTX 4080. Render distance 32 still stutters in places.
- **Fixed:** no sound on Linux; Linux builds that only ran on the machine they were built on; a missing DLL on Windows (the launcher no longer needs the Visual C++ runtime); memory growing and the frame rate dropping near villages (mobs were duplicated when chunks reloaded); resource preparation failing on Linux with "Missing … java-objc-bridge" ([#41](https://github.com/ludovic111/horizon/issues/41)); the hand and held item at high FOV; missing swing animations.
## Download
Horizon is downloaded only through **the Horizon launcher**: get it from [playhorizon.dev](https://playhorizon.dev) or from [the launcher's releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true) (the newest `launcher-v*` release, currently [launcher-v0.1.3](https://github.com/ludovic111/horizon/releases/tag/launcher-v0.1.3)), for Linux x86-64, macOS Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its resources from your own copy of Minecraft, starts the client or a dedicated server, and updates itself.
**You need** Minecraft: Java Edition installed with the official launcher, with 26.3 played once; Python 3.10 or newer and Java 25 (a JDK, e.g. Eclipse Temurin 25) for the one-time resource preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac.
### The launcher
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: v2026.10.14.1 ready to play, its summary, Client and Server, the version picker and Play, the resources and the Minecraft folder as a numbered list"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page, showing the notes of Horizon 2026.10.14.1"></td>
</tr>
</table>
It lists Horizon's releases and installs the build for your platform, shows each release's patch notes, updates Horizon and itself, and doubles as a command-line tool and an MCP server. See [the launcher's README](launcher/README.md).
## Screenshots
Captured in the Rust client itself, with vanilla assets and no mods or shaders.
### Menus and settings
<table>
<tr>
<td colspan="2"><img src="docs/media/menu-title.webp" alt="Horizon's title screen: the mark and the pages on the left, the Horizon title, Singleplayer and Multiplayer, and the last world played and the faint wordmark, over a veiled snowy landscape"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/menu-worlds.webp" alt="The Singleplayer page: three worlds with their dates and game modes, and Create world and Play buttons"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
</tr>
</table>
The vanilla menus are one setting away: Options → Interface → Menu style → Classic. See [Graphics settings](docs/graphics.md).
### In game
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village with farms, an iron golem and animals"></td>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie attacks near the village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/night.webp" alt="Moonrise over a torch-lit village and river"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/underwater.webp" alt="Salmon swimming in a river"></td>
<td width="50%" valign="middle">
Vanilla world generation ported to Rust (terrain, aquifers, carvers, features, villages and the Nether), every block model with smooth lighting and biome tints, the vanilla sky and fog, mobs, the HUD, chat and advancements: everything in these frames is drawn by Horizon.
</td>
</tr>
</table>
## What it does
Horizon reimplements the Minecraft Java client and its game logic in Rust, one subsystem at a time, and compares each piece with the vanilla game. Each `tools/test_*_parity.py` harness drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, then checks that the outputs are identical. It is for players and developers interested in game reimplementation, and for anyone who wants to see how far a behaviour-exact native rewrite can go.
The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla generators: noise terrain, aquifers, surface rules, carvers and placed features, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay includes:
**The Rust game is now playable.** The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla overworld generator: noise terrain, aquifers, surface rules, carvers, and the placed features ported so far, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay so far:
- validated player physics, health and hunger;
- mining with vanilla tool rules and loot tables;
- placing, crafting, chests and furnaces;
- dropped items and particles;
-sounds;
-passive and hostile mobs with combat;
-Anvil saves.
- dropped items, particles and sounds;
-passive and hostile mobs with combat and vanilla AI, including guardians, shulkers, illagers, ravagers, the wither, the ender dragon fight, the breeze, the creaking, the sniffer, the happy ghast, llamas and the copper golem;
-villagers that take jobs, trade, restock, meet at the bell and sleep in beds, on a port of vanilla's brain;
-raids and pillager patrols;
- dispensers, crafters, the loom and the recipe book;
- every command, scoreboards and teams, the player list, subtitles and the HUD overlays;
- boats, minecarts, item frames, paintings and armor stands;
- shields, maps, books, fireworks, beacons, advancements and statistics;
- Anvil saves, joining vanilla servers, and opening a world to LAN.
Parity is **not** complete. Still missing: multiplayer, the End, most structures (jigsaw starts and five piece-based structures exist), about half the placed features (127 of 273 are skipped), most mob types, chat and commands, enchanting, anvils, brewing, smithing and trading, and sapling growth. Fluids, redstone, pistons, crop growth, random and scheduled ticks and the Nether run in the client. See [Rust rewrite status](docs/rust-parity.md).
## Agent control
### Java parity: 55%
AI agents and scripts can play Horizon. `horizon-ctl`, shipped beside the game in every release folder, is a command line that reads the game as JSON and acts like a player; `horizon-ctl mcp` serves the same commands as an MCP server (one tool per command, screenshots returned as images).
```sh
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
horizon-ctl state # position, health, food, target, screen...
horizon-ctl find-block birch_log # the nearest logs
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
horizon-ctl break -1 65 -3 # mines it; replies with the drops
horizon-ctl open-inventory && horizon-ctl click-slot 0# menus: slots, buttons by label, text fields
```
Commands block until the action is done and reply with what happened; errors are JSON with distinct exit codes, `horizon-ctl help` lists every command and `horizon-ctl COMMAND --help` explains one with examples. It talks to the game over a Unix socket on Linux and macOS and over a token-protected localhost port on Windows. See [Agent control in the native client guide](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
## Build from source
For developers. Requirements: Rust 1.90 or newer, Python 3.10 or newer, JDK 25, and on Linux the ALSA and udev development headers (`libasound2-dev libudev-dev` on Debian and Ubuntu). Development and the vanilla harnesses run on Linux.
Horizon contains no game files and never downloads them. It builds its resources from **your own copy** of Minecraft: Java Edition 26.3, verified against the checksums in `research-lock.json`:
1.**Client:** install Java Edition with the official launcher and play 26.3 once. Horizon reads `versions/26.3/`, `libraries/` and `assets/` from the launcher's game directory (`~/.minecraft` on Linux). If yours lives elsewhere, pass `--minecraft-dir` or set `HORIZON_MINECRAFT_DIR`.
2.**Server jar:** the 26.3 server jar, from the `downloads.server.url` in your launcher's `versions/26.3/26.3.json` (or [minecraft.net/download/server](https://www.minecraft.net/en-us/download/server) while 26.3 is the current release). Pass it with `--server-jar` or set `HORIZON_SERVER_JAR`. It is only needed the first time.
cargo run --release -p horizon-prepare -- sounds # optional: sounds from your launcher install
cargo run --release -p horizon-client
```
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, playing and scripting through `horizon-ctl` and its MCP server, and the architecture are described in [the native client guide](docs/native-client.md). The rest of the documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Project status
The table below is the project's own audit of each subsystem. Every subsystem now matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not proof that nothing else differs. The remaining known differences are listed below.
### Java parity: 100%
Each subsystem scores 0 (absent), 0.25 (started, not playable), 0.5 (playable,
major vanilla features missing), 0.75 (playable, mostly complete) or 1 (complete
and verified against vanilla by differential tests). The percentage is the
average; `tests/test_parity.py` keeps it in sync with this table. Last audited
2026-10-08 against the code, not the docs.
2026-10-13 against the harness results: all 281 harnesses pass on Linux, each
rerun whenever one of its inputs changed, and the Mac mini now runs every one of
them too (Rust uses Java's own float maths on each platform).
| Subsystem | Score | Main gaps |
| --- | ---: | --- |
| Window, input and game loop | 0.75 | No controls screen or fullscreen toggle |
| Assets and resources | 0.75 | Heuristic item icons; no language files or resource packs |
| Network protocol and multiplayer | 0 | No networking at all |
| Chunk storage and palettes | 0.75 | Client uses plain arrays; palettes only on save |
| Section meshing | 0.75 | Whole-mesh output not compared with vanilla |
| Rendering | 0.5 | Few particles; chest is the only block-entity model; 9 mob models |
| Lighting | 0.75 | Slabs/stairs light wrong; light not saved or tested against vanilla |
| World generation | 0.5 | No End; 127 of 273 placed features skipped; few structures |
| Region files and saves | 0.5 | Mobs, dropped items, scheduled ticks and light not saved |
| Entities and physics | 0.5 | 9 mob types with simple AI |
| Interaction and inventory | 0.5 | No enchanting, anvil, brewing, smithing or trading |
| Menus, HUD and chat | 0.5 | No chat, commands, controls screen or recipe book |
| Audio | 0.5 | No music or ambient sounds |
| Block behaviours and ticking | 0.5 | Saplings never grow; TNT has no fuse; hoppers/dispensers don't move items |
| Window, input and game loop | 1 | None: the game loop, options, camera, auto-jump, toggle sprint/sneak, local input and the frame limiter match vanilla |
| Assets and resources | 1 | None: item models match vanilla over 196,290 stacks and the client draws every item through them; the resource pack screen matches vanilla's drawing over 3,000 sessions of clicks, scrolling and searching, its incompatibility confirmation included |
| Network protocol and multiplayer | 1 | None: joins vanilla servers; the dedicated server (`--dedicated`) matches vanilla's files, status ping, login, configuration, play, chunks, chat, recipes, advancements and command tree, a client built from vanilla's packet code joins it, Horizon joins a vanilla server, and LAN discovery matches over 462 cases (offline mode only) |
| Chunk storage and palettes | 1 | None: client and saves share paletted containers, identical to vanilla over 266 scenarios |
| Section meshing | 1 | None: 112 scenes and over a million quads identical to vanilla |
| Rendering | 1 | None: entity models, block entity renderers (407 cases, End portals, gateways and the loom's banner preview included) and their animations (296 cases, End gateway beams included), the HUD overlays (2,200 states), the recipe book (600 states), the player list, subtitles and the world border match vanilla |
| Lighting | 1 | None: saved and relit light matches vanilla, slabs and stairs included |
| World generation | 1 | None: 2,352 whole chunks over 225 structure sites, in all three dimensions, identical to vanilla |
| Region files and saves | 1 | None: saves round-trip with vanilla, and worlds from 1.8 to 26.2 upgrade as vanilla's DataFixers do, superflat worlds included; old options.txt and hotbar.nbt files are upgraded too |
| Entities and physics | 1 | None: attributes, dimensions, death loot, spawn rules, natural spawning (15,500 cases), raids and patrols (1,900 cases), spawned mobs' gear, enchantments and state (`finalizeSpawn`, 24,000 cases) and mob AI (92 scenarios: villager breeding and gossip, the warden, ghastlings and copper golems included) match vanilla |
| Interaction and inventory | 1 | None: menu clicks match over 8,000 click sequences, the loom and the crafter included; recipe book placement (6,000 cases), its buttons (1,200), the saved book (1,500) and the loom and crafter screens (1,600) match vanilla |
| Menus, HUD and chat | 1 | None: every command runs as vanilla (758 commands over 39,685 parsed inputs), scoreboards and teams match (81,000 operations), the player list matches over 872,000 draw calls with heads drawn from players' skins (authlib's textures, SkinManager's lookup, legacy skin conversion), and the chat matches focused and scrolled; the resource pack screen (selection over 4,000 cases, drawing and its incompatibility confirmation over 3,000 sessions) and the chat input with its command suggestions, usage lines, Tab completion and history (3,000 sessions on vanilla's client command tree) match vanilla |
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000 crafts), block entity removal (containers spill, 4,000 cases), copper golem chest sorting and the sculk family (435 scenarios: shrieker warnings and warden summoning included) match vanilla |
### Known issues
Online-mode servers (Mojang's session servers) are not supported; skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels). Before opening a pull request, run the self-check and paste the block it prints:
**Target machine:** the Rust rewrite targets one machine, **ludodesktop**: Ubuntu 26.04, Intel i7-13700KF, NVIDIA RTX 4080, GNOME on Wayland. The final deliverable is a playable native Linux build validated there. macOS (including the M2 Pro Mac) and Windows are out of scope. Track implementation and acceptance criteria in [Rust rewrite status](docs/rust-parity.md).
**Java performance packs (no longer developed):** the earlier track of pinned Java performance packs, distant terrain, an original frame/tick recorder and client/server starter downloads. No release downloads are published; build the packs and starters locally with `python3 tools/build_release.py` (see [Build and run](#build-and-run)). They are no longer a development target.
## Java packs: Mac starter (out of scope)
Build **horizon-mac-starter-0.21.0.zip** with `python3 tools/build_release.py` (it lands in `dist/`), extract it and open `Mac-Start.command`. It verifies and installs official universal Prism Launcher in `~/Applications` if needed, then opens the balanced pack import. Sign into Microsoft **inside Prism yourself**. This project needs no credentials.
Use native **Java 25 ARM64**, maximum heap **6144 MiB**, minimum **512 MiB** on your 16 GB M2 Pro. Prism can download Java automatically. Start at 1280×800 or 1920×1200; full Retina resolution is a larger GPU workload. [START-HERE.txt](packs/START-HERE.txt) has details. The same `.mrpack` files import into Prism on Windows and Linux.
If Prism is already installed, follow the [Mac import and first comparison guide](docs/mac-first-test.md).
Core and balanced also pass [macOS title/demo-world rendering probes](docs/client-graphics-smoke.md)
on the hosted ARM64 software renderer, including readback, saves and clean exits.
Physical M2 GPU performance and macOS 27 remain to be tested.
Balanced combines [Sodium](https://modrinth.com/mod/sodium), [Lithium](https://modrinth.com/mod/lithium), [FerriteCore](https://modrinth.com/mod/ferrite-core), [ImmediatelyFast](https://modrinth.com/mod/immediatelyfast), [EntityCulling](https://modrinth.com/mod/entityculling), and [Distant Horizons](https://modrinth.com/mod/distanthorizons). Cinematic adds [Iris](https://modrinth.com/mod/iris) and [Complementary Reimagined](https://modrinth.com/shader/complementary-reimagined), initially disabled. Vulkan independently experiments with the game's own backend and fallback, without Iris.
Distant LOD is simplified terrain. Nearby blocks retain full detail. Initial generation costs CPU/disk/memory and needs warm-up. DH adjusts clouds, chunk fade and transparency to blend its renderer; assess appearance as well as speed. Shaders cost GPU time and have [Mac-specific limits](https://shaders.properties/current/reference/miscellaneous/macos/). Universal CPU/GPU support and exact vanilla equivalence remain validation goals. Simulation distance stays 12 in every profile; normal 20 TPS rules are retained.
For the first comparison at the same view distance, use `baseline` versus
`core-performance`: both select 12 full-detail chunks and no distant LOD or shaders.
The latter adds only the pinned core performance mods and leaves every original
controller disabled. Then test balanced separately for its additional distant
terrain. Matching captured settings does not prove pixel-identical rendering.
[Core comparison protocol and selected observations](docs/core-performance.md)
include three repeated matched-view software-rendered pairs with 14–21% higher
FPS in this workload; M2 performance remains unmeasured.
[Heap budget experiment](docs/heap-budget.md): three pairs with 3 GiB versus
1.5 GiB maximum heap show variable RAM savings and GC/frame-time tradeoffs;
playable defaults remain unchanged.
[Matched graphics API comparison](docs/graphics-api-comparison.md): Vulkan adds
about 1.5% average FPS but about 12% CPU in three Linux software-rendered pairs;
backend defaults remain unchanged and physical Mac performance is unmeasured.
The optional `queue-wait` pack yields during pending distant-terrain debounce retries; `adaptive-queue` combines it with the existing controllers. [Queue experiment and measurement limits](docs/debounce-queue.md).
Version 0.16 adds a separate [terrain position arithmetic experiment](docs/terrain-position.md)
for the sampled full-data lookup. It retains the pinned library’s unusual
negative-coordinate rounding and invalid-level errors. Balanced keeps it off;
whole-game and Mac performance still require measurement.
Version 0.15 adds an isolated [culling ray scratch experiment](docs/culling-rays.md)
that reuses two numeric vectors within each culling instance while retaining
upstream arithmetic and decisions. Balanced keeps it off; hardware gains remain
unmeasured.
Version 0.14 adds an isolated [empty culling whitelist experiment](docs/culling-empty.md)
that avoids allocating iterators while there are no dynamic handlers. Live
registration and nonempty callbacks retain upstream behavior. Balanced keeps it
off; whole-game gains still need hardware comparisons. DH profiles also request
a scoped [interruptible blending-read wait](docs/terrain-read-wait.md) to let
background workers leave a pending metadata read during shutdown.
Version 0.13 adds an isolated [tick membership experiment](docs/tick-membership.md)
that reuses compact storage for DH's private per-tick membership checks. The
combined foreground profiles include it too; balanced keeps it off. The applied
holder test verifies action semantics and removes repeated entry allocations;
whole-game FPS and RAM gains still need hardware measurements.
Version 0.12 captures effective distant-terrain quality, radius, worker budgets,
Iris state and field of view. Start/end snapshots flag changed visual settings;
[measurement details](docs/terrain-measurements.md) explain their limits.
Version 0.11 adds combined foreground-first OpenGL/Vulkan profiles with a 120 FPS
adaptive goal, plus explicit collector experiments and GC measurement guidance.
See [combined controls](docs/foreground-first.md), [Vulkan on your Mac](docs/vulkan.md)
and [collector comparisons](docs/collectors.md). The frame goal is not a rendering cap.
Version 0.10 adds an isolated [rejected-dispatch retry experiment](docs/dispatch-retry.md).
It bounds retry passes when a DH worker rejects queued work, preserving original
requeue handling and allowing other executors to progress. Balanced keeps it off.
Version 0.9 also makes the queue's idle wait wakeable for incoming work, preserving
its debounce rules and bounded cache-maintenance cadence. It remains part of the
optional queue profiles, with separate wake-latency measurements.
Version 0.8 fixes a fresh-instance graphics preset that could override the selected
render distance. It also records actual GC/header settings and supports an optional
[compact object headers experiment](docs/compact-headers.md) for Java 25; packs
do not enable that JVM flag automatically.
## Measure on your hardware
The optional [adaptive terrain experiment](docs/adaptive-terrain.md) throttles distant
terrain background work when frames slow down or heap pressure rises, keeping the
balanced profile's view distances and simulation settings. It remains opt-in until
real hardware demonstrates a benefit.
The additional [precise worker pacing experiment](docs/worker-pacing.md) makes short
terrain tasks respect their duty-cycle budget and avoids boxed worker timestamps.
It changes generation speed and CPU use, so it stays separate from balanced.
Press **F8** during gameplay to start a 120-second recording, or again to stop early. Rebind in Controls if needed. This also works with restricted chat accounts. Alternatively: `/horizon bench <label> 120`, `/horizon stop`, `/horizon status`.
Use separate identical world copies and the same camera route, resolution, render/simulation distance, heap, VSync and FPS limit. Warm up, then alternate at least three baseline/optimized runs. Test fresh chunk exploration separately. For everyday play cap FPS or enable VSync to reduce power usage.
The instance's `horizon-benchmarks/` folder receives JSON/CSV frame and integrated-server tick timings: average FPS, slowest-1%-average FPS, p95/p99 durations, stalls, JVM heap/GC, sampled process RSS when available, GPU/backend/driver and render settings. Frames include presentation/limiting waits and exclude menus/pauses/unfocused time. Tick `meanRate` is execution capacity, **not TPS**; observed tick counts are separate. Version 0.20 [captures sampled tick-manager state](docs/tick-state.md), including restored freeze/step/sprint/rate changes; this does not measure completed world work. Resident process memory is sampled on a shared background thread only during recordings; it includes native resident pages, while heap measures JVM objects. Neither is a complete measurement of unified GPU memory. [Memory measurement details](docs/process-memory.md).
Open `Compare-Benchmarks.html` from the starter ZIP or [the source file](tools/Compare-Benchmarks.html) in your browser to compare without Python. Select each JSON and optionally its matching CSV. It exports summary JSON, flags settings and duration differences, and charts frames by recorded active time or ticks by sample number. It is self-contained and makes no network requests.
Version 0.18 adds an isolated [terrain quad reuse experiment](docs/quad-reuse.md).
It reuses a temporary quad discarded by successful premerge when the existing
pool would otherwise allocate. Generated geometry and thread ownership checks
pass; allocation results are not whole-game RAM or FPS evidence. It remains off
in combined profiles.
Version 0.17 also records loaded mod versions and the Java vendor, VM and runtime
build in client and server reports. Both comparison tools flag software changes
as explicit experiment variables; older reports remain usable with an unknown
software warning. The viewer can expand the complete loaded mod list.
records the metadata and file-count validation; its short capture is not a
performance comparison.
The comparison flags captured setting differences, but cannot verify route, world, shader state or cache warm-up. See [validation evidence](docs/playable-validation.md). Software-renderer test FPS is not evidence of a Mac/GPU speedup.
For CPU/allocation investigation, [capture and stream a private JFR summary](docs/profiling.md).
It distinguishes directly observed allocation counters from sampled hot methods;
profiling runs remain separate from normal timing comparisons.
## Build and run
Requires Python 3.10+ and JDK 25. No Gradle, Rust or native compilation is required for playable packs.
```sh
python3 tools/build_mod.py
python3 tools/build_release.py
```
Gitignored `dist/` contains twenty packs, our original instrumentation JAR, client/server starter ZIPs and SHA256SUMS. [mods.lock.json](packs/mods.lock.json) pins mod versions, exact dependency closure, SHA-1/SHA-512 and source/license; [fabric.lock.json](packs/fabric.lock.json) pins loader/library SHA-256. Packs use the [Modrinth format](https://support.modrinth.com/en/articles/8802351-modrinth-modpack-format-mrpack). They embed only original project code; third-party mods, game and assets download from their publishers.
The server starter uses Lithium/FerriteCore and `/horizonserver bench <label> <seconds>`. [Server instructions](docs/server-quickstart.txt) cover setup and a baseline. Files are prepared before you accept Minecraft's EULA locally. A 50-active-player workload remains to be measured.
Local smoke testing uses the game's built-in limited demo; normal play uses Prism's licensed account authentication:
vscreen run python3 tools/runtime.py demo --instance .local/runtime/m2-balanced
```
The standalone demo launcher applies the pinned official manifest's conditional
JVM flags, including macOS `-XstartOnFirstThread` and the Windows heap-dump-path
option. Prism applies its own official manifest. Server JVM policy is separate;
this launcher correction is not a measured graphics performance improvement.
## Local game inspection
Game JARs, decompiled Java, libraries, inventories, worlds and logs live **inside this checkout** under gitignored **`.local/`**. No game code is in Git history or release downloads. Fresh clones regenerate the inspection workspace from official downloads; Microsoft credentials are not needed for this step.
`research-lock.json` pins the official manifest and Vineflower. The inner server JAR is extracted and checked; client/server libraries provide decompiler context. Output: `.local/minecraft/26.3/sources/{client,server}/` and coverage/inventory reports. Current inventories cover 11,383 client classes and 7,762 server classes, producing 7,301/5,037 Java files. See [decompilation status](docs/decompilation-status.md).
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a self-hosted runner holding the maintainer's own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
Class-file SourceFile metadata covers inner classes, which often share a source file. Coverage does not prove semantic equivalence or recompilability; original bytecode remains the reference. Completed source directories are not overwritten automatically. Older obfuscated releases require a separate remapping stage.
## Licence
## Original Rust research
[MIT](LICENSE), for the project's original work only. Dependencies keep their own licences. Minecraft is a trademark of Mojang Synergies AB; Minecraft code and assets belong to Mojang and Microsoft and are covered by the [Minecraft EULA](https://www.minecraft.net/en-us/eula). Decompiled game source and prepared resources stay on your machine and are never published.
The dependency-free `horizon-core` crate starts the Rust simulation foundation:
integer block/chunk/section positions, legacy and xoroshiro seeded random streams and positional factories,
padded palette-index storage, scheduled block/fluid tick queues, and double-precision
bounding-box and voxel-grid collision geometry. Noise samplers, octave stacks and
bulk density kernels provide the next world-generation foundation. Compiled
density samplers add gradients, shifted and blended terrain noise, distance
metrics and arithmetic evaluation.
Differential runners compare selected
operations with the checksum-pinned vanilla client and server bytecode.
[Implemented scope, validation, and remaining rewrite work](docs/rust-parity.md).
Third-party credits and licenses: [Third-party notices](THIRD_PARTY_NOTICES.md).
```sh
python3 tools/test_rust_parity.py --prepare
python3 tools/test_world_parity.py
python3 tools/test_tick_parity.py
python3 tools/test_geometry_parity.py
python3 tools/test_collision_parity.py
python3 tools/test_noise_parity.py
python3 tools/test_density_parity.py
```
## Author
The core corpus checks 1,634,520 output lines per reference JAR, including
signed boundaries, RNG rejection/state consumption and raw storage words.
Passing it does not establish full gameplay parity or an FPS improvement.
The separate [noise suite](docs/noise-generation.md) checks 26.3 Perlin/simplex,
inside hits, and cube-versus-array cell lookup. A cached collision registry connects
imported shapes to resident block sections while checking exact state identities.
Movement helpers resolve supplied colliders and step-up candidates, including
landing adjustment and the second collider query. Movement-path block visits
preserve vanilla ordering, step indices and packed-position deduplication.
Uncached states require block behavior; the entity-context overload also needs
separate implementation. [Geometry and collision scope](docs/collision-geometry.md).
```sh
cargo test --workspace
cargo run --release --bin voxel-bench -- 100 > .local/voxel-benchmark.csv
```
The dependency-free occupancy experiment compares scalar and bitset exposed-face counting for synthetic 16³ sections. It has no real mesh, material/AO/light work, neighboring-section context, JNI bridge or game integration. Speedups are kernel-only, not Minecraft FPS. [Research plan](docs/optimization-plan.md) describes profiling, improved Java baselines, native integration and behavior checks before expanding scope.
Public branding and new mod downloads use **Horizon**. The existing Fabric mod
ID and `dev.horizonlab` namespace remain stable for installed instances and
measurement compatibility. The installer backs up both old `horizon-lab-*.jar`
and new `horizon-*.jar` versions before installing an update.
CI checks offline tools/Rust and compiles the original mod/builds packs on Linux and Apple Silicon macOS. Game dependencies download to ephemeral `.local/`; CI uploads only our original JAR and packs. Compilation is not Mac GPU gameplay testing. Publication checks allow Java only under our original `dev/horizonlab` namespace; review diffs as well.
[MIT](LICENSE) covers only our original tracked work. Dependencies retain their licenses. Mojang [removed obfuscation](https://www.minecraft.net/en-us/article/removing-obfuscation-in-java-edition) without changing [Minecraft's EULA](https://www.minecraft.net/en-us/eula). Decompiled game source remains local; published code implements independent tooling and instrumentation.
Integrated-server reports record the shared graphics backend at recording start; see [the context and comparison limits](docs/server-graphics-context.md).
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
"Separate private copies, baseline then core. JFR begins about 121 seconds after demo dismissal; a separate 120-second frame capture begins about 307 seconds after dismissal.",
"Both captures have zero menu/pause/focus exclusions and no drops. Actual CSV counts and means match; viewer charts, 390-pixel layout and explicit software-change warnings pass.",
"Core records about 20.7% higher average FPS and 22.2% lower sampled peak RSS in this pair; whole-process CPU is slightly higher while rendering more frames.",
"No mod is isolated by this group comparison. Rendering-work completion, entity activity, cache/JIT and thermal state remain variables. Matching captured configuration is not pixel equivalence or exhaustive vanilla gameplay proof.",
"Tick execution inverse meanRate is capacity, not actual TPS. Record counts/elapsed seconds are exposed separately and include any pauses. Both captures have about 20 tick samples per elapsed second.",
"Preceding JFR server/render allocation counters are lower with core, but total and longest measured GC pauses are higher. Those profiles precede the frame captures and are not simultaneous with them.",
"No hardware GPU or M2/Apple Silicon result; RSS is not complete unified/GPU memory. Repeat alternating trials on real hardware.",
"No private worlds, full game logs, JFR recordings, game binaries or fixture agents are included."
"Fixed-order single pair using separate copies of the same private saved world and stationary view, with approximately three-minute warm-up before clean frame recordings.",
"One earlier candidate frame capture contained pause exclusions and was discarded; its separate preceding allocation recording had already completed.",
"Allocation counters have their own roughly 64-second observation spans; recordings started approximately 36-42 seconds after joining. Profiling overhead and JIT/cache effects are not eliminated.",
"Both worlds saved and exited with no crash report or remaining game process. The outer foreground control launcher returned 143; it did not expose the game exit status.",
"No useful allocation reduction or whole-game speedup is established. Not a Mac, retained-memory or GPU performance benchmark."
"Separate copies of the same private world; candidate then control. JFR begins about121seconds after demo dismissal; frame capture begins about307seconds after dismissal.",
"JFR allocation counters span about64seconds; pause measurements are from the preceding JFR recordings, not the later frame capture.",
"Recorded overworld queue allocation counter rate is about15.3MB/s upstream versus11.5KB/s with waiting. Task-completion and generated geometry volumes are not controlled quantitatively.",
"Control has230frames versus225candidate; sampled peak RSS is lower in control, and later whole-process CPU differences are small. No game FPS, retained-memory or whole-process CPU improvement is established.",
"Daylight advances normally; lighting, entity activity and cache/JIT differences are not fully controlled. No Apple Silicon or hardware-GPU result.",
"Tick meanRate is inverse execution duration, not observed TPS. Both recordings contain2401tick execution samples over about120seconds.",
"Actual JSON/CSV viewer comparisons and390pxlayout pass. The real discarded capture with zero eligible frames and209exclusions is rejected by the viewer and excluded from performance comparisons.",
"No private worlds, full game logs or JFR recordings are published."
"Apple Paravirtual device and MoltenVK on a hosted ARM Mac; not physical M2 Pro GPU testing.",
"Library/device creation and readback request were observed, but no completed nonuniform readback report was produced.",
"All failures retained; neither successful rendering nor a screenshot-specific root cause is established.",
"Baseline includes the same original measurement mod and private test fixture; no core optimization mods or distant terrain.",
"Full logs, screenshots, game binaries and assets remain private.",
"Later selected stack locations place the observed failures in the screenshot callback; the independently logged Vulkan identifier is not established as their cause.",
"Actual assertions rejected uniform callback images in all three cases. Overlay state was not recorded, and displayed-window correctness is not established.",
"After guarding overlays, core/distant-terrain title readbacks pass but vanilla remains uniform; the aggregate research run fails. No world-rendering or physical-M2 claim.",
"Later world probes pass all three Vulkan cases on the hosted Mac; failed title cases remain retained. These are short startup/readback/save probes, not physical-GPU performance tests."
"Fixed-order candidate then control single pair from separate copies of the same private world. Both stationary captures start approximately 307 seconds after the demo popup is dismissed; first saved cameras match.",
"Both reports have zero menu/pause/focus exclusions, no dropped samples and matching captured settings apart from the isolated quad hook. CSV counts and actual offline viewer timelines pass.",
"The preceding 60-second JFR recordings begin approximately 121 seconds after dismissal. Allocation counters have their own approximately 64-second spans; GC pause data is from those recordings, not from the later frame captures.",
"Render-loader allocation counter rates are higher in this candidate observation. Task-completion/geometry volumes and cache/JIT effects are not controlled quantitatively. No useful live allocation improvement is established.",
"Frame count and sampled peak RSS differences in this single software-rendered pair do not establish a meaningful game speed or retained-memory gain. RSS is not complete GPU/unified RAM. Not an Apple Silicon benchmark.",
"Candidate saved, reloaded, saved again and exited with launcher code zero. Control saved, closed all terrain databases and exited with launcher code zero.",
"No private worlds, full game logs or JFR recordings are included in these selected observations."
"fixture_protocol":"55,296 generated faces plus four concurrent builders, geometry/list boundaries checked before and after upstream merge; warm 2,000 batches, then 500 warm batches and 2,000 measured batches per sequential observation, 512 faces/batch"
"scope":"Actual client FaceBakery geometry overload with a real unit-atlas sprite and default JOML math options. Identity blockstate transform, all bundled cuboid geometry and synthetic rotations, rescaling, default/explicit UVs, winding, degenerate and reversed bounds, inherited models and missing materials. Positions, UVs and element matrix coefficients compare by float bits. This does not cover blockstate transforms/UV lock, atlas packing, transparency classification, lighting, GPU rendering or gameplay."
"scope":"Actual pinned CuboidModel.fromStream parser compared over every bundled block model and synthetic JSON/numeric/syntax cases. Decoded geometry, texture materials/references, nullable children, inheritance markers, face quadrants, light emission and display transforms; floats compare by IEEE-754 bits. Rejections normalize exception text. This client-only API is absent from the server. Does not yet validate parent resolution, rotation matrices, quad baking, blockstate dispatch, rendered pixels or gameplay."
"scope":"Compiled scalar UnaryFunction.LogSampler compared by float result bits across the recorded input bit ranges, including signed zeros, subnormals, finite values, infinities and NaNs. NaN results are canonicalized as Float.floatToIntBits. Exhaustive mode checks all 2^32 float input bit patterns against both checksum-pinned JARs on this machine's JDK. Bulk composition, range propagation, compiler rewriting and other operations belong to the separate density suite. This does not establish double-precision Math.log parity, other-platform math parity, complete terrain, gameplay or performance."
"scope":"Actual CollisionContext factories and EntityCollisionContext/Empty/PositionCollisionContext queries on controlled player and marker snapshots. Checks entity identity, captured descending and placement flags, fluid collision flags, empty-stack AIR identity, height thresholds including f32 epsilon, null factories, callback arguments/order/failures and all five fluid type pairs. Entity lifecycle, inventory mutations after capture, experimental minecart contexts, contextual block shape dispatch and live movement integration remain pending."
"scope":"Actual CollisionContext.getCollisionShape dispatch and native scaffolding behavior for all 32 scaffolding states, nine context variants, descending flags, integer Y extremes, nonfinite positions and adjacent double values around the top/bottom float-epsilon thresholds. Full shape coordinates, extrema, box ordering and cube metadata match. A rejecting world proxy verifies selection performs no world reads. Scaffolding placement/update/falling behavior and live movement remain pending."
"scope":"Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim."
"scope":"Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim."
"scope":"Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim."
"scope":"Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim."
"scope":"Actual CollisionContext factories and EntityCollisionContext/Empty/PositionCollisionContext queries on controlled player and marker snapshots. Checks entity identity, captured descending and placement flags, fluid collision flags, empty-stack AIR identity, height thresholds including f32 epsilon, null factories, callback arguments/order/failures and all five fluid type pairs. Entity lifecycle, inventory mutations after capture, experimental minecart contexts, contextual block shape dispatch and live movement integration remain pending."
"scope":"Native contextual LiquidBlock collision dispatch for all 32 water/lava states. Checks the always-fluid bypass, source-only selection, living-entity gating, supplied entity standing shapes, exact height boundaries, all 33 above-block fluid states, callback order and failure propagation, null/placement contexts and wrapping above-block coordinates. Full selected shape data and callback traces are compared. Entity-specific standing shape implementations, liquid simulation and live movement remain pending."
"scope":"Actual CollisionContext.getCollisionShape dispatch and native scaffolding behavior for all 32 scaffolding states, nine context variants, descending flags, integer Y extremes, nonfinite positions and adjacent double values around the top/bottom float-epsilon thresholds. Full shape coordinates, extrema, box ordering and cube metadata match. A rejecting world proxy verifies selection performs no world reads. Scaffolding placement/update/falling behavior and live movement remain pending."
"scope":"Actual CollisionContext factories and EntityCollisionContext/Empty/PositionCollisionContext queries on controlled player and marker snapshots. Checks entity identity, captured descending and placement flags, fluid collision flags, empty-stack AIR identity, height thresholds including f32 epsilon, null factories, callback arguments/order/failures and all five fluid type pairs. Entity lifecycle, inventory mutations after capture, experimental minecart contexts, contextual block shape dispatch and live movement integration remain pending."
"scope":"Direct normal-noise JSON decoding using the actual pinned vanilla CODEC. Every bundled noise registry resource, explicit/default parameters, empty and full amplitude modifiers, normalization modes, inclusive field bounds with signed-zero comparator ordering, malformed types, null fields, duplicate and ignored keys, decimal/exponent integer truncation and wrapping, signed zero, seeded modern/legacy and Nether construction, octave metadata, range and normalization bits, scalar noise values and parent random state. JSON resource contents stay private. Strict JSON parsing; no claim for Gson's permissive non-JSON syntax. Bundled density registries are also decoded with actual vanilla registry-aware CODECs: named forward references, all bundled function kinds, declared range/axes, raw compilation errors, seeded optimized roots, cache allocation, scalar values, strided bulk values, repeated cache hits and surface-volume errors are compared using RandomState's actual compile context. All seven bundled noise-setting resources contribute their actual eight-field NoiseRouter.CODEC objects to RandomState. Router fields are compiled in declaration order with a shared compiler/context; cache allocation, raw/optimized scalar and repeated strided volume values, successful single-layer surface volumes and invalid surface-volume shapes are compared. Actual RandomState climate sampler binding and quart-to-block coordinate wrapping are checked at ordinary and extreme positions. Float-to-long climate quantization and long-to-float conversion include subnormals, signed zeros, NaNs, infinities, cast saturation and seeded random bit patterns. Actual vanilla Climate.Parameter and ParameterPoint JSON codecs compare scalar, two-element list and min/max object intervals, float endpoint bounds/ordering, required coordinate/offset fields, negative zero, precision-sensitive float parsing, malformed types and duplicate keys. Climate parameter distances, wrapping squared fitness, stable search-tree leaf order and nearest lookup are compared across every supported branching value 2..32, sizes around branch powers, the 65536-entry API bound, duplicates, warm ties, fresh lookups, brute-force lookup and extreme signed-long endpoints/targets. The Rust tree is immutable with per-worker search state instead of hidden thread locals; invalid or degenerate searches return typed errors. Inline arbitrary distance metrics and behavior outside the bounded API are not covered. Built-in Overworld and Nether quantized biome parameter tables are exported independently by each authenticated JAR and required to agree before Rust consumes them. The Rust multi-noise resolver and actual vanilla MultiNoiseBiomeSource compare biome identities through scalar climate queries and prepared bulk quart volumes, including forward/reverse/repeated query order, wrapped extreme origins and shared previous-result state. Biome holder keys in the reference are registration placeholders; no biome property or gameplay behavior is evaluated. The Rust loader reads a Horizon quantized table format, not the vanilla biome codec, and the preset builders themselves have not been ported. Source and private fixture mutations during validation invalidate the report. The two-field 26.3 NoiseSettings codec, create helper and LevelHeightAccessor clamping are also compared across bundled noise members, dimension/alignment bounds, zero/negative runtime heights, disjoint ranges, wrapped integer arithmetic and decimal/exponent integer conversion. Global fluid cutoff selection and the actual disabled Aquifer implementation are compared with original default block-state identities, extreme sea levels/coordinates, signed-zero/subnormal/nonfinite density and counted picker calls; developer fluid-disable behavior is outside the regular-mode corpus. The actual noise-based Aquifer.Config codec and runtime are compared across bundled aquifer settings, modern/legacy world-seeded aquifer factories, synthetic density roots, nondefault water/lava states, cave/void air, nonfinite sampler overrides, strided volume extents, wrapped coordinates, repeated and reversed queries, counted picker calls, sampler call order/values, injected failures and retry state, scheduling flags, populated location/status caches and sorted surface-cache entries. Oversized or wrapped-invalid grid allocations and developer fluid-disable behavior are outside the bounded regular-mode API. Terrain filling is not yet validated. Router, vertical noise and optional aquifer members are decoded from each noise-setting resource; complete NoiseGeneratorSettings, material rules, nonempty world fields, biome registry/codecs and complete chunk biome assignment, generated chunks, gameplay and rendering remain pending. Inline noise holders and general codec behavior beyond the covered fixtures remain pending."
"Linux llvmpipe software renderer; no physical M2 shader performance or native GPU test.",
"OpenGL requires actual pinned shader activation and matching runtime pack identity, with nonuniform readback, focused gameplay, saved data and clean exit.",
"Linux Vulkan plus DH fails before readback; selected stack locations alone do not establish the root cause.",
"Removing DH privately produces an ordinary Vulkan world readback but Iris reports shaders inactive; the required-activation assertion rejects it.",
"Both hosted Mac cases hit the 240-second private fixture timeout; earlier diagnostics do not retain stages or waiting state. They do not establish shader compatibility or incompatibility.",
"Full logs, raw PNGs, world data, assets and shader ZIPs remain private.",
"Diagnostic repeat: OpenGL loads a world but retains a screen before readback; Vulkan initializes its device but records no waiting-state snapshot. Both time out; screen identity and root causes are unknown."
"experiment_note":"Experimental projection variant (reused sounds, Arc-keyed string pools and direct flags); reverted after mixed CPU/RSS evidence. Current in this file is not the final PR version."
"current":"release example built at cf837118 + current uncommitted delta in PR #607 worktree",
"historical":"release example built at exact cf837118 commit, with same Cargo release profile and shared dependency target"
},
"mode_commits":{
"baseline":"independent loader algorithm at e422bd13, executed in current binary",
"pr607":"shared early construction algorithm at a01d47f2, executed in current binary",
"cf837118":"exact historical binary at cf837118, deferred mode",
"current":"current PR worktree, deferred mode with interned strings"
},
"limitations":"Loader-only (Assets + BlockFacts + Behaviours), no full client startup or first-frame timing. Same release profile but different compilation contexts for historical binary; hardware activity and cache warmth vary. CPU and elapsed differences between cf837118 and current are mixed and may be noise. Peak RSS is whole-process Linux ru_maxrss."
"measurement":"same optimized binary, isolated processes in alternating forward/reverse four-mode runs",
"limitations":"Includes Assets::load, BlockFacts, Behaviours; excludes complete client startup, pack/mod root resolution, graphics and first frame. Different modes emulate the original independent, PR #607 at a01d47f2, paired and deferred startup sequences in the same release build. Machine was not idle. Elapsed times show high variance; no statistically established gain between PR, paired and deferred modes."
"experiment_note":"Exploratory variants (Arc-key pool, reused sounds, combined proposal) alongside cd61e11b; none gave a robust consistent improvement and all were reverted."
"Actual frame and integrated-server tick reports contain identical Java identity and 59 loaded mod versions, captured before each recording starts.",
"The short 15-second recording validates metadata and file output only; it is not a valid performance comparison or a Mac result.",
"The actual old/new report pair loads in the offline viewer with matching CSV timelines, an explicit Horizon mod-version difference and unknown legacy Java implementation warnings.",
"Synthetic changed versions, reordered mod lists, missing legacy identity, escaped mod names and expanded lists at 390px pass offline checks."
"Fixed-order candidate then control single pair, separate copies of the same private world; approximately 307 seconds from demo-popup dismissal to capture for each. Initial player camera and both first saved camera positions matched.",
"Stationary view includes LOD terrain; both recordings have zero paused/unfocused exclusions and matching captured settings apart from the isolated hook. No statistical speed estimate is established.",
"Preceding 60-second JFR captures ran separately before the frame recording; JIT/cache/profiling effects are not eliminated.",
"Candidate saved, reloaded, saved again and quit; control saved and quit. Both foreground launchers returned 0, with no crash reports or remaining game processes.",
"Scene differs from earlier release captures, so comparisons across releases are not valid. Private test-world camera preparation is not included in the mod or release packs.",
"No FPS improvement is established. Software rendering dominates this machine; these are functional observations, not Mac, GPU or unified-memory performance results. RSS is process resident memory, not complete GPU/unified RAM."
Loaded 100 of 1693 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.