Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Environment variables

Every NOVA_* variable the game reads, in one place: what it gates, which crate owns it, and who it is for. This page is the INDEX. It does not restate what each variable means - the page that owns the mechanism does that, and the link in each row goes there.

Nothing type-checks an environment variable. Renaming one compiles clean and fails at load, or - worse - silently stops arming something and the run looks fine while measuring nothing. So there is one rule, and tests/env_contract.rs enforces it:

  • Every variable is a named constant, declared once, in the crate that owns the behaviour it gates. No bare literals at a use site in crates/ or src/. The measurement knobs are computed from one prefix by nova_probe::probe_env, so the host that pushes a name into a child run and the child that reads it back cannot disagree.
  • The test is also a roster. It walks crates/, src/ and tests/ for NOVA_* literals and fails on anything it does not name, so a new variable cannot arrive undeclared.
  • examples/ keeps its literals on purpose. A range that drifts turns a probe run red, which is detection shipped code does not have (AGENTS.md Nova behavior).

Three names are spelled twice, deliberately: nova_gameplay’s mute policy, nova_scenario’s Screenshot action and the probe sandbox each need a name another crate owns, and a dependency edge from a shipping crate to a dev-tooling one - or from the host harness to the whole asset stack - is worse than the repetition. The contract test asserts each pair equal.

Who the columns are for

  • harness - set by a run script, by CI, or by probe. A player never sees it, and an unarmed run pays nothing for it.
  • tooling - set by a test or a contributor’s shell to keep a run off the real profile on disk.
  • player - usable on a normal cargo run or a shipped build.

The harness: what drives a run, and where its pictures go

Owned by nova_autopilot. The mechanism is The automation harness, which carries the values and the stall semantics.

VariableGatesFor
NOVA_AUTOPILOTarms the scripted state driver; unset, the plugin adds nothing. Also makes the settings store INERTharness
NOVA_AUTOPILOT_DEADLINEseconds before the completion watcher error-exits naming the laggardsharness
NOVA_CAPTUREputs a script on its CAPTURE path - its shot beats write PNGs, its loops record. Also makes the settings store INERTharness
NOVA_CAPTURE_DIRdirectory relative capture paths stage under; absolute paths ignore itharness

NOVA_CAPTURE arms the SHOTS, never a driver, so a capturing run sets NOVA_AUTOPILOT too and one script owns the window. NOVA_CAPTURE_DIR is also read by the scenario Screenshot action, which is the in-game photo-mode lever rather than a harness one.

Either of the two also silences the run and pins its settings. SettingsStorePlugin::from_env reads the same pair the mute does (HARNESS_ENVS, through nova_gameplay::harness_env_active) and goes INERT: no load at startup, no save on change, no window mode applied. So a scripted run flies on the DEFAULTS whatever the launching player has saved - which is what makes a capture the same picture on every machine - and cannot write that player’s settings.ron. A wasm build has no environment to read, so an app that must be inert there (nova_perf_web) adds the plugin itself with access: SettingsStoreAccess::Inert.

Inert is the far end of a three-way access, not a flag. A normal app gets Read: it boots on the player’s saved settings and cannot write them back. NovaMenuPlugin raises that to ReadWrite when it builds the settings panel, which is the only surface that asks for a value to be kept. An inert store is never raised, so a scripted run that happens to build a menu still writes nothing.

Measurement: what a run records about itself

Owned by nova_probe, all under one prefix, all inert unless set. The full table - defaults, units, and the wasm URL-query twin of each - is the crate’s own rustdoc (cargo doc --open -p nova_probe), and Measuring performance is what to read before quoting a number from any of them.

