Skip to content

Latest commit

 

History

History
448 lines (358 loc) · 26 KB

missingFeaturesFromRaft.md

File metadata and controls

448 lines (358 loc) · 26 KB

Missing in RustingEngine

Things Rusting Raft needed that RustingEngine does not have yet (checked against RustingEngine 2.4.1, 10e9702). Each entry says what the game wrote instead, so the workaround can go once the engine has the feature.

Features

Raycasts before the first physics step

GameScene::raycast reads PhysicsWorld bodies, which only step_cpu_physics fills (set_bodies, src/runtime/cpu_physics.rs:1948). On the first fixed tick (fixed_tick 0) the game's tick sees no collider at all. Child colliders (the island ground) also take the GlobalTransform propagated after the previous frame (src/runtime/cpu_physics.rs:2196), and the first windowed frame runs several steps (up to 8) before any propagation, so their bodies sit at unpropagated poses until a step in a later frame: a grid sampled at fixed_tick 1 found 484 of 8660 heights. Scenarios do not show it, since their tick 0 is a zero-delta frame that propagates first.

  • Instead: tick in src/app.rs builds SceneGround once fixed_tick is past the value it had in the first frame, and the session waits for it.
  • Wanted: bodies built and posed from propagated GlobalTransforms when the scene loads, so rays hit from tick 0.

Status: done (this commit, test: rays_hit_child_colliders_before_the_first_physics_step). cpu_physics::pose_bodies propagates transforms and builds the bodies when a project loads, on GameScene::load_scene / restart (forget_old_scene) and after a code-reload restore. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Hardened relay

2.4.0's run_relay_with (src/net.rs:863, RelayLimits) caps connections overall and per address (IPv6 per /64), new connections per address per minute and the read rate per connection, skips failed accepts, and gives each handshake its own thread and a deadline. Still missing next to the game's relay: forwarding writes straight to each socket while holding the one lock over every room (its ponytail: note, src/net.rs:829; forward, src/net.rs:1048), so a peer that stops reading stalls forwarding, room opens and joins in every room on the relay for up to 5 s (the write timeout) instead of filling its own send queue; no cap on open rooms or on clients in a room; no separate limit on room opens and join attempts; no idle timeout after the handshake; a sender over the rate is slowed, not dropped.

  • Instead: Rusting Raft ships its own std-only relay (relay/, docs/relay.md) with per-IP limits, join and room rate limits, a handshake deadline, per-connection bandwidth caps and send queues, and a systemd sandbox unit (deploy/rusting-relay.service). The game client talks to it with net/ (raft_net), whose protocol differs from NetSession's.
  • Wanted: those limits in run_relay, so games can use NetSession::host_room / join_room directly.

Status: done (this commit, tests: a_peer_that_stops_reading_stalls_only_itself, a_relay_caps_rooms_clients_and_room_attempts, a_relay_drops_a_sender_over_its_rate, a_relay_closes_a_connection_idle_after_its_handshake, a_throttle_allows_one_seconds_burst_then_refuses, a_client_flooding_a_slow_host_is_cut_off_not_the_room_or_others). Each relay connection writes through its own bounded queue and writer thread (RelayPeer), so nothing touches a socket under the rooms lock; RelayLimits gains max_rooms, max_clients_per_room, room_attempts_per_minute (per address, refused attempts count), idle_timeout and send_queue (bytes); a sender over bytes_per_second, or a message larger than one second of it, is dropped. Each client holds at most send_queue / max_clients_per_room of its host's queue; a client over its share is dropped, not the host or other clients. Known limit: without a token, 16 addresses (16 connections each) can hold all 256 rooms; run a public relay with a token. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Scenario step for the mouse wheel

