Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
49 KiB
Horizon native client
horizon-client is the playable Rust client for Minecraft Java Edition 26.3.
It streams a world around the player on worker threads, renders every vanilla
block model with smooth lighting and biome tints, and runs the validated player
physics, health and hunger at 20 ticks per second. You can mine with vanilla
hardness/tool rules, collect drops from the block loot tables, place blocks,
craft in the 2x2 inventory grid or on a crafting table, and save and reload worlds
in vanilla's Anvil format.
Status: playable; players get it through the launcher. The Java parity table in the README scores each subsystem against vanilla and lists what is still missing; Rust rewrite status has the details.
Run from a checkout
Use current stable Rust (1.90+) and JDK 25 (Python 3.10+ only for the development
harnesses). Horizon never
downloads the game: resource preparation imports your own copy (the 26.3
client from your official launcher install, and the 26.3 server jar you
downloaded yourself; see Build from source),
verifies it against the pinned checksums, runs vanilla's data generator and small
exporters against those jars, and extracts the jar's assets and data into ignored
.local/horizon-client/26.3/. Sounds (about 380 MB) are copied from your
launcher's asset store by horizon-prepare sounds, each file checked against the asset
index's SHA-1. Preparation also tries to replace the JAR's gray 1×1 title panorama
placeholders with the full textures from that asset store. If the store is missing or
incomplete, preparation logs a warning and keeps the placeholders. To import the panorama
later without rerunning the Java exporters, run horizon-prepare panorama (or
cargo run --release -p horizon-prepare -- panorama in a checkout). Pass
--minecraft-dir PATH when using a different Minecraft installation.
Prepared resources are private; never commit them.
Preparation is horizon-prepare (crates/horizon-prepare, Rust): it needs no Python, only
the JDK, for the small exporters that call Mojang's own code. Release folders ship it beside
horizon-client, and the launcher prepares with it and copies the sounds as part of preparing.
cargo run --release -p horizon-prepare -- --prepare --server-jar ~/Downloads/server.jar
cargo run --release -p horizon-prepare -- sounds
cargo run --release -p horizon-client -- --render-distance 12
Development builds that link quickly: cargo build --profile fast -p horizon-client.
| Option | Meaning |
|---|---|
--world NAME |
World folder under saves/ (default "New World"); reopened if it exists |
--seed N |
Seed for a new world (numbers or text, like vanilla) |
--creative |
Creative mode (instant breaking, pick block) |
--render-distance N |
Chunks, 2..64 (default 12) |
--resources PATH |
Prepared resource directory |
| Control | Action |
|---|---|
| Mouse | Look (vanilla default sensitivity) |
| WASD / Space / Left Shift / Left Ctrl | Move, jump, sneak, sprint |
| Left / right click | Break (hold) / place or use; right click opens crafting tables |
| Middle click | Pick block (creative) |
| 1–9, scroll | Select hotbar slot |
| E | Inventory and 2x2 crafting (shift-click to quick-move or craft all) |
| Q / Ctrl+Q | Drop one / drop stack |
| F3 | Debug overlay (position, chunk, light, biome, FPS) |
| Esc | Pause menu (Save and Quit) |
Worlds autosave every 6000 ticks (5 minutes), when chunks unload and on quit.
On headless Linux use vscreen start ./target/release/horizon-client and
vscreen shot.
Sound: the game prints the output it opened (Horizon: audio output ...) or
why it has none (Horizon: no sound: ...). It opens none with
HORIZON_NO_AUDIO set, nor on a vscreen, where tests run beside someone
listening; HORIZON_AUDIO=1 opens it there anyway, so send it to a null sink
(PIPEWIRE_PROPS='{ target.object=<sink> node.dont-fallback=true }'). Don't
mute a test run's stream in the mixer: PipeWire remembers the mute for every
later horizon-client. The launcher starts the game as "Horizon" for that
reason.
Agent control (horizon-ctl and MCP)
AI agents and scripts play Horizon through horizon-ctl, a command line that
reads the game as JSON and acts like a player, and horizon-ctl mcp, an MCP
server with the same commands. --control makes the game accept them on a
local socket; under control, losing window focus does not pause the game or
release held keys, so a script can play while you use other windows.
cargo build --release -p horizon-client --bins
C=target/release/horizon-ctl
$C launch --world Survival --seed 5 --wait # start the game, open or create the world, wait for it
$C state # player, world, screen, crosshair target
$C find-block birch_log # the 16 nearest loaded logs
$C goto -0.5 65 -2.5 --near 2.5 # walk there along a path
$C break -1 65 -3 # mine it: {"drops": {"minecraft:birch_log": 1}, ...}
$C inventory # hotbar, inventory, armor, offhand, counts
$C open-inventory # craft planks in the 2x2 grid:
$C click-slot 0; $C click-slot 100 right; $C click-slot 0; $C click-slot 200 shift; $C close
$C place 2 65 -2 --item birch_planks # {"changed": true, "block": {...}}
$C pause; $C ui # the pause menu's widgets: ids, kinds, labels, bounds
$C click options; $C set fov 0.75; $C click done # widgets by id
$C screenshot /tmp/view.png # PNG of the next presented frame
$C quit # save and exit
horizon-ctl help lists the commands by group and horizon-ctl COMMAND --help
explains one with examples; horizon-ctl help --game lists what the running
game accepts, test hooks included.
- Observe:
state(position, rotation, velocity, health, food, saturation, XP, effects, game mode, dimension, time, weather, the screen and what the crosshair points at),looking-at,entities [RADIUS] [TYPE](mobs with ids and health, dropped items, orbs),block X Y Z,blocks X1 Y1 Z1 X2 Y2 Z2,light X1 Y1 Z1 X2 Y2 Z2,find-block BLOCK [RADIUS],inventory(stacks with their components as SNBT),screen(the open screen or title menu, its buttons and slots) andmessages(chat, title, subtitle and action bar). - Move and act:
move DIR SECONDS,jump,goto X Y Z(A* over standable blocks: steps up one block, drops up to three, avoids lava),look,look-ata point, block face or entity,attack [ID],break X Y Z,place X Y Z [--item ITEM],interact X Y Z(doors, chests, crafting tables),use-item [--hold S](eat, drink, throw),select SLOT|ITEM,drop. - Widgets:
uilists every widget of the open screen (title menus, pause, options pages, inventories and menus): an id, its kind (button,sliderwith its 0..1value,field,row,tabwithselected,slotwith its stack), the label and its bounds. Ids come from labels (FOV: 70isfov,Video Settings...isvideo-settings, a world row is the world's name; a repeat gets-2), elserow-0,field-0, and slots areslot-ID.click ID [right] [shift]clicks one as the mouse would andset ID VALUEmoves a slider to 0..1; after a click, the nextui,clickorsetwaits for the screen to be redrawn. Design and plans: agent-control.md. - Options:
options-get [KEY...]gives everyoptions.txtoption by its vanilla key with its value and what it accepts ({"type": "int", "min": 30, "max": 110, "value": 70}; enums withchoices, key bindings askey.keyboard.w);options-set KEY VALUEsets one, applies it at once and savesoptions.txtas the options screen does. A value the option refuses is an error saying what it accepts, not vanilla's silent reset. - Waits and events:
wait-for screen NAME|widget ID|loaded|chat TEXT|event KIND [--timeout S] [--since SEQ]replies once the game gets there (a timeout is an error with"kind": "timeout"), so scripts don't poll.events [--since SEQ]lists what happened, oldest first, each with aseqand thetick:screen(from, to),chat(text),death,respawn,item(item, delta),dimension,world(opened, closed). Takelastfromeventsbefore an action whose event you thenwait-for --sinceit, so a fast one isn't missed. - Screens:
open-inventory,click-slot SLOT [right] [shift](0-8 hotbar, 9-35 inventory, 100+ crafting grid, 200 result, 300+ containers, 500-503 armor, 504 offhand, 600+ stations),close,click-button LABEL(any button of any screen or title menu, by its label),click-at X Y,type TEXT(on the create-world screen--field name|seed --replace),chat,command,respawn,pause,resume. - World:
tp,give,time,setblock,summon,wait TICKS, and the benchmarking commandsbench,statsandfly(below).
Actions block until they finish or time out and reply with the outcome: goto
says whether it arrived, break what dropped and what was picked up, place
the block placed. stop cancels the running action. Every reply is one JSON
line; errors are {"error": "..."} objects, and the exit status is 0 on
success, 1 when the game replied with an error, 2 for a usage error and 3 when
the game cannot be reached or started. horizon-ctl script FILE (or stdin)
runs one command per line with shell-like quoting and stops at the first error
(--keep-going doesn't); sleep S waits. horizon-ctl - is the older form
that reads stdin and keeps going.
launch finds the game to start: --game PATH, else a horizon-client next
to horizon-ctl (a build folder or a release folder), else the newest version
the launcher installed (--version TAG picks another), run with its prepared
resources from the game home, where the saves are. So after installing with
the launcher, horizon-ctl launch --world X --wait is all an agent needs.
Other arguments go to the game; its output goes to a .log file beside the
socket.
Logs and crash reports. Like vanilla, the game writes logs/latest.log
in the game home ($HORIZON_HOME, else ~/.local/share/horizon; the
dedicated server in its own folder) with lines like
[12:04:31] [Render thread/INFO]: message, and at start archives the last
run's as logs/<yyyy-mm-dd>-<n>.log.gz (seven a day). Messages still print on
stderr as before. A panic on the main thread or a fatal error writes
crash-reports/crash-<yyyy-mm-dd_HH.MM.SS>-client.txt (the error, its stack,
the machine, the GPU, render settings, resource packs, the open world and the
last 50 log lines) and the game exits with a non-zero code. horizon-ctl logs [--lines N] prints the end of latest.log and horizon-ctl logs --crash the
newest report (the logs MCP tool returns the same). F3 + C held for ten
seconds crashes the game on purpose, as in vanilla (the world is saved first);
development builds also crash with HORIZON_DEBUG_CRASH=SECONDS, that long
after the first frame.
MCP. horizon-ctl mcp serves the Model Context Protocol over stdin and
stdout (JSON-RPC 2.0, one message per line), with one tool per command and a
JSON schema for each; screenshot returns the frame as an image. Register it
in an MCP client as a stdio server with the command horizon-ctl and the
argument mcp (--socket PATH before mcp to pick a socket).
Transport. On Linux and macOS the endpoint is a Unix socket:
$HORIZON_CONTROL, else $XDG_RUNTIME_DIR/horizon-control.sock, or
--control-socket PATH (horizon-ctl --socket PATH). Windows has no Unix
sockets: there the game listens on a random 127.0.0.1 port and writes
{"transport":"tcp","port":N,"token":"..."} to the endpoint path
(%TEMP%\horizon-control.json by default), readable only by its user; the
client's first line must be auth TOKEN. Any endpoint path ending in .json
selects that transport on Linux and macOS too.
tools/agent_control_e2e.py checks it all on a virtual screen: it creates a
world through the title menus, walks to a tree, breaks a log, crafts planks in
the inventory screen and places one, through the CLI and through MCP.
fly X1 Y1 Z1 X2 Y2 Z2 SECONDS [PITCH] moves the camera along a line at
constant speed and replies with the same summary as bench (avg/1%-low FPS and
p50/p95/p99/max frame times), plus CPU and GPU time per frame; the graphics
benchmark is built on it. Frame times include waits for
presentation, so bench reports what is on screen.
Discord Rich Presence
While Discord runs on the same computer, the player's Discord profile shows
"Playing Horizon" and what they are doing: "In the menus", "Singleplayer",
"Multiplayer" or "Hosting a world" (with the party size, of 8), the dimension
("In the Nether") and the time since they entered the world. It never shows a
world's name or a server's address. Horizon Settings has the switch
(Discord Rich Presence, on by default; discordPresence in
horizon-extras.txt), and HORIZON_NO_DISCORD=1 turns it off for one run.
crates/horizon-client/src/discord_presence.rs speaks Discord's local IPC
itself (the discord-ipc-N socket or named pipe, no SDK) from a thread of its
own: the frame only hands it a status when it changes. It retries every 15 s
while Discord isn't running and stays within Discord's 5 updates per 20 s.
Dedicated servers never start it, and automated runs (offscreen, benchmarks)
only talk to a fake Discord: lab.game_env sets HORIZON_NO_DISCORD=1, and
tools/discord_presence_e2e.py points games at a fake server with
HORIZON_DISCORD_IPC_DIR (title, world, Nether and quit on both single-player
paths; a LAN host and guest; the switch; a Discord started after the game).
DISCORD_APP_ID is Discord's "Horizon" application, whose horizon image is
the large one; there are no game-mode images yet.
Play on another of your machines
Horizon targets Linux on x86-64 with an NVIDIA GPU (Vulkan); macOS and Windows are out of scope. To play on another Linux machine without Rust, JDK or prepared resources, package a release build where they exist:
cargo build --release -p horizon-client
python3 tools/package_native_client.py --out ~/builds/horizon
The archive holds the binary, the prepared resources and horizon.sh. Copy it
to the target machine (same OS and architecture), unpack it and run
horizon/horizon.sh; horizon.sh --install adds an application menu entry.
Saves and options.txt go to ~/.local/share/horizon (or $HORIZON_HOME), so
unpacking an update keeps them. The archive contains Mojang assets: keep it
private.
How it fits together
streaming.rs: worker pool that generates or loads, lights and meshes chunk columns closest-first; stale results are discarded by sequence number.light.rs: vanilla sky/block light rules (sky sources down to the first occluding edge,max(1, dampening)falloff) over a 3x3 chunk region.mesher.rs: blockstate models with neighbour culling,ModelBlockRenderersmooth lighting/ambient occlusion,BlockTintSourcestints with 5x5 biome blending, andFluidRendererwater/lava surfaces.gfx.rs+terrain.wgsl: camera-relative section rendering with frustum culling, the vanilla lightmap formula, timeline-driven sky/fog, and solid/cutout/translucent passes;entity.wgsldraws dropped items.environment.rs: the data-driven day timeline (sky colour, light factor, celestial angles) sampled with vanilla's keyframe/easing rules.items.rs,crafting.rs: item components from vanilla's reports, tool mining rules, loot tables and crafting recipes from the data pack.save.rs: Anvil region files andlevel.dat.
Validation
The client loads a separate, registry-checked collision-shape export alongside
render resources. SceneCollisions feeds its actual TerrainChunk instances
into registered shape selection, block scanning, world movement resolution and
occupancy checks. The scene has no other entities, block entities or border;
entity-dependent queries report an error until an entity runtime is supplied.
Camera collision uses a position context and disables stepping. The walking
binding uses the current pose bounds, descending input and registered step-height
attribute. Sneaking invokes ledge backoff before collision resolution. The
complete authoritative player tick remains unfinished.
cargo run --release -p horizon-prepare
python3 tools/test_native_scene_collision.py
The scene collision comparison checks 14,150 movement requests, each with before/after occupancy queries, against both pinned reference JARs. Native code independently builds the nine- and twenty-five-chunk scenes and all sixteen gallery placements. Movement components compare by exact double bits. Cases include partial blocks, pyramid ledges, ceilings, initial overlaps, negative and missing chunk boundaries, tiny and zero movement, and varied step heights. This verifies the scene adapter; it does not establish player locomotion or complete gameplay parity.
The movement binding stores a player's position, velocity, pose bounds,
ground/support state, collision flags, movement history and live registered
attributes. MotionWorld composes the existing air-travel, jumping, collision,
ground-support, restitution and block-coefficient code over the same scene.
The stored-motion comparison
checks 42,257 operations across 637 evolving scenarios against actual native
Player.travel, jumping and movement, using real native attributes and chunks.
It compares every resulting position, velocity and bounding-box component by
exact bits, along with support, climbing and collision state and both separate
velocity/position synchronization flags.
The dry movement tick additionally binds small-velocity cleanup, float input
damping, held-jump delay and server frost-modifier removal. The expanded
tick comparison
checks 47,393 operations in 847 scenarios against both pinned JARs, including
direct calls to the actual LivingEntity.aiStep body. It compares lazy attribute
instance creation, stored base/cache/dirty state, modifier values and permanence,
and pending update/sync membership alongside the motion fields. Queue iteration
order depends on runtime identity hashes and is outside this scene comparison.
The test caught an omission where matching positions concealed a retained
powder-snow modifier and missing attribute notifications; dry ticks now remove
that modifier after travel, as vanilla does.
The walking movement comparison
extends this to 65,893 operations in 947 scenarios, invoking actual
Player.aiStep with empty inventory/equipment and no nearby entities. It checks
head yaw, jump-trigger countdown and speed refresh after movement. Prepared
input is supplied at the native virtual input boundary. Separately, the
local input comparison
checks 13,988 cases against actual KeyboardInput.tick and
LocalPlayer.modifyInput, covering every key combination, diagonal adjustment,
item/riding/sneaking speed gates, signed zero and nonfinite values. This verifies
input arithmetic; the sprint and crouch integration checks below extend it.
The full LocalPlayer lifecycle remains unfinished.
The fixed-rate driver limits work per rendered frame while retaining overdue ticks. Clock tests cover varied frame schedules and long stalls. The development focus pause is explicit; no vanilla pause-menu or multiplayer timing is claimed. The Linux walking smoke check enabled F5, landed at ground height 4, walked, jumped, switched modes and exited with Ctrl+Q. Ground, moved and airborne screenshots were visually inspected; the run recorded 207 movement ticks and finished grounded without an error. The renderer startup check also passes after the movement integration.
cargo run --release -p horizon-prepare
python3 tools/test_native_scene_motion.py
python3 tools/test_local_input_parity.py
This binding is not yet a complete player tick or input controller. Its current
domain is the dry resident scene with standing/crouching/crawling dimensions,
zero frozen ticks, empty inventory/equipment, and no entity interpolation,
vehicles or status effects. Selected Player.aiStep phases are bound; the full tick and local input
lifecycle remain unfinished.
Landing now accumulates and resets the stored fall distance, clears climbing
state on ground, and invokes registered block fall/bounce reactions. The
composed landing comparison
checks 73,025 operations in 1,449 scenarios against both pinned JARs. It compares
fall state, impulse exemptions, attributes and ordered damage, statistic,
particle, sound and hit-ground requests alongside the previous motion fields.
Actual native LivingEntity.checkFallDamage, registered block methods and
Player/LivingEntity.causeFallDamage execute in the reference harness.
The cases include hay, honey, slime, powder snow, pointed dripstone, farmland,
turtle eggs and shelf mushrooms, with seeded world randomness. Farmland pushes
the stored player and changes the resident block to dirt; repeated egg changes
preserve hatch state. The resulting block state is checked after each operation.
The client refreshes its mesh after a resident block revision changes. This is a complete scene rebuild for now, with no performance improvement claimed. Damage requests are counted in runtime reports, and the health binding described below applies them synchronously. Full death/respawn, audiovisual playback and networking still need consumers. Block writes currently update resident storage; neighbor updates, drops and block-entity lifecycle remain unfinished. Block-inside effects such as cobweb slowdown and the full entity lifecycle are also unfinished. The reference harness explicitly controls these boundaries, which remain outside the comparison's scope. The earlier scene collision comparison and renderer startup also pass with this binding and the additional prepared movement resources.
The accompanying startup check passes after sharing scene loading between the GUI and headless collision probe. An interactive Linux check enables F4, holds Shift to descend, verifies that the camera stops at the terrain surface, and exits with Ctrl+Q. The window was visually inspected on Vulkan llvmpipe; this is not a hardware performance measurement.
The separate horizon-assets crate decodes the pinned 26.3 cuboid model format.
Its native comparison covers all 2,883 block-model JSON files in the client
JAR plus 2,457 synthetic cases: element bounds, optional geometry, nullable
children, face UVs and quarter turns, single-axis/Euler rotations, texture
references and translucency materials, light emission, display transforms and
inheritance markers. Floating-point fields compare by their exact bits. The
comparison also covers primitive coercion, integer truncation/wraparound and
selected strict-reader numeric/syntax limits. The numeric scanner also preserves
vanilla's rejection when its wrapping integer accumulator hits zero before
another digit, including in ignored fields. Twenty newly exposed acceptance
mismatches were corrected; the complete comparison now covers 5,340 models.
The resolution,
baking and
animation
comparisons also pass with the shared scanner correction.
python3 tools/test_cuboid_model_parity.py
The archived decoder result records source, fixture and reference hashes. The oracle uses actual client classes; those rendering APIs are absent from the server. The decoder, parent resolver and baker now feed the development gallery. Blockstate selection, transformed models and UV locking have the separate checks described below. Rendered appearance still needs its own checks.
ModelLibrary resolves parent geometry, ambient occlusion, GUI lighting and
each display context. It shares inherited elements rather than cloning them.
Texture aliases bind after child overrides are applied; successful and failed
alias walks are cached. Missing resources use the native missing-model cube;
parent cycles invalidate their descendants. The
resolution comparison
matches 5,648 requested roots across 439 graphs, including the full bundled
block-model catalog, missing parents, 702 cyclic roots, 1,024-model parent chains,
512-slot alias chains, explicit empty geometry and identity/null transforms.
It calls native ModelDiscovery, ResolvedModel and TextureSlots APIs.
python3 tools/test_model_resolution_parity.py
cargo +stable test --locked -p horizon-assets
Cuboid baking now produces positions in block units, normalized unit-atlas UVs,
face winding and facing, material references, tint indices, shade overrides and
light-emission metadata. It supports single-axis and Euler element rotations
with the pinned rescaling rule. The
baking comparison
matches 7,100 roots across 440 graphs, using native FaceBakery with a real
sprite and the default JOML math options. All bundled block models bake without
errors. Synthetic coverage includes reversed/degenerate bounds, signed zero,
arbitrary rotations, rescaling, null geometry children and parent cycles.
Position, UV and matrix fields compare by exact float bits for these fixtures.
python3 tools/test_model_baking_parity.py
That comparison uses the identity blockstate transform. Atlas packing, transparency classification, actual tint/light evaluation and GPU rendering remain separate from the geometry result. Other operating systems still need their native validation runs.
BlockModelTransform now prepares all 64 XYZ quarter-turn combinations once
and shares them across baked models. It rotates geometry around the block center,
rotates culling directions and applies UV locking in the original face basis.
The transformed baking comparison
checks every distinct model/rotation/UV-lock tuple in bundled blockstates and
all 128 rotation/lock combinations on 81 synthetic models, plus further arbitrary
element rotations. Positions, UVs and every orientation/inverse-face matrix
compare by exact float bits: 19,704 roots and 148,439 quads match, including
7,884 bundled model/transform tuples. Quaternion basis rounding and numeric identity
rechecking are retained. This remains geometry validation; atlas packing and pixel parity are separate work.
python3 tools/test_blockstate_transform_parity.py
Blockstate resources now decode into ordered variant selectors and multipart
conditions. The native blockstate comparison
covers all 1,288 bundled definitions and 1,351 synthetic cases (2,639 total,
468 expected rejections). It uses the same StrictJsonParser and
BlockStateModelDispatcher.CODEC as the pinned loader. The Rust representation
preserves single versus weighted variants, all three quarter-turn axes, UV-lock
flags, positive weights and total-weight limits, ordered selectors, nested
AND/OR conditions and per-term negation.
python3 tools/test_blockstate_model_parity.py
Property binding now selects variant roots or multipart part indices for an ordered native state list. The binding comparison covers all 1,288 bundled definitions against 35,723 registered block states plus the four special item-frame states, followed by synthetic overlap, malformed selector and predicate cases: 3,802 definitions and 687,466 state results match, with 220 expected binding errors. It checks the native replacement-before-abort behavior for overlapping variants, eager multipart validation even when variants cover every state, integer-property aliases, nested conditions and empty part selections. The controlled chunk renderer now uses this binder for its model catalog.
The Java integer-property parser is shared by horizon-core, client binding and
world-state decoding. Its codec regression
matches 221,261 output lines against each authenticated client/server JAR.
python3 tools/test_blockstate_binding_parity.py
CompiledChoices prepares cumulative weights once per definition and shares
those choices across bound states. Selecting an ordinary single variant consumes
no random draw; a weighted singleton still does. Multipart collection draws a
seed even for an empty subset and resets the same stream before each chosen
part. The native choice comparison
uses actual WeightedVariants.collectParts and MultiPartModel.collectParts
with inert observable leaves. All seven distinct bundled weight patterns,
synthetic large weights and random multipart graphs produce matching selections
and subsequent random values: 6,960 fixtures, 111,360 collections, both legacy
and xoroshiro streams. The development gallery now bakes and renders selected
models using the legacy stream and the default block position seed. Specialized
block seed callbacks remain separate work.
python3 tools/test_model_choice_parity.py
The sprite animation module reproduces the pinned metadata codec's integer conversion, strict primitive types, optional null fields and frame dimensions. It preserves repeated frames, filters out-of-image frame indices, and treats sequences with fewer than two valid entries as static physical frame zero. Frame lookup uses cumulative durations and binary search, so skipped rendering frames do not require replaying all elapsed animation ticks. The native oracle also checks dirty flags, draw requirements and interpolation timing, including transitions near the maximum Java integer duration. Interpolation pixel output and resource-loader failure recovery are outside this comparison.
python3 tools/test_sprite_animation_parity.py
The animation comparison
records 907 fixtures and 258,996 native tick states from the actual client codec
and SpriteContents.AnimationState.tick, including all 63 bundled animated
texture metadata sections and synthetic boundary cases. The associated
startup report
checks the renderer using this shared implementation. Images, input
fixtures and native traces stay private under .local/.
cargo +stable test -p horizon-client
cargo +stable clippy --workspace --all-targets -- -D warnings
python3 tools/test_native_client.py
# Headless Linux alternative:
python3 tools/test_native_client.py --vscreen
The mesh tests compare expanded rectangles against independently enumerated
exposed voxel faces, including solid neighbors, cavities, material boundaries,
negative chunk origins and random surroundings. The startup runner launches a
nine-chunk scene, requires 30 presented frames and a clean exit, verifies 3,169
greedy cube rectangles and 179 other model faces across 18 sprite layers, and writes GPU,
platform, binary and source information to ignored
.local/horizon-client/startup-smoke.json. It has an external timeout as well as
the application's frame deadline. These checks do not establish gameplay or
pixel parity with Java. Source and resource fingerprints must remain unchanged
during the run. An animation test covers duration boundaries, wrapping and
repeated physical frames independently of render frequency.
Initial local validation uses Linux x86_64, a headless Wayland compositor and Vulkan llvmpipe software rendering. Textured geometry and resizing were visually inspected; movement, arrow rotation and Ctrl+Q were exercised using virtual physical key codes. Captured relative mouse movement and Escape release were exercised through a virtual pointer. Physical-GPU runtime tests on the target machine remain pending. Software-renderer frame rates are not evidence of a hardware speedup.
The model gallery was also inspected at close range on Linux/Vulkan llvmpipe. With the camera stationary, screenshot differences were confined to the lantern as its frames changed. The model-client startup report records the bounded presentation check and the scope of this visual inspection.
The blockstate-client report
also checks all 16 gallery placements against actual Java registry binding, root
baking and model collection using state.getSeed(position). A tagged empty
geometry adapter identifies the 20 selected model parts and their transforms;
the separate baking gate covers actual quad geometry. Preparation exports typed
property domains and canonical state order, which the Rust loader checks against
its imported registry before binding. Each distinct transformed model is baked
once during gallery preparation and reused across positions. The selected state
IDs, properties, positions and models are recorded in the startup report.
Neighbor-face visibility now has native client/server coverage across all registered states and suppression-rule families. Integrating that result with resident chunk model meshing is now covered by the scene check below.
The resident-model report
checks all 10,919 occupied cells in the bounded startup scene against a separately
constructed native scene. Actual Java state-model selection, parent geometry,
rotated culling directions and Block.shouldRenderFace produce the same 5,991
visible and 59,636 culled model faces. The sixteen featured model selections also
retain their direct native comparison. Expanded greedy-rectangle tests cover
rotated and mirrored UV bases, texture and shade boundaries, negative origins,
neighbor seams and cavities; cube projection tests cover all XYZ quarter turns.
These checks cover CPU mesh behavior and GPU startup, not native shader pixels
or a hardware performance comparison.
The landing interactive smoke check exercises F5 walking from the initial camera position. The player lands at y=4, resets fall distance and records one damage request of 9.0. That historical checkpoint preceded the health integration below. The Linux window exits cleanly after visual inspection.
The health integration comparison connects actual server-player hurt to the same live attributes, synchronized health/absorption storage and hunger state used by the resident player. It checks 90,065 operations in 1,881 scenarios against both pinned JARs. Repeated hits, absorption, armor/toughness, health limits, damage cooldown boundaries, lethal hits and nonfinite incoming damage retain native results and event order. Travel reads the live movement-speed attribute through the actual native player getter, independently of the base-living stored speed field. This removes a controlled getter from the older movement harness and corrects the runtime query. Dead-or-dying state disables movement input and jumping. Server-player jumps now add the native exhaustion amount and queue a jump statistic, including when jump strength prevents upward motion. The damage-clock fields advance at 20 Hz; clock operations in the comparison establish elapsed-time state rather than invoking complete native tick methods. That checkpoint preceded the hunger integration below.
The scene loads native-decoded damage definitions, damage tags and health field IDs from the pinned private resources. Statistics, combat records, sound, network broadcasts and advancement criteria are queued service requests. The death consumer marks the stored actor dead and queues a request; drops, removal, respawn and the death screen are unfinished. F5 constructs a fresh development actor when walking is enabled. This is not vanilla respawn behavior.
The health interactive smoke check enables walking and F3 diagnostics, lands from the initial position, then walks and jumps before exiting. Health falls from 20 to 11, matching the single damage request of 9.0; the cooldown expires to zero. The final report is grounded at y=4 with 110 movement ticks and no error. The ground view was visually inspected. The startup check also passes with the additional health resources. These are Linux development checks, not release acceptance or a hardware performance comparison.
The hunger integration comparison checks 196,489 operations in 2,837 scenarios against both pinned references. The resident simulation now ticks hunger after movement. It binds actual food, saturation, exhaustion and timer fields to the same live health and attribute storage, including regeneration and starvation through registered damage sources. The damage callback borrows the FoodData currently being ticked, so exhaustion writes remain visible before its timer resets. Peaceful recovery also runs in the native Player.aiStep phase before movement. Difficulty and natural regeneration settings live with the resident world.
The reference executes actual FoodData.tick, ServerPlayer.tickRegeneration,
health setters, armor/absorption and hurt methods. Cases cover all four
difficulties, starvation health limits, absorption during starvation, changing
regeneration rules, fast/slow healing timers, timer overflow, signed zero,
nonfinite food state and combined falling/movement/food ticks. Explicit clock
setup supplies elapsed-time fields; complete enclosing tick/doTick methods
remain outside this comparison. The default walking scene uses Normal
difficulty with natural regeneration enabled. At that checkpoint food item use,
movement-distance exhaustion, effect ticks, full death/respawn services and save integration remained
unfinished.
The interactive hunger check
enabled walking and F3 diagnostics on Linux. After one landing damage request
of 9, stored health recovered from 11 to 12.5 as saturation fell from 5 to 3;
food remained 20 and exhaustion ended at 1. The player remained alive and
grounded at [25, 4, 25], with an expired hurt cooldown, after 42 movement ticks.
The ground view was visually inspected and the client exited without an error.
The development client pauses movement without focus, so wall time is not
simulation time. This checks the resident simulation and window, not full
gameplay, release acceptance or hardware performance.
The movement statistics comparison checks 215,204 operations in 5,510 scenarios against both pinned references. The resident simulation now feeds the change in player position to the movement statistics method after movement and before hunger ticking. Sprint distance adds exhaustion to the same stored food state; ordinary walking adds zero. Climbing and airborne distance also produce their native statistic requests. Statistics persistence and scoreboard delivery remain unfinished.
Vanilla invokes ServerPlayer.checkMovementStatistics from its packet listener
when accepting movement, separately from Entity.move and travel. The resident
scene directly accepts its resolved local displacement at this boundary; its
comparison invokes the actual native statistics method. It does not yet run the
packet listener's acceptance, rejection or scheduling logic. The shared kernel
also covers swimming, underwater walking, water-surface walking, crouching and
gliding. Direct method fixtures check all 512 overlapping query-flag combinations,
invulnerable abilities, float-versus-double centimeter rounding, integer overflow,
zero and nonfinite deltas. Those query overrides are restricted to statistics
fixtures; that checkpoint used dry, standing resident physics. Combined scenes exercise
repeated movement, sprint-state changes, jumps, hunger consumption and healing.
That checkpoint preceded the local sprint controls described below.
The Linux window check
enabled walking, showed diagnostics and held W for 650 ms. The player moved from
[25, 4, 25] to [23.12939004788537, 4, 23.12939004788537], remained grounded
and alive, and exited without an error after 82 movement ticks. Health recovered
to 14 after the initial fall. The ground view was visually inspected; this is a
client smoke check, while statistic amounts and exhaustion are checked by the
native comparisons above.
The sprint input comparison
checks 502,112 evolving steps in 146,648 scenarios. It executes the actual
LocalPlayer.aiStep prefix, including KeyboardInput.tick and the private
sprint eligibility and stop methods. A sentinel ends execution at the riding
boundary before parent movement. The fixtures supply environmental, effect,
vehicle and UI queries; actual food/ability storage and item USE_EFFECTS
components supply the remaining conditions. The comparison covers all combinations
of 17 condition flags, keyboard states and previous input, food thresholds,
timer boundaries, sprint windows from 0 to 10, and double-tap sequences. It
checks each ordered sprint setter request, including repeated requests.
The combined sprint scene then checks 13,387 operations in 419 scenarios. Its Java input actor shares the native scene actor's food and collision state, and its sprint setters update that actor's real attributes. Actual input preparation, movement, distance exhaustion, hunger and healing follow. Snapshots compare the sprint timer and previous input alongside position, velocity, modifiers and health/food fields. Cases include collisions, releasing the sprint key while moving, double taps, jumps, and hunger crossing the sprint threshold. The common scene check also passes against both pinned references.
The walking client now samples sprint input on each simulation tick and uses the default seven-tick double-tap window. The live food and collision fields control eligibility; sprint changes update the existing movement-speed attribute. The F3 title shows sprint state, and exit reports include sprint ticks and movement-distance counters. These counters are diagnostics, not saved statistics. Fluid movement, vehicles, effects, item use, input remapping and the complete client/server tick remain unfinished. The input comparison tests those eligibility queries in isolation; the sprint checkpoint used dry, standing actors with empty equipment. The crouch integration below extends that domain.
python3 tools/test_sprint_input_parity.py
python3 tools/test_native_scene_motion.py --sprint-input
Linux window checks exercised both Left Ctrl
and double-tap W.
The Ctrl run recorded 71 sprint ticks and 1,948 sprint centimeters. The double-tap
run visibly showed Sprint true during the second press and Sprint false
after releasing W. Both runs ended grounded and alive without an error, and
their views were visually inspected. These are development client checks;
release acceptance on the target machine and complete gameplay remain pending.
Crouching and dry pose transitions
Left Shift now binds crouching to the resident player. Local crouch selection uses the previous keyboard state before the new input is sampled; that local state controls slowdown and sprint eligibility. The current Shift input controls ledge backoff, descending collision context, bounce suppression and ladder sliding. After movement, pose-fit checks choose standing, crouching or crawling and refresh the cached bounds and eye height. Releasing Shift under a ceiling does not force the actor into a standing volume that cannot fit.
The combined input scene
checks 21,241 operations in 514 scenarios against the pinned client, including
the actual LocalPlayer.aiStep input prefix and modifyInput. The
shared scene comparison
checks 223,645 operations in 5,605 scenarios against both pinned JARs. It invokes
actual native pose selection, dimension refresh and edge backoff. The new cases
include low ceilings, blocked pose changes, scale and sneaking-speed attributes,
diagonal ledges, step-height thresholds, jumping, release transitions and landing
on different materials. Seven output lines per operation compare motion,
attributes, events, health, hunger, input history and pose/dimension bits.
The existing isolated sprint comparison
also passes after updating its scene controller.
The native fixture controls pose storage and invokes the actual dimension-refresh body when the pose changes. Full synchronized pose data and network notifications remain unfinished. That checkpoint used explicit dimension refreshes; the pending attribute integration below adds the simulation tick phase. Crawling here means dry collision pose selection; swimming physics, sleeping, flight and death poses are unbound.
The client camera follows the cached eye height with the inspected native float smoothing arithmetic. Full camera behavior and pixel parity are not established. The Linux window check visibly switched from crouching to standing after releasing Shift. It recorded 100 crouching ticks and 613 crouching centimeters, ending alive and grounded without an error. Both views were inspected. The renderer startup check, seven client tests and all-target client Clippy also passed. These checks do not establish full vanilla gameplay or final Linux release acceptance on the target machine.
Pending attribute updates
The resident simulation now dispatches pending attribute callbacks after movement
and before pose selection, matching their position in LivingEntity.tick and
Player.tick. Reducing maximum health or absorption clamps the stored value;
changing scale rebuilds the cached player dimensions even when the pose stays
the same. The update set clears after the callbacks finish, while attribute
synchronization stays queued. Existing modifier, cache and dirty-state behavior
is preserved.
The scene oracle invokes the actual private native refresh method and the
ServerPlayer.onAttributeUpdated override. Waypoint transmit/receive changes
produce ordered manager requests; waypoint management and network delivery remain
unbound. Health and absorption use the same synchronized float storage as damage
and hunger. The full enclosing player/entity tick is still incomplete.
Callback order depends on native attribute-instance identity hashes. The oracle now exports each reference process's holder and instance identities, and the Rust probe replays them. The check compares ordered health/waypoint events and validates that the same number of attribute instances is allocated. It does not sort those events to conceal ordering differences. Registry snapshots still compare map and modifier contents independently of iteration order.
The shared scene check passes 234,949 operations in 5,801 scenarios against both pinned JARs. The combined input scene passes 30,025 operations in 710 scenarios against the client. New cases combine health and absorption limits, scale, both waypoint ranges, permanent/transient modifiers, repeated refreshes and nonfinite values. Low-tunnel histories exercise movement, releasing Shift under a ceiling and changing scale between ticks.
The Linux walking check recorded 36 sprinting ticks and 36 crouching ticks, ending grounded and alive without an error. Its view was inspected. The startup check, seven client tests and all-target client Clippy passed. Full gameplay parity and playable Linux release acceptance on the target machine remain pending.