VariableGatesFor
NOVA_PROBEarms frame-time capture, the scene census and the frame-cost breakdownharness
NOVA_PROBE_MODEcorrectness drops the measuring passes from a child runharness
NOVA_PROBE_WARMUP / _FRAMESthe capture window, in frames; wins over an example’s declared oneharness
NOVA_PROBE_OUTdirectory the run writes frametime.csv, <label>.json and census.json intoharness
NOVA_PROBE_LABELthe row label a capture records itself underharness
NOVA_PROBE_RESforced primary-window resolution for the measured runharness
NOVA_PROBE_RENDER_SCALEforces the render-scale lever, holding the rest of the quality preset fixedharness
NOVA_PROBE_MAX_DELTAceiling on how many fixed steps one frame may runharness
NOVA_PROBE_PRESENTpresentation mode forced on the primary windowharness
NOVA_PROBE_QUALITYgraphics preset for the run, recorded in the metadataharness
NOVA_PROBE_SCENARIOthe scenario a sweep cell measuresharness
NOVA_PROBE_SHA / _HOSToverride the recorded git SHA and host tagharness
NOVA_PROBE_CENSUS_FRAMEframes after Playing at which the scene census is takenharness
NOVA_PROBE_FRAMECOST_FRAMESframes averaged into one frame-cost reportharness
NOVA_PROBE_RENDER_DIAGasks the renderer for GPU timestamp queries, so passes can be namedharness
NOVA_PROBE_TIMELINEJSONL path for the run timeline: states, events, variables, markersharness
NOVA_PROBE_INVARIANTSarms the continuous engine-bound invariant checksharness
NOVA_PROBE_CONTRACTJSON path the run declares its wired capabilities toharness
NOVA_PROBE_SNAPSHOT / _SNAPSHOT_FRAMESJSONL path for world-state snapshots, and the frames to take them atharness
NOVA_PROBE_STEPDIAGCSV path for the per-fixed-step physics diagnosticsharness
NOVA_PROBE_STEPDIAG_BODIESthe body-count REGIME floor its end-of-run summary is taken overharness
NOVA_PROBE_SANDBOX_RESOLVER_CHILDmarks the re-executed child in the probe host’s own sandbox testharness

NOVA_PROBE_RENDER_DIAG is declared in nova_core rather than nova_probe: the wgpu feature can only be requested where RenderPlugin is built, and nova_core is the lowest crate both name.

Outputs off

One variable per output device, and a matching debug-only flag on the game binary. An example has no command line of its own, which is why the environment half exists at all. See Building and running.

VariableFlagGatesOwnerFor
NOVA_NORENDER--norenderevery AppBuilder::new() in the process assembles a headless app: no device, no window, no winitnova_coreharness
NOVA_MUTE--mutezeroes the audio OUTPUT; the volume setting is untouchednova_gameplayplayer

NOVA_MUTE unset still mutes a run that has NOVA_AUTOPILOT or NOVA_CAPTURE set; NOVA_MUTE=0 forces sound through one. The flag wins over both, and a muted run says nova audio: output muted for this run once.

The agent bench: an agent at the channel

Owned by nova_bench. The mechanism is The agent bench.

VariableGatesFor
NOVA_BENCH_SOCKETthe unix socket the referee answers on; bench play sets it for the agent process, and a cmd: agent reads itharness
NOVA_BENCH_PIthe pi binary bench play --agent pi spawns; unset, pi on the pathharness

The bench sets NOVA_SEED on the child game from --seed, and points the child at an empty profile under the run directory, so a play never reads or writes the settings on disk.

The replay seed

Owned by nova_gameplay, next to the entropy plugin it seeds.

VariableGatesFor
NOVA_SEEDseeds the gameplay RNG with one u64, so a driven run replays byte for byte; unset, the OS seeds it and no two runs agreeharness

A value that does not parse as a u64 refuses the boot rather than running unseeded - a replay that silently lost its seed is the failure the knob exists to prevent.

Modding, and the settings store

Owned by nova_assets. See /create/publish-a-mod/ for the portal.

VariableGatesFor
NOVA_MODDING_CACHE_ROOTmoves the local mod cache off the platform data dirtooling
NOVA_MODDING_PORTAL_URLpoints a native build at another portal treetooling
NOVA_CONFIG_ROOTmoves the settings store off the platform config dirtooling

NOVA_CONFIG_ROOT is deliberately NOT in the modding family. It is the settings store root, and its name is already right.

It is the store root for a whole PROCESS, so it is a tooling lever, not a test one: a test that repoints it repoints every test running beside it. A test names its own root on SettingsStorePlugin instead.

The menu

VariableGatesOwnerFor
NOVA_MENU_BACKDROPpins the first backdrop of each menu entry to one role: Backdrop scenario id; the rotation continues from there; an unknown id warns and falls back to the drawnova_menuharness