Scenario steps (StepAction, src/scenario.rs:1046) can move the pointer, click a label and press actions, but cannot turn the mouse wheel. The game's long windows (Workbench, Logbook) sit in an egui ScrollArea (src/ui/windows.rs:75), and click only finds text that is visible, so rows below the fold (Wood Farm, Radio Station) cannot be clicked by label.

  • Instead: tests/flow_05_farm.json and tests/flow_06_radio.json drag the scroll bar: a pointer step on the bar, press use (bound to MouseLeft), more pointer steps, then release. This depends on the bar's pixel position.
  • Wanted: a {"scroll": [dx, dy]} step, or a click that scrolls the target into view first.

Status: done (this commit, test: scroll_steps_turn_the_wheel_over_an_egui_scroll_area). StepAction::Scroll records wheel lines in RuntimeInput::record_scroll; the headless egui pass sends them as Event::MouseWheel, and the windowed runtime records MouseWheel events too, so game code reads RuntimeInput::scroll(). Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Cursor grab that follows mouse look and focus

The controller's look code (player_look, src/runtime/player.rs:326) frees the cursor when mouse look goes off or Esc goes down, but grabs it again only on the next left click. So after a window closes (GameScene::set_mouse_look(.., true), src/project_runner.rs:872) the camera does not turn until the player clicks. Losing focus also keeps the grab: RuntimeInput::record_focus (crates/rusting-core/src/input.rs:235) releases keys only, and GameScene has no call to grab or free the cursor.

  • Instead: cursor in src/app.rs writes RuntimeInput::set_cursor_captured through scene.world().resource_mut::<RuntimeInput>() every frame: grabbed while mouse look is on, so a window closing or an Esc that opens nothing (placing a part) keeps the camera turning. Focus loss or the Windows key frees it until the next click, and that click does not fire the held tool. The engine's look code runs after the game's update, so an Esc the game used still frees the cursor for that one frame.
  • Wanted: GameScene::set_cursor_captured(bool), a grab when set_mouse_look turns look on while the window has focus, a release on focus loss, and a way for the game to keep an Esc it used (cancel placing a part) from freeing the cursor.

Status: done (this commit, test: cursor_grab_follows_mouse_look_focus_and_the_game). GameScene::set_cursor_captured writes RuntimeInput::set_cursor_captured, which also marks a grab asked for this frame; player_look skips its Esc release on that frame. set_mouse_look(.., true) grabs at once while focused, and RuntimeInput::record_focus(false) frees the cursor. Not done: keeping the regrab click from reaching game tools (game code can check cursor_captured() before acting on a click). Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Scenario step for a raw key

Scenario press and tap (StepAction, src/scenario.rs:1046) take a named action only. A key that no action uses, such as the Windows key (SuperLeft), cannot be pressed in a scenario.

  • Instead: the unit test cursor_follows_look_and_stays_free_after_focus_loss_until_a_click in src/app.rs covers the Windows key; tests/ui_cursor.json covers focus loss.
  • Wanted: a {"key": "SuperLeft"} step that presses one key code.

Status: done (this commit, test: key_steps_tap_a_key_no_action_uses). StepAction::Key takes a winit KeyCode name, records the press before the tick's update and the release before the next, like tap; an unknown name fails to parse. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Quaternion-safe animation blending

Clip sampling (AnimationTrack::sample, lerp in src/runtime/animation.rs:265) and the crossfade (mix, src/runtime/animation.rs:591) lerp Orientation keys component by component, with no sign check (the result is only normalised when written). Two keys for the same rotation with opposite signs (q and -q) pass near zero in between, and the bone collapses or spins. A crossfade also keeps a track that only the old clip keys at full weight, so that bone stays where the old clip left it.

  • Instead: the hand-keyed "swim" and "tread" clips (swim_clips in tools/scene/build_scene.py) keep w <= 0 like the imported clips, add a second key at the same time wherever the sign flips, and key Position and Orientation on every bone. The crawl arms are keyed as a swing from rest with no twist (stroke), so a full stroke ends on the same key instead of the negated one. Every clip on a swimming human also gets rest keys for the bones it leaves alone, so the swim roll does not stay on after the fade into a clip that never keys the root.
  • Wanted: nlerp or slerp with a shortest-path sign flip for Orientation in sample and mix, and a crossfade that eases tracks only one side has back to rest.

