# Running the F3 sandbox

You fly an interceptor against a bot, next to a 1.6 km capital ship for scale.

## Controls
| Action | Keyboard and mouse |
|---|---|
| Capture the mouse | click in the window (Esc releases it; Esc again quits) |
| Pitch and yaw | move the mouse (it acts as a virtual stick that recentres) |
| Pitch / yaw / roll | W S / A D / Q E |
| Throttle up / down | R / F (you start at 80%) |
| Boost | Left Shift |
| Fire | Left Control or left mouse button (after the mouse is captured) |
| Drift (hold, at boost speed) | Space |
| Cockpit / chase view | C (cockpit is the default; chase is for debugging) |
| Next target / nearest target / next part of a locked capital ship | Y / T / U |
| Guns or missiles | G or middle mouse button |
| Chaff | H |
| Shield forward / aft (press again for even) | Z / X |
| Scoreboard | Hold Tab |
| Arming screen (loadout; in a match, also on the Escape menu) | L |
| Power to weapons / shields / engines, even split | Up / Left / Right arrow, Down arrow |
| Full power to engines / shields / weapons | 1 / 2 / 3 |
| Free look | hold right mouse button |
| Look at target (cockpit view: head turns to your current target, within neck limits) | hold Left Alt |
| Photo mode (press to hide the HUD, chat, banner, scoreboard and version stamp; press again to show them; a chip names the key for 2 s; the out-of-bounds countdown and the missile warning still show, and no sound is muted; the cockpit stays, the match keeps running and nothing is saved; a new match starts with it off) | P |
| Ready (warm-up) / pause / field of view / chat / bug report | Enter / Esc / [ ] / (chat key) / F5 (not rebindable) |

Every row above except the last, the mouse buttons and the free-look mouse button is rebindable in Settings, Keys (five pages: flight; throttle, boost, fire, drift, scoreboard; targeting, weapons and shields; power, look at target, view and photo mode; full power and fire mode). Pad and stick buttons for the same actions (next target, nearest target, guns or missiles, chaff, shield forward, aft and cycle, scoreboard, boost, fire, drift, free look, look at target, photo mode, throttle, hat) are in Settings, Buttons (four pages), and axes in Settings, Axes. A key or button already in use swaps with the one you change.

Default pad buttons: 0 boost, 10 drift, 1 guns or missiles, 2 nearest target, 3 next target, 4 free look, 5 fire, 6 shield balance cycle, 7 shield aft, 8 scoreboard, 9 shield forward, 11 chaff, 12 look at target. Default stick buttons: 0 fire, 1 boost, 10 drift, 2 guns or missiles, 3 chaff, 4 shield balance cycle, 5 nearest target, 6 next target, 7 scoreboard, 8 shield forward, 9 shield aft, 11 look at target. Button numbers differ between devices; run `--input-debug` and rebind in Settings.