Not on the roster

  • Example-local knobs. NOVA_STRESS_PD_*, NOVA_EDITOR_FRAMELOG, NOVA_SOAK_SCENARIO, NOVA_SOAK_SECS, NOVA_VFX_RANGE_BARE_SLUG, NOVA_RAILGUN_AFTERMATH, NOVA_RAILGUN_LIVE, NOVA_WAKE_LOOP, NOVA_COLLAPSE_LOOP, NOVA_SIGHT_LOOP and NOVA_STANDOFF_TRACE belong to one example each and stay literals beside the range that reads them. The two railgun names pick which of the shipped loops screenshot_railgun records - the aftermath cut, and the real-speed pass against the default slowed clock - and scripts/capture-web-media.sh sets them. Neither goes wrong quietly. NOVA_RAILGUN_AFTERMATH is a duration: unset it falls back to the producer’s own window, and a value that is not a positive number of seconds panics. A row that forgets NOVA_RAILGUN_LIVE=1 records the SLOWED loop name, so the live row’s file never appears - and the script rm -fs that file before the run and hard-checks it after, so the capture aborts naming the missing loop, having overwritten the already-staged slowed row on the way.

    The three *_LOOP names put one range each on its RECORDING path instead of its reading one, and scripts/capture-web-media.sh sets them. NOVA_WAKE_LOOP=1 records railgun_wake_bench’s close pass as news-0130-railgun-wake instead of walking the stills. NOVA_COLLAPSE_LOOP=1 records stress_hull_collapse’s collapse as news-0130-hull-collapse; the range still checks its invariants, but the loop pins the clock, so that run’s millisecond claims are the recorder’s and not the collapse’s. NOVA_SIGHT_LOOP=1 records system_lock_line_of_sight’s drifting cover as news-0130-lock-occlusion and runs no assertions at all. Each takes 0 or 1, and any other value is an authoring error that panics rather than picking a path.

    NOVA_STANDOFF_TRACE is a diagnostic and not a mode: set to anything, it adds loop_goto_standoff’s per-leg print - the plan the computer publishes beside what the hull does, five times a second of world time - and changes nothing the producer records.

  • NOVA_OS_*. Around 180 of these exist and NONE is an environment variable: they are const Color, layout and volume values in nova_interface, nova_command and nova_gameplay::audio. A grep for NOVA_[A-Z_]* is dominated by them, so count env::var call sites instead of identifiers.

  • Shell-only. NOVA_UI_PORT, NOVA_GAME_PORT, NOVA_MODS_PORT, NOVA_MODS_DIR, NOVA_PORT_LO/_HI are read by scripts/ and web/webpack.config.js. No Rust reads any of them.

    The media packagers own two more. NOVA_UNFREEZE takes a comma-separated prefix list and re-opens the frozen news- names of the post being authored (NOVA_UNFREEZE=news-0130), in scripts/gen-web-screenshots.py for stills and scripts/capture-web-media.sh for loops; without it a name a post already ships is never overwritten. NOVA_REUSE_STAGE=1 tells scripts/capture-web-media.sh to package a completed target/loop-shots set rather than re-run the producers, which is the repackage path after a capture that finished.

  • Retired. NOVA_MENU_PATH is read by nothing. examples/ui/editor.rs owns the create-a-ship-and-Play sequence end to end, so system_menu_boot dropped the branch; the only surviving mention is that example’s module doc saying the branch is gone.

  • Foreign variables the code legitimately reads: DISPLAY, WAYLAND_DISPLAY, RUST_LOG, BEVY_ASSET_ROOT, CARGO_*, CI, XDG_DATA_HOME, XDG_CONFIG_HOME, LVP_ICD, RUSTFLAGS, VK_ICD_FILENAMES, VK_DRIVER_FILES, WGPU_BACKEND, TRACE_CHROME.

Adding one

  1. Declare it as a named constant in the crate that owns the behaviour it gates. A measurement knob belongs in nova_probe, never in a gameplay plugin - the four fixed-step knobs that lived in NovaGameplayPlugin, one of them able to panic! on a malformed value, are the worked example of getting this wrong.
  2. Export it through that module’s prelude.
  3. Add it to the roster in tests/env_contract.rs. The test fails until you do, which is the point.
  4. Add a row above.