Status: done (this commit, tests: orientation_blends_take_the_short_way_round, a_crossfade_eases_tracks_the_new_clip_leaves_back_to_rest). sample (Linear and Smooth), mix and override layers nlerp Orientation after flipping the far key to the near sign. AnimationPlayer records each Position / Rotation / Scale / Orientation value before it first writes it, and a crossfade blends a track only the old clip keys toward that rest value, ending on it. Tracks only the new clip keys still start at full weight. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

Features for v2 (checked against RustingEngine 2.5.6)

What the v2 plan (docs/superpowers/specs/2026-10-14-rusting-raft-v2-design.md) needs. E1 to E6 were planned in spec 4.3; E7 onward turned up while building it.

E1. Open ocean in rusting.water (staged)

rusting.water is a fixed rectangle of three sines. It has no endless sea around the camera, no Gerstner waves, no way to change the waves at runtime, and no game clock.

  • Instead: nothing. The change is written and tested (the water tests pass, clippy is clean) and the game already uses it. It is uncommitted in ~/RustingEngine on local branch raft-v2-gaps (made from the v2.5.6 tag), with a CHANGELOG.md [Unreleased] entry. Commit it there for v2.5.7.
  • Wanted: WaterBody gains waves: Vec<OceanWave> (direction, length, height, steepness, with analytic normals), ocean: bool (the grid follows the active camera, its cells grow away from it, and bodies float everywhere) and time: Option<f64> (a game clock in place of the fixed tick's). WaterBody::wave returns the swayed Gerstner surface. WaterBody stops being Copy, and wave takes its time as f64.

Status: split into E1a (waves, clock, f64 time) and E1b (ocean). The staged change on raft-v2-gaps is not on this machine, so both are rebuilt here. E1a: done (this commit, tests: gerstner_waves_sharpen_crests_and_keep_time_in_f64, a_water_clock_moves_the_mesh_in_place_of_the_fixed_tick). WaterBody gains waves: Vec<OceanWave> (direction, length, height, steepness; deep-water speed, analytic normals) and time: Option<f64>; WaterBody::clock picks the seconds the mesh, buoyancy and swimmers use. wave takes f64 time, wraps phases in f64, and returns the swayed surface over (x, z) (8 fixed-point steps back to the swayed start point). WaterBody is no longer Copy. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run. E1b: done (this commit, test: an_ocean_follows_the_camera_and_floats_bodies_everywhere). WaterBody::ocean makes contains true everywhere (buoyancy and swimming), and sync_water centers the grid on the active camera (same pick as active_camera, snapped to whole center cells) with cells a quarter of the even size at the center growing to 1.75x at the rim. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.

E2. Clouds on rusting.procedural_sky

  • Instead: the sky has no clouds, so weather barely shows overhead.
  • Wanted: cloud_cover, cloud_darkness, cloud_speed and a wind direction, with clouds that catch and dim the sun.
  • Status: done (this commit, test: clouds_grey_the_sky_and_hide_the_sun (GPU) and clouds_drift_with_the_wind_and_clamp_their_cover). ProceduralSky gains cloud_cover, cloud_darkness, cloud_speed and wind_direction; the background pass draws an fbm sheet 2 km up that drifts with elapsed_seconds, glows toward the sun and hides its disc. The sky bake (lighting, reflections) stays cloudless so it does not rebake as clouds move. Passed: fmt, clippy (default, no-default-features, gpu-tests), cargo test --workspace, gpu-tests under the lock. Looks on real hardware: needs hardware check.

E3. Particles stopped by roofs

  • Instead: rain falls through roofs and into cabins.
  • Wanted: an emitter option that kills particles below a height field or inside rusting.particle_blocker boxes.
  • Status: done (this commit, test: blocked_rain_stops_at_roofs_and_the_ground). ParticleEmitter::blockers kills CPU particles inside ParticleBlocker { extents } boxes (turned and scaled with the object) and below any Heightfield, in the fixed step and the editor preview. GPU simulated emitters ignore it. Passed: fmt, clippy (default, no-default-features, gpu-tests), cargo test --workspace, gpu-tests under the lock.

E4. Global wetness

  • Instead: only terrain can look wet (set_terrain_look). Decks, sails and rocks stay dry in rain.
  • Wanted: one global wetness value from 0 to 1. Materials marked wettable get darker albedo and lower roughness as it rises.
  • Status: done (this commit, test: wetness_darkens_only_wettable_materials). Scene files do not save wettable yet (needs a cooked format version); games set it in code.

E5. GPU skinning (done)

The engine has had GPU skinning (rendering::gpu_skinning) since 2.5.0, so nothing is needed here. What is still missing is E15.

E6. Lens droplets post effect

  • Wanted: a post effect for rain on the camera lens, with a strength from 0 to 1. It is for the first-person view in storms.
  • Status: done (this commit, test: lens_droplets_bend_the_image_and_follow_the_tick). rusting.color_grading lens_droplets, drawn in the tone-map pass.

E7. Viewmodel layer with its own FOV

A camera has only projection, active, priority and viewport. There is no render layer and no second pass that clears depth.

  • Instead: the held tool is a child of the camera, 0.45 m in front. It shares the world FOV and can poke into walls.
  • Wanted: a render layer on MeshRenderer, and a camera that draws only that layer over the main view after clearing depth, with its own FOV (spec 5).

Status: split into E7a (renderer and components) and E7b (scene files, editor, schema). E7a: done (this commit, tests: viewmodel_meshes_draw_over_walls_with_their_own_fov (GPU), viewmodels_extract_only_under_a_viewmodel_camera). The layer is a RenderLayer component (World, Viewmodel) next to MeshRenderer, since MeshRenderer literals are everywhere. The camera side is ViewmodelCamera { vertical_fov_radians } on the active camera. There is no second pass: viewmodel meshes scale clip-space xy to their FOV and squash their depth toward the near plane, so they draw over walls in the same pass and keep true world lighting. Frustum culling still uses the main camera's FOV. E7b: done (this commit, test: examples_load_through_the_scene_parser_and_registry_unchanged). Scene files, snapshots, the editor and the schema know rusting.render_layer and rusting.viewmodel_camera.

E8. Reading the resolved quality profile

resolve_quality runs inside the renderer. On an integrated GPU it picks Eco, under 4 GB of VRAM it picks Balanced, and otherwise High. Game code only sees the requested RenderSettings::quality.

  • Instead: game-side content (ocean grid, foliage counts, rain particles, view distance) treats Auto like Medium.
  • Wanted: a read-only RenderSettings::resolved_quality, or a GameScene getter.
  • Status: done (this commit, test: resolved_quality_follows_the_gpu_once_a_renderer_starts). GameScene::resolved_quality(); the game runner now puts RendererCapabilities in the world, as the editor already did. It stays Auto before a renderer starts.

E9. Shadow cascade count

ShadowQuality sets the map size and distance: Low 1024 and 30 m, Medium 2048 and 50 m, High 4096 and 80 m. It does not set how many cascades there are.

  • Wanted: a cascade count per setting. Spec 4.2 asks for 2 on Low, 3 on Medium and 4 on High/Ultra.
  • Status: done (this commit, test: shadow_settings_override_the_profile). shadow_settings returns the cascade count: 2 on Low/Eco, 3 on Medium/Balanced and 4 on High. The atlas stays 2x2, so fewer cascades leave tiles unused at the same texel size.

E10. Texture resolution cap

  • Wanted: a setting that limits loaded textures to a maximum size or mip level. Spec 4.2 ships 2K textures with 1K mips, and Low should load only the 1K level so it fits the iGPU's memory and bandwidth.
  • Status: done (this commit, test: max_texture_size_reuploads_textures_at_the_capped_size, texture_cap_halves_each_layer_until_it_fits). set_max_texture_size(1024); HDR textures keep full size.

E11. Water waves on the GPU

sync_water rebuilds and re-uploads the whole water mesh every fixed tick (the ponytail note in water.rs says so too). An ocean at resolution 96 has about 9,400 vertices, which means about 0.6 MB and roughly 1 ms of CPU per tick. Storm seas need finer grids.

  • Wanted: wave displacement in the vertex shader.
  • Status: split into E11a (the vertex shader displaces a flat grid by RenderWorld::water_waves) and E11b (sync_water builds the flat grid once and extraction fills the waves each tick).
    • E11a Status: done (84f4f6b, test: water_waves_lift_a_flat_grid_in_the_vertex_shader).
    • E11b Status: done (this commit, tests: gpu_waves_trace_the_surface_the_cpu_samples, a_water_clock_sets_the_waves_in_place_of_the_fixed_tick).

E12. Foam on wave crests

Foam is drawn only at the shore (shore_foam). Vertex colour cannot add it either, because it multiplies the base colour and so cannot turn blue water white.

  • Wanted: a crest_foam strength in the water shader, driven by how much the Gerstner sway pinches the surface (its Jacobian). Spec 4.1 asks for foam on crests and around the raft.
  • Status: done (this commit, test: crest_foam_whitens_where_steep_waves_pinch_the_surface).

E13. rusting test building once

rusting test . --keep-going runs cargo build again before every scenario.

  • Instead: no source edits while the suite runs, since later scenarios would pick up the changes or fail.
  • Wanted: build once and run every scenario with that binary. This is faster and keeps a laptop cooler.
  • Status: done (this commit, test: a_scenario_folder_runs_every_scenario_in_the_one_build).

E14. Mouse wheel input actions

rusting.input_action takes keys, MouseLeft/MouseRight/MouseMiddle and gamepad buttons (src/runtime/actions.rs:33), but not the wheel.

  • Instead: the 10-slot hotbar is picked with keys 1-0 only.
  • Wanted: WheelUp / WheelDown as pressable inputs (one press per wheel line), or GameScene::scroll_delta().
  • Status: done (this commit, GameScene::scroll_delta(), test: a_tick_function_reads_each_wheel_turn_once).

E15. Copies keep animation and skin

copy_tree (src/project_runner.rs:4107) copies the meshes, but the copy's Animation tracks still point at the original's bones, so only the original animates.

  • Instead: the first shark plays its clips. A second or third shark only sways (src/view/movers.rs, ONE_INSTANCE). Players use a fixed pool of 7 avatars in the scene.
  • Wanted: copy_tree remaps Animation tracks and skin joints onto the copied bones, so a copy animates on its own.
  • Status: done (this commit, test: copies_of_copies_animate_their_own_bones). Tracks and joints already resolve copied bones by path; a copy of a copy named them "Shark 3/Shark 2/Tail", so path Tail missed. copy_tree now drops the old prefix.

E16. Listing a node's children

GameScene has no way to list the children of a named node (only SceneTree::children inside the engine), and set_color changes one node.

  • Instead: the four coloured parts of a raft tile are written out in the game code (TILE_PARTS in src/view/mod.rs), so a damaged tile can be darkened part by part.
  • Wanted: GameScene::children(name) -> Vec<String>, or set_color_tree(name, tint) that multiplies the colour of a node and every node under it.
  • Status: done (this commit, test: children_lists_the_named_parts_of_a_node). GameScene::children; tint a tree with it plus color/set_color.

E17. Foliage of a multi-part model

Foliage scatters copies of one object's mesh. The Kenney palms are two meshes (trunk and leaves), so they cannot be foliage.

  • Instead: palms are placed one by one at the sampled terrain height, each with a sway material (tools/scene/build_scene.py).
  • Wanted: Foliage on a parent copies the whole subtree as one instanced group, or takes a list of meshes that share each copy's transform.
  • Status: done (this commit, test: foliage_on_a_parent_scatters_every_part_together). Each mesh under a Foliage parent draws one instanced batch per part.

E18. Foliage density per quality preset

Foliage::count is fixed; the quality profile does not thin it.

  • Instead: the client stores each foliage's base count and rescales Foliage::count when the graphics preset changes (src/app.rs, Graphics::foliage_share: Low 0.3, Medium 0.6, High/Ultra 1.0).
  • Wanted: a per-profile foliage share (and far impostors on Ultra) in the engine.
  • Status: done (this commit, test: quality_profiles_thin_foliage_from_the_first_copies). foliage_share (Eco 0.3, Balanced 0.6, High 1.0) thins every scatter at extraction. There is no Ultra profile, so far impostors are left out; rusting asset bake-foliage cards cover them.

E19. Heightfield from an image

Heightfield.heights is inline Vec<Vec<f32>> in the scene JSON. The 14 island terrains add about 160k samples (~45k lines) and 0.8 MB to scenes/main.rscene.

  • Instead: the generator writes the samples inline.
  • Wanted: a heightfield that reads a 16-bit PNG or raw file under assets/ with a height scale, like the textures do.
  • Status: done (this commit, test: heightfields_read_their_heights_from_an_image_under_the_scene). Heightfield.image (relative to the scene) and height_scale; saved scenes drop the samples.

E20. World pose of a named bone

GameScene has no bone_world(object, bone). skinning::joint_matrices gives skin matrices, not a world pose.

  • Instead: the octopus holds a player at a fixed offset from its body. (Finding the bone with scene.world() plus scene_tree::find_path and combining its parents' transforms by hand would work but reaches into engine internals.)
  • Wanted: GameScene::bone_world(object, bone) -> Option<[f32; 16]> (or position and rotation) after this frame's animation.
  • Status: done (this commit, test: bone_world_gives_a_named_bones_pose_in_the_world).

E21. Global wind for foliage

Foliage and sway materials lean only inside force fields (field_pushes skips copies outside a field), and a Wind force field also pushes every GPU particle (gpu_particles.comp:157).

  • Instead: one scene-wide wind sensor box (2400 x 300 x 2400 m) with a weak Wind field (0.5 m/s²), so rain, smoke and fire are pushed too; higher sway values make up for it.
  • Wanted: a scene-level wind (direction, strength, gusts) that sways foliage and materials without being a force field, with a per-emitter wind factor for particles.
  • Status: done (this commit, test: scene_wind_pushes_particles_by_their_wind_factor, scene_wind_reaches_the_lean_and_gpu_particles_but_not_bodies, swaying_materials_bend_plain_meshes_in_wind_fields). rusting.wind and ParticleEmitter::wind_factor.

Bugs seen

  • The frame cap (FramePacer::request_next_frame, src/rendering/frame_pacer.rs:77) sets next_frame = now + interval from the moment the loop wakes, so each late wake from ControlFlow::WaitUntil adds to the period and the rate settles below the cap. next_frame += interval (back to now only when far behind) holds the cap. The game's Max FPS setting (GameScene::set_max_fps) inherits this. Status: already present (src/rendering/frame_pacer.rs:96, FramePacer::due steps next_frame += interval; test the_limiter_holds_its_rate_through_late_wakes_and_uneven_frames).
  • FrameTime::real_delta is documented as real time since the last frame, but RuntimeApp::update (src/runtime/mod.rs:686) clamps it to one fixed step while the app is overloaded, so a frame counter built on it never reads below 60. The game's FPS counter (src/ui/mod.rs) reads Instant instead. Status: done (this commit, test: fixed_steps_slower_than_real_time_fall_back_to_slow_motion asserts real_delta 50 ms while delta is clamped to 1 ms). App::update clamps only the clock; replays keep the real delta in ReplayFrame::real_nanos. Passed: cargo fmt --all --check, clippy default / --no-default-features / --features gpu-tests with -D warnings, cargo test --workspace, and the locked --features gpu-tests run.