Repository navigation
Conversation
New command-line options:
-headless no window, renderer, audio, speech, joystick, HUD/radar or
movies; implies -noninteractive and -nomovies. Graphics
runs on the stub renderer, and model geometry/collision
data still load. Frames run as fast as the CPU allows, on
a mission clock that advances only by simulated time
(wall-clock APIs are unchanged for profiling and UI
deadlines). The clock starts at one hour and never moves
backwards, so timestamps never collide with the "never"
and "immediate" values.
-fixed-dt <s> authoritative step in seconds (default 1/30, range
[1/65536, 0.25]).
-max-sim-time <s> end the run after this much simulated time (at most
32767; larger values overflow the fixed-point time type and
are rejected).
-sim-result <path> write a JSON result (mission, seed, simulated and wall
seconds, why it stopped, and the state of each mission
goal) when the engine shuts down.
The unthrottled clock is not a separate option: on its own it would freeze menu
timers and break multiplayer timing, so it only comes with -headless.
Script frame callbacks (On Frame) normally run from gr_flip(); they are split
out as gr_process_frame_callbacks() so headless runs still execute them once per
simulated frame. Camera and photo-mode interpolation are skipped (presentation
only). Keyboard and joystick input is not read, but read_player_controls() still
runs, so scripted (Lua) flight control, player physics and the player warp-out
sequence work as usual.
Unattended single-player runs: -start_mission normally stops at the fiction,
command briefing, red alert and briefing screens and at the debriefing, waiting
for a player. Headless runs skip the fiction and command briefing, enter a
red-alert mission through the red alert screen as its Continue button does, and
otherwise go to the briefing state and take the default loadout through
commit_pressed() (the same call the engine makes for missions flagged
no-briefing). The debriefing is left by quitting. The death popup cannot be
answered, so once the player's ship has blown up the run ends. The result's
"termination" is time_limit, start_failed (the default loadout could not be
committed), player_died, or mission_exit.
Modal popups cannot be answered in a headless run, so popup() logs the text and
returns the abort answer instead of waiting (unless a script handles On Dialog
Frame). Without this a first launch of a mod with no settings file, or any
loadout-commit failure, hangs until the caller's timeout.
With -noninteractive, Error() and Warning() already print their text to stderr before they abort or return, so the reason is visible without a dialog. A Lua error only threw LuaErrorException, which ends the process as an unhandled exception, and release builds write no log, so an unattended run that hit a Lua error (for example a syntax error in a script table) gave no indication why. The Lua error text and traceback now go to stderr first, as for Error().
Use nullptr for the new -headless parameter, and mark the two range checks that are written as !(in range) on purpose: the suggested De Morgan form (< min || > max) would accept NaN.
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split out of #7874, now on top of #7834. Works best with the afterburner cooldown fix (#7895) merged first, since afterburner cooldowns otherwise run on the real clock while headless runs simulate many seconds per real second.
An opt-in mode for running single-player missions unattended and fast, for balancing, regression and AI experiments. Nothing changes unless
-headlessis passed.-headless-noninteractiveand-nomovies.-fixed-dt <s>-max-sim-time <s>-sim-result <path>time_limit,player_died,start_failed,mission_exit) and the state of each mission goal.The fast clock is not a separate option: on its own it would freeze menu timers and break multiplayer timing.
What runs: the normal authoritative frame (
game_frame(),game_simulation_frame()), so objects, AI, weapons, collisions, goals and scripting all run.On Framecallbacks normally fire fromgr_flip(); they are split out asgr_process_frame_callbacks()and run once per simulated frame. Keyboard and joystick input is not read, butread_player_controls()still runs, so scripted (Lua) flight control, player physics and the player warp-out sequence work.Unattended flow: with
-start_mission, the fiction and command briefing are skipped, a red-alert mission goes through the red alert screen as its Continue button does, the default loadout is committed throughcommit_pressed()(as for no-briefing missions), and the debriefing is left by quitting. A failed commit ends the run (start_failed). The death popup cannot be answered, so the player's death ends the run (player_died). Other modal popups are logged and answered with the abort answer, unless a script handles On Dialog Frame.Second commit: under
-noninteractive, Lua errors are now printed to stderr before the exception ends the process, asError()already does. Release builds write no log, so before this a Lua error (e.g. a syntax error in a script table) ended an unattended run with no indication why.Without
-headlessthe additions are single boolean checks on the normal path (timer read, frame timing,radar_plot_object,popup(), the player-controls block).Testing (retail
sm1-01, Release unless noted):end-missionfrom a script, and the player killed at 10 s end astime_limit,mission_exitandplayer_died; without-max-sim-timea player death now ends the run in 1 s (before: never)-headlessturns the player ship (heading dot 0.39 after 10 s, vs 1.00 without steering and 1.00 on Add an opt-in headless fixed-step simulation mode, seeded RNG, On Ship Damage hook and batch runner #7874's build)'end' expected ... near '<eof>'on stderrFSO_FATAL_WARNINGS=ONon Windows (MSVC) and Linux (gcc-13, clang-16), and the unit tests passKnown limits: single player only; without
-start_mission(or a script driving the menus) the engine waits at the main hall; long runs with many AI ships can still diverge for equal seeds.