Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
20 KiB
MVP browser client
The client lives in client/ and is served by shacraft-server at /. It uses ES modules and its own WebGL2 renderer, with no bundler, npm dependencies, or off-the-shelf game engine. Open http://127.0.0.1:4000 after starting the server. Modern desktop browsers with WebGL2 are supported; localhost or HTTPS is required for crypto.subtle and package verification.
Controls
- Click the world to capture the mouse. A drag or rejected request never disables the next capture attempt. If the browser blocks pointer lock, select the explicit drag-control mode in the game menu; holding a mouse button turns the camera and short clicks edit blocks in that mode. The canvas is focusable, and F3 reports capture status and browser errors.
- WASD or arrow keys move; Space jumps. Movement follows the camera; +Y is up, and yaw 0 looks along −Z. The player's position specifies their feet.
- Hold Ctrl to sprint and Shift to crouch or descend in creative flight. Crouching reduces the collider and eye height and prevents walking off supported edges. Double-tap Space within 300 ms, or press F, to toggle flight when the world permits it; Space ascends. The same controls handle swimming and ladders.
- Left-click removes the selected block, right-click places a block against the selected face, and middle-click copies the selected material into the current hotbar slot.
- 1–9 or the mouse wheel selects a hotbar slot. E opens the library of all states; search uses English Minecraft identifiers. Selecting a state replaces the current slot.
- T or Enter opens chat, Enter sends, and Esc closes it.
- The game menu includes Render distance, from 2 to 64 chunks (up to 1,024 blocks) around the player, with Apply and a loading counter. It remembers the choice locally; increasing it also extends fog, the projection range, and actual streaming bounds. Larger views load progressively with nearby sections first.
- The world-name button opens the instance selector. F3 opens diagnostics. The menu contains the player name, return-to-spawn action, and Spleef start action.
- Esc releases the mouse. Losing focus or opening a panel sends zero input so the player does not keep moving.
Rendering and synchronization
Geometry is built from each material's local box shapes. Meshes are divided into 16³ sections; shared faces between full opaque cubes are culled. A block change rebuilds affected sections, including diagonal neighbors whose corner lighting changes. Transparent materials use a separate pass with sections sorted by distance; transparent surface ordering within each section is simplified. A package with the declarative style effect: bounce (including the trampoline and custom blocks) supplies a texture, a verified GLSL highlight function, and an original sound; the response is tied to the player's upward movement received from the server. Entity shapes come from the catalog; players have separate multipart avatars. These are original, simplified visuals rather than an exact reproduction of Minecraft.
Static terrain uses separate persistent module workers for whole-view light and local terrain meshing. Protocol v2 uses dense SectionVoxelMap arrays and transfers copied section buffers, retaining main-thread collision ownership. The legacy path uses indexed SectionBlockMap records. Later updates send block deltas, complete entering sections, section unloads, sky occlusion columns and changed material definitions. Nearby meshes compute their own bounded light tiles and do not wait for whole-view light. Eight cached tiles cover 2×2 chunk columns each, with an 18-block halo and the full active vertical range. Spatial invalidation and per-section tickets prevent unrelated distant batches or view-edge changes from cancelling useful mesh work. Mesh vertices are section-local; camera-relative drawing preserves precision at distant coordinates. The controller permits one mesh request in flight and at most two completed meshes waiting for upload, prioritizing received nearby dirty sections. See world streaming for storage, wire format and bounds.
The main thread uploads completed section buffers in bufferSubData steps of at most 64 KiB, targeting a 2 ms upload budget per frame. It keeps the previous visible mesh until both opaque and translucent replacement buffers are complete, then replaces their GPU handles while retaining the section object. This avoids displaying partially uploaded geometry. GPU allocation and an individual driver call cannot be interrupted, so the budget is a scheduling target rather than a guaranteed frame duration. Dynamic avatars/entities, their light samples, draw submission and UI remain on the main thread. If the terrain worker cannot start or later fails, the renderer retains visible meshes and uses synchronous terrain meshing with the former BlockLightController; diagnostics identify this fallback explicitly.
The default shader has Moonlight and Daylight modes. The game starts at night, with a dark sky, stars and weak cool moonlight; the lighting button in the game menu switches to daylight and remembers the choice locally. Daylight combines warm direct sunlight with cool sky illumination and muted ground bounce. Texture colors are decoded from sRGB, lit in linear space and encoded for display; texture sampling stays nearest-neighbor. One fixed world-space light direction controls the sun/moon and shadows. The sky, distance fog and water reflections follow the selected mode. Emissive materials also illuminate nearby surfaces and actors through separate propagated block/sky light fields, each with canonical levels 0–15. Skylight controls natural illumination, reflection and fog inside roofed spaces; source light remains independent of the time of day. See block lighting for measured rules, worker integration and boundaries.
Sun shadows use a 2048² depth map (bounded by GPU support), a light-space grid that snaps to texels, and a stable 3×3 PCF filter. Coverage fades near the 48-block radius. Opaque terrain, cutout texture silhouettes, players and entities cast shadows; translucent glass and water do not cast solid shadows. Shadow geometry updates with world edits and actor movement. Geometry-aware corner ambient occlusion also considers slabs, stairs and shapes extending beyond their owning cells, while leaving unobstructed flat planes clean. Shader sources live in lighting-shaders.js, shadow projection in shadow-frame.js, and AO sampling in ambient-occlusion.js. F3 shows the active lighting and shadow-map size. If a depth framebuffer is unavailable, the client keeps daylight and AO without sun shadows.
Open /tests/renderer-smoke.html for a deterministic lighting fixture with overview, corner, sun-facing and enclosed-room cameras, a shadow toggle and a removable pillar. The room includes a torch/sea-lantern/off control, glass window and removable partition, plus canonical block/sky readings. Its DOM diagnostics retain WebGL errors and mesh counters for browser verification. Unit tests cover corner occlusion, partial shapes, chunk-boundary invalidation, shadow projection, propagation, measured Java light transitions and asynchronous result ordering; the fixture checks the actual GPU programs and rendering path.
The client runs the same Rust movement solver as the server, compiled into client/physics.wasm. It loads the module before joining and advertises movement_prediction_v1 only after its ABI has been verified. Input is sampled at fixed 50 ms steps independently of display FPS. A stalled frame contributes at most five catch-up commands, and hidden pages reset their accumulator. The client sends controls and sequence numbers, never its own position, and does not edit blocks optimistically.
The server's per-player motion message supplies the authoritative body, processed input sequence, tick, movement settings, and reset epoch. The client removes acknowledged commands, then replays remaining commands from that body using the shared solver and its current collision neighbourhood. A maximum of 120 commands is retained; an overflow suspends prediction until the missing history has been acknowledged. Out-of-order ticks and decreasing acknowledgements are ignored. Respawn, world changes, reconnects, and input-reset epochs clear old commands. A teleport also clears interpolation and pending commands.
Render frames interpolate toward one disposable predicted next step, so local movement and mouse look respond before the round trip to the server. Small authoritative corrections decay visually without changing the simulated position or velocity. Crouching and swimming change the camera eye height. The local collision query includes the swept body volume, extended collision boxes, climbable blocks, and fluids even when their collision shape is empty. If a required chunk or material definition has not arrived, prediction waits for authoritative motion instead of treating unknown space as air. Block and chunk changes invalidate the render preview. A missing or incompatible WebAssembly module leaves the client in a visible server-only fallback mode.
Opening a panel, losing focus, or hiding the page sends input_reset when prediction is negotiated. The server clears queued movement and acknowledges the discarded commands with a new epoch, while gravity continues. A look packet immediately before a block action updates the server's selection ray without queuing another movement step. On legacy servers the client uses the previous zero-input packet and smooths server positions. Server-side package jump hooks remain authoritative; their impulses can produce a small correction on the first predicted jump.
Remote player yaw has a separate server target and displayed angle. Each render frame smooths position and yaw with 1 - exp(-19 * dt), using the shortest angular arc for yaw. New packets replace the target without snapping the displayed pose. Initial appearances and world changes initialize the pose immediately. This avoids 20 Hz rotation steps and long spins across the angle wrap.
The client prefers chunk_stream_v2, view_buffer_v1 and full_height_v1, with a default 9×9×24 data window of 16³ sections (configurable from 7×7×24 to 131×131×24), typed cell arrays, palette/RLE batches and explicit readiness. Uniform sections use one uint32 value, including in worker transfers; editing expands only the affected section. The entire world height, Y=−64 through 319, remains resident during vertical flight. This includes one loaded chunk ring beyond the selected radius. Horizontal loading anchors move after two chunk crossings, keeping overlap and avoiding immediate unloads on a single crossing. Distance fog blends the visible edge before buffer unloads. A view generation rejects late batches from previous windows; revisions still order actual edits. Same-world resync keeps existing visible geometry while replacing authoritative sections. The v1 path remains available for older nonprocedural servers: an aligned 64×48×64 window with indexed block records. See section protocol v2.
F3 includes movement recordings, automatic routes, a rolling frame/CPU chart and JSON export. Automatic scenarios deliberately continue through the diagnostics panel; Stop/Esc or hiding the tab stops them. Manual play still resets held input when a panel opens. Diagnostics is a nonmodal overlay: clicking the world, resuming play and starting a recording leave it open. F3 or its close button hides it. Focusing its controls pauses manual movement; clicking the canvas restores control. Trace limits, timing definitions and measured local results are in movement diagnostics.
A full registry is optional: snapshots contain at most 256 definitions of materials in use, and the client gradually fetches the remaining shapes through /api/catalog?ids=...&limit=128. Newly received material definitions also refresh retained sections that were displaying neutral fallback cubes. blocks events are applied only in revision order; data outside the current view bounds is discarded while the revision still advances. A chunks message must match the current world, revision, and previous center, with exactly the expected entering and departing sections; it cannot advance the world revision. A revision gap or an inconsistent transition triggers resync before any chunk data is changed. Servers without the negotiated feature can still send full snapshots using the legacy 64×40×64 window. After a disconnect, the client retries after 1, 2, 4, 8, 16, then 20 seconds. Connection failures, incompatible protocols, resource errors, and actions rejected by the server are shown to the user.
Dynamic actors already contain camera-relative vertices. Both the color and shadow passes explicitly reset their own program's section offset before drawing players and entities. They must not inherit the last terrain mesh's offset. Regression checks cover mesh-order changes, empty terrain frames, negative/distant coordinates and camera-origin crossings. The renderer fixture includes paired actor cameras at X=15.99 and X=16.01 for a visual boundary check and reports actual WebGL errors.
Packages
Before joining through WebSocket, the client fetches /api/manifest, downloads the declared client files from the same server, verifies their sizes and SHA-256 hashes, and stores their contents in CacheStorage keyed by hash. Cached files are verified again. Cache failure does not skip integrity verification. A hash mismatch removes the file from the cache and prevents joining. A single file is limited to 64 MiB, the set verified per download to 128 MiB, and the cache to its 512 most recent entries. These limits apply to verified contents; browser network buffers and image decoding also consume memory.
The Control API token is never sent to the client. Packages do not execute arbitrary privileged JavaScript in the browser. Client-side hash verification establishes resource compatibility; it does not prove that the client itself is unmodified on the player's device.
Debugging and checks
F3 displays real values: world, revision, visible block and entity counts, players, WebGL, FPS, triangles, section mesh counts, mesh rebuilds, geometry resets, full snapshots, chunk updates, tick, acknowledged input, physics mode, pose, pending input count, correction distance, coordinates, packages, the active block texture pack and its image count, and available server metrics. #world-canvas exposes data-world, data-revision, data-blocks, data-entities, data-webgl, data-connected, data-texture-pack, data-texture-count, data-texture-size, data-full-snapshots, data-chunk-updates, data-mesh-resets, data-mesh-rebuilds, data-section-meshes, and data-view-center; these update once per second for automated browser checks. Movement checks also use data-physics (predicted, waiting-world, or server), data-player-position, data-player-pose, data-prediction-pending, and data-prediction-correction. Secrets and control commands are not exposed through the DOM. See local block texture packs for the package descriptor and preparation command.
Terrain diagnostics include frame-time p95/maximum over the most recent 240 frame intervals, the number of dirty sections, worker/fallback mode, the last packet-preparation time, worker section-build time and current frame's geometry-upload time. The canvas exposes data-terrain-mode (worker or main-thread-fallback), data-terrain-prepare-ms, data-mesh-build-ms, and data-mesh-upload-ms. Lighting diagnostics include data-block-lighting, data-light-build-ms, data-light-sources, data-block-light, and data-sky-light. Worker build time is background CPU time; it is distinct from the frame and upload measurements.
F3 also shows the geometry worker mode (data-terrain-meshing), first nonempty terrain publication (data-first-terrain-ms), completed/received sections in the camera's 3×3×3 neighborhood (data-near-meshes / data-near-loaded), and local light tile work (data-local-light-cells / data-local-light-ms). These distinguish nearby rendering progress from background completion of the full draw distance.
Run the client checks with cd client && npm test. They cover camera axes and projection, negative coordinates, the nearest face, reach limits, exact hits on non-full-block shapes, partial-pack fallback, per-face texture selection, cutout/tint metadata, horizontal log UV orientation, retained block maps and meshes during streaming, empty incoming sections, exact view bounds, same-world snapshot differences, and rejection of inconsistent or out-of-order chunk transitions. Physics checks cover FPS-independent timing, bounded catch-up, acknowledgement replay, stale packets, reset epochs, teleports, missing collision data, history overflow, and disposable render predictions. They also instantiate the shipped WebAssembly through the browser loader, compare its per-tick positions and velocities with independently measured Java 26.2 movement fixtures, and replay delayed authoritative updates through the actual solver. Rebuild the asset with scripts/build_physics.sh after changing the Rust crate. Visual and end-to-end checks use a real server separately from these unit tests.
Confirmed small block edits use a separate edit-mesh worker, independent of whole-view lighting. It receives only the section and its sampling neighborhood, builds current geometry/culling/AO with the previous light field, and gets upload priority. Lighting is corrected by the regular terrain worker afterwards. At most one edit job is in flight and two completed buffers are queued. Section tickets reject superseded edits, unloads and world resets; unrelated chunk updates do not discard a valid local edit. Large bulk edits keep the regular terrain path. If the edit worker fails, edits still use the ordinary terrain queue.
F3 separates the last local edit’s server acknowledgement, confirmed-to-mesh and total time. data-edit-timings contains acknowledgementMs, geometryMs and totalMs; data-edit-prepare-ms, data-edit-mesh-ms and data-edit-worker expose the small worker path. Timings end at mesh publication in the render loop, not GPU completion or monitor scanout. block-edit events are included in recorded traces. Server authority, edit validation and collision updates are unchanged.
Terrain tests cover indexed world changes, persistent worker state, transferred deltas, stale generations, material/texture changes, pure geometry output and staged GPU uploads. Fake-GL upload tests check byte offsets and the 64 KiB step limit, preservation of old opaque/translucent meshes until completion, partial-upload disposal, and yielding after an expensive allocation. They also check independent edit uploads against newer edits and unrelated terrain versions. Local edit geometry is compared byte-for-byte with whole-world geometry at section boundaries and distant coordinates, including glass and lamps; live voxel/light buffers must survive worker transfer. They do not establish real GPU frame-time guarantees.
Open /tests/terrain-streaming-smoke.html for an isolated browser comparison between worker and main-thread meshing. It warms a synthetic 64×64 view before a 12-second run at eight blocks per second, crossing six section boundaries without modifying a server world. Results include frame and JavaScript draw timings, the duration of each chunk-update stage, retained-mesh checks and WebGL errors. Keep the tab visible and compare modes in the same browser at the same canvas size. CPU draw timing measures JavaScript and graphics submission, not GPU completion.