Logs: the client writes `logs/client.log` in its settings folder (`%APPDATA%\FoundationThree\logs\` on Windows), keeping the last three runs (`client.1.log`, `client.2.log`); console output, panics and every handshake step go there. A server logs its own join steps to stderr (journald on a VM). See `JOIN_PATH.md` for the lines and the `F3-` join error codes.

Instruments (cockpit and chase views both show them): cyan bar = shield, green bar = hull, amber bar =
guns ready (it drops as you fire and refills), white bar under it = boost fuel (red while recharging, amber while drifting), left round gauge =
speed (red ticks are the boost range), right round gauge = throttle setting (green mark = half, where you turn tightest; set it with R/F),
the score lamps on the canopy header (green yours, red the enemy's), the coloured lamp = loaded weapon (red = laser, blue = ion),
and a 3D radar scope: the centre is you, forward is up the scope, range rings at 333, 667 and 1000 m, the
red blip is the enemy and its stalk shows whether it is above or below your nose. A yellow bracket marks the
enemy in the main view. The pale crosshair is where your guns converge; the green ring is the lead pip: put the crosshair on the pip and your shots will meet the enemy (it only shows within bolt range). The window title still shows speed, health and range as a backup.

## Gamepad, joystick and HOTAS
Plug it in before or while the game runs. Default pad layout: left stick = yaw and pitch, right stick X = roll,
triggers = throttle (right up, left down), A = boost, right bumper = fire. Other joysticks use their own
axis and button numbers: stick X roll, stick Y pitch, twist yaw, button 1 fire, button 2 boost, and a separate
throttle device's first axis as the lever. Untested on real hardware so far, so run
`f3_client --input-debug` to print each device and the axis/button numbers it sends, then edit
`data/input/default_profile.json` (device names there match as substrings of `hotas stick: <name>`,
`hotas throttle: <name>` or `gamepad: <name>`).

**Free look** (cockpit view): hold the left bumper (button 4) on a gamepad and the stick moves your head instead of the ship (fire and throttle stay live); let go and the head eases back. On a HOTAS the stick's hat looks around in the cockpit and moves focus in the menus. Phill's stick sends its hat as four buttons (65244 up, 65246 down, 65247 left, 65245 right; the order is a guess from `--input-debug`), so those are the defaults; rebind them in Settings, Controls, Buttons (Hat up, down, left, right, then press the hat direction). A stick that reports its hat as two axes can use `look_yaw` / `look_pitch` axis bindings in the profile instead (`DeviceBindings::set_hat`), and the menu follows those too. Right Mouse still works on the mouse.

## Windows build from CI
Open the latest CI run on GitHub, download the `f3-windows-build` artifact, unzip, run `f3_client.exe`.
In the release zip, double-click `f3_client.exe`; it opens the game at the main menu (it does not join a server).
Keep the `data` folder next to the exe: ships and input bindings load from it.

## Build from source
```
cargo run --release -p f3_client
```
Needs Rust (https://rustup.rs) and a GPU with Vulkan, DirectX 12 or Metal.

## Screenshot without a window
```
cargo run --release -p f3_client -- --screenshot out.png --seconds 6 --size 1280x720
```

## Record telemetry from a fight
```
cargo run --release -p f3_client -- --telemetry fight.csv
```
Writes one CSV row per simulation tick (60 per second) while you play: range, and for you (`p_`) and the bot (`b_`) position, speed, pitch/yaw/roll rates and their combined `rate_dps` in degrees per second, stick commands after clamping, fire, shield, hull, alive, how far the other ship is off your nose, plus the damage each side dealt that tick. Used to tune turn rates and combat from real play.

## Speed and turning
**Drift:** hold Space at cruise speed or faster (fighter: 95 m/s up; boost kicks in over about 0.2 s). Thrust cuts, the nose turns about 1.6 times faster than normal and the ship keeps sliding the way it was going (it swings round to the nose over about 3 s, losing 25 m/s each second down to a floor). It spends the boost reserve at three quarters of the boost rate, so a 1 s boost leaves about 2.7 s of drift, then the usual recharge. Let go and the ship swings back to flying where the nose points. Starts only above a ship's `drift_min_speed`; a ship with no `drift_turn_scale` in its JSON cannot drift. Profiles saved before drift existed move Fire from Space to Left Control.

Speed costs agility. Turn rate peaks at half throttle or slower, and the smallest turn circle is at low speed (about 30 m at half speed, 85 m at full cruise). Boosting gives a big acceleration burst to 190 m/s but cuts turn rate to about half, so the circle opens out to roughly 320 m. Throttle is a live control (R/F, or a lever or trigger on a stick or pad): drop it to win a turning fight.

## Dedicated server (early)
```
cargo run --release -p f3_server -- --config server.json
```
`server.json` is optional; every field has a default (listens on 127.0.0.1 only, so local play raises no firewall prompt; `--bind 0.0.0.0` or `"bind": "0.0.0.0"` opens it to other machines; port 27015, 2 players, 60 Hz, `interceptor`, map `arena`, first to 5 kills, 8 s result screen). Unknown or bad fields stop startup with an error naming the field. The server runs the whole sim, publishes the ship and weapon JSON it loaded to each client, and sends snapshots; clients can only send flight commands and the trigger. 
### Playing on it
```
cargo run --release -p f3_server -- --bind 0.0.0.0        # on the host, open to other machines
cargo run --release -p f3_client -- --connect HOST[:27015]   # each player
```
The client is the same cockpit and HUD as the sandbox, with your ship flown locally (prediction, so controls answer at once) and the other pilot drawn about 50 ms behind the newest snapshot. The window title shows the connection state and round trip. Firing, damage, kills and bolts come from the server, so your own bolts appear after about half a round trip (predicted tracers are not in yet). How prediction and hit rewind work, and their measured budgets, are in [NETCODE.md](NETCODE.md). Two players per server for now; a third is refused.

### Choosing a ship (story #57)
`server.json` can offer several ship classes: `"ship_pool": ["interceptor", "dreadnought"]` (ids from `data/ships` or the mods folder; `ship` is the default and must be in the list; an empty pool offers only `ship`; with no `ship_pool` in `server.json` the default is `["fighter", "bomber"]`). A pilot asks for one with `f3_client --connect HOST --ship <class>`; with no `--ship` they get the default. A class the server does not offer is refused with the list of choices, and the refused client takes no slot. The welcome carries the match settings (map, kills to win, player cap, respawn delay, classes on offer, and the class you were given); the window title shows them as `arena, first to 5, you fly interceptor (also: dreadnought)`. `server.json` also takes `respawn_delay_s` (default 5) and `spawn_protection_s` (default 5): seconds after a respawn in which the pilot takes no damage, ended early by firing; capital ships are not protected (`crates/f3_server/src/config.rs`). `"launch_sequence": true` makes a pilot who dies respawn through their frigate's launch rail (about 11 s, `CAPITAL_SHIPS.md`); it needs a map with frigates. Left out, it is on for a two-team server of up to 10 pilots (Quick Fight, 5v5) and off for bigger ones such as 8v8 and for a server with a coach seat (lessons respawn instantly); `false` turns it off anywhere.

### Matches
A match (a round, on the scoreboard) ends when a pilot reaches `kills_to_win` kills (25 unless set; with teams, the team's total). Dogfight options in `server.json`, all off unless set: `time_limit_s` (30 to 7200; a round nobody has won by then ends and the pilot or team with more kills wins, equal is a draw), `friendly_fire` (true or false; bolts and missiles also hit your own team, needs `teams` of 2 or more, and a friendly kill scores nothing) and `barge_safe_zone_m` (0 to 2000 metres; a team's pilots take no damage within that radius of their own indestructible barge, so it needs a map with barges such as `arena` and `teams` 2; 0 is off). Bot pilots are off unless set: `bot_mode` (`fill`: bots seat up to `bot_count` pilots, 0 meaning every seat, and each player who joins takes a bot's seat; `hold`: `bot_count` bots, 1 to `max_players` minus 1, stay seated for good and players get the remaining seats), and `bot_rank` (`cadet`, `ensign`, `lieutenant` (the default), `commander` or `wing_commander`, or `mixed`, which seats bots of every rank in turn; the skills are `data/bot_ranks/<rank>.json`, which a mods folder may replace). Bots fly through the same input path as players, are listed on the scoreboard with their rank and a House callsign, and do not count as players in the server browser. They fight other pilots only, never capital ships. What a pilot can see is `interest_radius_m` (100 to 1000000 metres; unset shows everything). Ranges are checked at start and a bad value names the field.

Between matches the whole world stops (protocol 31). The server holds the result for `match_over_s` seconds (10 unless set; nothing moves, nobody can score; the window title shows VICTORY or DEFEAT). It then builds the next match from scratch: every ship is rebuilt from its definition (full hull, shields, ammo, power), the barges and frigates are new, scores are zero, teams are dealt again by skill, and each side is lined up at its spawn, behind its capital ships in a team match. Pilots keep their seat, ship class and parts. Each person is sent the next map (`ServerMsg::MapChange`: the map name and their new ship index) and answers `MapReady` once it is loaded; the server waits for everyone for at least 1 s and at most 20 s (a client that never answers is dropped with a message), then runs a 10 s count-in with the ships held and the controls locked, and the match goes live. Parts can still be changed in the loading and count-in. The ready-up warm-up only starts the first match.

`map_pool` in `server.json` (up to 16 map names, same rules as `map`) makes the server go round those maps in order, one per match, starting on `map` (which is added at the front if it is not listed). Without it, or with a pool of one, every match replays `map`. Order and state live in `crates/f3_server/src/core.rs` (`start_next_match`, `rebuild_world`, `run_handover`); the client switches map in `follow_map_grade` when `NetSession` changes `settings().map`.

### Mods folder (data only)
`mods/` next to the server (or `mods_dir` in `server.json`) layers on top of `data/`: a `ships/*.json` or `weapons/*.json` with the same name replaces the built-in one, a new name adds one, and `maps/<name>.json` supplies a map (`"map": "<name>"` in `server.json`). Clients receive the ship and weapon JSON the server is running, so a modded ship needs no client change. Only `.json` files are read; a file that looks like code (`.dll`, `.so`, `.exe`, `.lua`, `.py`, `.js`, `.wasm` and similar) anywhere in the folder stops the server with an error naming it. Ships, weapons and maps may carry `"schema_version": 1` (a missing field means 1); a file with a newer version than the game reads is refused with a message saying so, so a mod never half-loads (see `crates/f3_sim/src/schema.rs`). A map is `{"name": ..., "spawn_points": [{"position": [x, y, z], "facing_deg": yaw}, ...]}`; pilot *i* spawns at point *i*. A map may add `"grade": {"exposure": 0.0, "vignette": 0.12, "lut": "name", "lift": [0,0,0], "gamma": [1,1,1], "gain": [1,1,1], "auto_exposure": {"enabled": true, "key": 0.016, "strength": 0.5, "min_stops": -1.0, "max_stops": 0.75, "brighten_rate": 0.7, "darken_rate": 2.0}}` (every field optional, missing means neutral): the picture's colour mood, read by the client only. `auto_exposure` is eye adaptation (on unless `"enabled": false`): it measures the picture's brightness (log-average luminance, centre weighted) and moves exposure against it by `strength` of the difference from `key`, never beyond `min_stops` (negative) and `max_stops`, easing at `brighten_rate` (when the view gets darker) and `darken_rate` (when it gets brighter) per second. `lut` names `luts/<name>.png` (in `mods/` or `data/`), a 1024 x 32 PNG strip of 32 slices of 32 x 32 (slice `b` at x = 32b, red along x, green down); when it is set `lift`, `gamma` and `gain` are ignored. A map may also add `"sky": {"nebula_up": [0.2, 0.24, 0.42], "nebula_down": [0.07, 0.05, 0.05], "sun_glow": [0.55, 0.42, 0.3], "sun_colour": [1.0, 0.93, 0.8], "sun_intensity": 3.2, "sun_dir": [-0.55, 0.65, 0.35]}` (linear colours, every field optional): the nebula and sun that light ships and show in their reflections, built into a cube map when the map loads. `sun_dir` points toward the sun. A map with no `sky` block keeps the built-in nebula. A `sky` block may also name `"skybox": "<folder>"`: the folder under `data/skybox/` (or `mods/skybox/`) holds `sky.f3sky`, a packed cube map from `f3_packer --skybox`; the faces replace the nebula dome. Placement is `"sky": {"skybox": "ventry"}` inside the map's top-level object, next to the other `sky` fields, never at the top level of the file. Over a skybox the `sky` block may also draw `"sun_disc": {"radius_deg": 0.9, "intensity": 3.5}` (a disc toward `sun_dir` in the sun's colour; `intensity` is its scene-linear brightness, so bloom and auto exposure react to it) and `"stars": {"seed": 7, "density": 0.07, "brightness": 0.5, "bright_share": 0.04, "band": 0.5, "band_axis": [0.3, 1.0, 0.2]}` (a seeded procedural star field with a few brighter stars and a faint galactic band, every field optional; `band_axis` turns the band, `seed` changes which stars appear). The sun disc is off unless the block names it. Stars are on by default on every map: a `sky` block with no `stars` key draws the default field (also on a map with no `sky` block), and only an explicit `"stars": null` turns them off. A still of a sky heading: `f3_client --screenshot out.png --look-map NAME --look-yaw DEG --look-pitch DEG` turns the cockpit head by those angles (degrees, yaw to the right, pitch up) for the whole still. `f3_client --look-map sky_test` shows the sandbox in the grade and sky of `data/maps/sky_test.json` (a copy of the arena with a violet nebula and low warm sun) so the sky path can be seen and timed; the arena itself is unchanged. Test: `crates/f3_server/tests/match.rs`.

### Tuning stick and pad feel (story #43)
Each device axis in the input profile has `deadzone`, `expo` (0 linear to 1 cubic), `invert` and `sensitivity` (gain after the curve, 0.1 to 5, capped at full deflection; default 1). `f3_client --profile data/input/stick_sensitive.json` loads the same bindings with sensitivity 1.2 so the two can be compared in a fight. `--telemetry fight.csv` now also records `p_raw_pitch`, `p_raw_yaw` and `p_raw_roll` (what the device reported) beside `p_cmd_*` (the shaped command), so a feel problem can be traced to the curve or to the hands. In a scripted test (`crates/f3_input/tests/aiming.rs`) a person-like aimer, 0.25 s late with 1.5 degrees of angle error, held the target in its sights about 5% of the time on the default profile through either device, close to what it manages with direct command access, so the curves were not the main loss. Against the old circling bot sensitivity 1.5 looked like it more than doubled that, but against the bot that breaks off (story #44) the two profiles are equal within noise (about 7.7%), so the scripted aimer cannot rank them and only a real hand can.

### Bot levels (story #44)
`f3_client --bot easy|normal|hard` picks the sandbox bot. Levels are data: `data/bots/<level>.json` (reaction time, aim wander, turn cap, firing cone, throttle, and when it breaks off), so a new file is a new level. A bot that is close to its target and cannot get it onto the nose for 1.5 s slows to turn while it tries, then gives up the turn, boosts away in a straight line and comes back round, instead of circling for minutes. Against a person-like pilot (`crates/f3_sim/tests/bot_duel.rs`, 10 duels of up to 400 s): the longest standoff is 3 to 7 s (it was 108 s), and the bot's share of the kills goes 0% (easy), 0% (normal), 67% (hard). Duels do end, but slowly: the first kill takes about 110 s (hard) to 250 s (normal) because two fallible pilots only deal about 4 damage per second against 12 per second of shield regeneration; that is a balance call for the lead.

### Shadows
The sun throws shadows from ships, rocks and the cockpit frame (story #123). Video screen: Shadows Off / Low / High (default High); `f3_client --shadows low` overrides it for one run and `--screenshot` honours it. Details and budgets in docs/PERFORMANCE.md.

### Lens flare

Looking toward the sun gives a lens flare that fades when a ship, rock or the canopy covers the sun (story #127). Video screen: Sun lens flare; `f3_client --flare off` turns it off for one run.

### Fog

Maps may carry a `fog` block (colour, density, noise_scale, shaft_strength). Settings, Video, Fog (or
`--fog off|low|high`) sets the quality; Off restores the plain haze. `sky_test` has fog for looking at it.

### Field of view
The cockpit view is 80 degrees vertical (about 111 across on a 16:9 screen; it was 65) and the chase view 75 (was 60). `[` and `]` narrow and widen it by 5 degrees while flying (limits 50 to 120), and `--fov 90` sets the cockpit value at launch. Above about 95 the cockpit shell fills the edges of the screen, so the useful range is roughly 70 to 95; screenshots at 65, 80 and 95 are in project files `screenshots/fov_*_v2.png`.

### Performance
On a networked client (`--connect`) `--telemetry` records one row per tick too: your ship from the prediction, the other pilot from the interpolated mirror, damage dealt each way read from health drops. The other pilot's stick and trigger are not known to a client and read 0 (their throttle comes from the server).
`--telemetry fight.csv` also writes `fight.csv.frames.txt` when the game closes: CPU time per frame (p50, p99, worst), the gap between frames, and the last frame's triangles and draw calls. Budgets are in `docs/PERFORMANCE.md`.

### Anti-aliasing
The main pass uses 4x MSAA by default so hard low-poly edges do not shimmer. `--msaa 1` turns it off (2x is not offered: the half-float scene format only guarantees 1 and 4); the same flag works with `--screenshot`. Comparison frames are in project files `screenshots/msaa1_v1.png` (off) and `msaa4_v1.png` (4x).

### Bloom and glow
Laser bolts, engine glow and trail sparks are emissive and bloom: a soft halo around anything much brighter than the lit scenery. `--bloom off` turns the glow pass off (it still copies the frame), also with `--screenshot`. Comparison frames are in project files `screenshots/bloom_on_11s_v1.png` and `bloom_off_11s_v1.png`. Art can make anything glow with one per-instance number (`Instance::emissive`).

### Frame pacing

`--present mailbox|fifo|immediate|auto` picks how frames reach the screen and `--latency 1|2|3` how many frames the driver may queue. The default is `mailbox` with latency 1 and a frame cap at the monitor's refresh rate (`--fps-cap N|off` overrides; the cap applies to Mailbox only unless you set it); Mailbox falls back to Fifo where the driver lacks it. On the lead's 144 Hz PC Fifo hitched by whole refreshes (frame gap p99 14-28 ms at latency 1-3, scaling with the latency) while capped Mailbox paced evenly. With `--telemetry` the `.frames.txt` starts with the present mode, latency, monitor refresh rate and the average frame rate.

### Menu framework test screens

`f3_client --ui-demo` opens three test screens (a main menu, a settings page and a list) that use every widget. Drive them with the mouse, the keyboard (arrows or WASD, Enter or Space to use, Esc to go back), a gamepad (d-pad or left stick, A, B) or a HOTAS (hat and the first two buttons). Typing into the pilot name field needs a keyboard. If your stick's hat does nothing, run `--input-debug`, note the axis or button numbers, and tell me: they are plain fields in `NavBindings`. See `docs/UI.md`.

### Menus, settings and pause

The game opens on the main menu (practice against the bot, join a server by address, settings, quit) over the live scene. Esc (or a gamepad's Start) pauses during a match; Resume, Settings, Leave match and Quit are there, with the score. Pausing never stops the simulation or a server: the ship just stops getting your stick, rudder, boost and fire while a menu is open, and the mouse is released.

Joining a server asks for an address, connects, and opens the server's lobby without taking a seat: the server describes itself (map, kills to win, player limit, classes on offer, pilots seated, refreshed every two seconds), you pick a class from its list (the first is its default) and press Take a seat. The client then sends `Hello` with that class, and when the server seats you the menu closes and you fly. A refusal shows the server's reason with only Leave. `--connect` still seats you at once (with `--ship` if given). Snapshots go only to seated pilots, so a lobby visitor costs the server nothing per tick.

Settings: mouse sensitivity and invert, per-axis deadzone, response curve, sensitivity and invert for each gamepad or stick axis, key rebinding (a key already in use swaps with the one you change), gamepad and stick button rebinding (press the button on the device itself), field of view, anti-aliasing, glow and interface size. They are saved to your config folder (`%APPDATA%\FoundationThree` on Windows, `~/.config/foundation-three` elsewhere) as `settings.json` and `profile.json` when you leave a settings screen, and read back at start. A hand-edited bad file is an error naming the field. `--profile file` still overrides your saved profile for one run.

**Server list.** Join a server shows the live dedicated servers (name, map, players/max, ping the client measures itself with a UDP probe; wrong-version servers are greyed with their version; unreachable ones say "No answer") above the address box. Using a row goes to that server's lobby. It refreshes when the screen opens, every 10 s, and with Refresh list. It works on a background thread, so the menu never waits, and if the list is empty, down or not set up the address box and Connect still work. The client asks the project's public listing service by default (`DEFAULT_REGISTRY_URL`); change it with `registry_url` in `settings.json` or `--registry https://...` for one run, and set `"registry_url": "off"` to turn the list off. Ship classes show in the lobby after picking a server (the listing does not carry them).

`--connect host` and `--telemetry file` skip the menu and fly at once, as does `--no-menu`.
