Repository navigation
Add an opt-in headless fixed-step simulation mode, seeded RNG, On Ship Damage hook and batch runner - #7874
Add an opt-in headless fixed-step simulation mode, seeded RNG, On Ship Damage hook and batch runner#7874BTawaifi wants to merge 5 commits into
Conversation
…uses -seed only seeded the main RNG. RandomRange instances (seeded from a separate generator before tables parse) and the std::random_device shuffles of weapon firepoints, asteroid splits and random-message senders stayed unseeded, so two runs with the same seed could diverge from the first frame. - Random::seed_ranges() seeds the generator that RandomRange instances draw from. - game_init() seeds both families when -seed is given, before table parsing. - Random::shuffle_seed() gives each std::shuffle its generator seed: a draw from the seeded main RNG under -seed, OS entropy otherwise (normal gameplay is unchanged). The ship firepoint, asteroid-split and message-sender shuffles use it instead of three separate std::random_device sites.
Runs after resolved damage has been applied to a ship (lethal hits run before the destruction callback). Hook variables: Target, Source, Attacker, WeaponClass, WeaponSubtype, SourceKind (PROJECTILE, BEAM, SHOCKWAVE, COLLISION, OTHER), DamageType, ShieldDamage, HullDamage, Quadrant, Ballistic, BeamWeapon and LethalHit. Conditions can filter on the target ship. Nothing is computed unless a script has registered the hook.
tools/headless_batch/run_batch.py runs many headless engine processes in parallel, one mission, workspace, seed and UDP port per process, and collects each run's -sim-result file into results.jsonl and summary.json. A run counts as ok only if the engine exited 0 and wrote a readable result; crashes, missing results and timeouts are recorded as failures, and a timed-out process tree is killed. Plans are validated up front: unknown keys, out-of-range values, a missing pilot or mod folder, a base workspace and output directory on different volumes (the .vp packs are hardlinked), and, on Windows, an output directory so deep that the engine could not open a staged file are all errors that say what to change. Workspaces hardlink .vp packs and copy everything else so a run does not write through to the base. Supports --dry-run and --resume. README.md documents the headless options, what runs and what is skipped, the plan format, outputs, reproducibility and known limits. test_run_batch.py covers validation, command construction, staging, the volume and path checks, and a stub engine (success, crash, missing result, timeout) and needs no game files.
New command-line options:
-headless no window, renderer, audio, speech, joystick, HUD/radar or
movies; implies -unthrottled, -noninteractive and -nomovies.
Graphics runs on the stub renderer, and model
geometry/collision data still load.
-unthrottled run frames as fast as the CPU allows, with a virtual 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) when the engine shuts down.
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).
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). A failed commit ends the run with a warning and the result says
"start_failed". The debriefing is left by quitting, so most single-player missions can be run
without a person present.
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.
afterburners_start()/afterburners_stop() measure the afterburner cooldown (and the player's re-engage delay) with timer_get_milliseconds(), the real clock. An unthrottled run simulates many seconds per real second, so a cooldown lasted far longer in simulated time and varied with machine speed and load. Of the wall-clock uses found in the simulation sources searched (not the whole engine), this was the one that affects gameplay, and a possible source of run-to-run variation between same-seed runs. Under -unthrottled these sites now use timestamp(), the virtual mission clock. Without the flag they still call timer_get_milliseconds().
2d63ffe to
64c1d68
Compare
|
I expect this would need some of the bugfixes in #7834. Wait until that goes in first. Also, @dustinspace217 has put together another FSO harness for testing. Let's see if we can reconcile the different designs. |
|
Interesting. I'll see if there's anything this build does better than mine and adopt it if so. Mine has gotten quite a bit of testing and fine tuning at this point. Mine is primarily designed for agents, but can be driven fully from CLI. |
|
Ran a comparison between this PR and my harness (reviewed with Claude, two models independently, claims checked against the engine source). Tl;dr: Mine is better at flying like a human does and drives the engine from the outside on stock builds. This one is better at fast, structured, repeatable runs, but needs engine changes. They sit at different layers, so I think they fit together rather than compete.What this PR does better
Issues I found (each checked in source)
Requests
Where mine is aheadLive control and game-state reads during a run, screenshots and renderer comparison, qtFRED, multiplayer, crash reports and validation, a Lua safety check, hard resource limits and network isolation, and an agent interface (MCP and CLI). It runs on stock release builds today. Suggested way forwardFor the maintainers to weigh in on: splitting this into separate PRs might make review easier.
Once those are in, I'll add support for them to my harness: a seed option, a headless launch mode, a batch command, and the damage hook in its telemetry. On folding the harness into the SCP org: I think it should be its own repo, so downloading it isn't tied to the engine. It also needs Linux today (but I'm working on that), while the engine builds everywhere. The batch runner could live here in tools/ or with the harness, whichever the maintainers prefer. |
|
This is used to accelerate the autoplay of missions and/or get more logging out of what happens in the engine. In RTS fleet command, you have 2+ factions that coordinate the warping and complex orders and tactics of ships and sudo wings, and to tune it; it had to be replayed hundreds of times, I have tools that read the traces and find out for example did the Capital spawn in 20 mins in because the cost of the ship was too low, and how much damage it did to the base so the damage done to the base by that ship or beam is reduced or adjusted, we need to know how far the turrets shoot because you don't want a turret blowing another base thousands of space units away, so the hooks are needed for deeper tracing and error logging. I think the layer that @dustinspace217 is building will provide much needed stability testing and agent driving can unlock stuff like skirmishes or entire generated campaigns from the player perspective that goes beyond simple mission generation and much more, having the logging and speed unlocked on demand will allow the reviewers to do much more per unit of time. Do take what you see is most useful from the PR, Good luck O7. |
|
Sorry for the confusion here @BTawaifi - I actually think you should craft the layered PRs I mentioned that will build the hooks into the engine. That part was firmly your idea and you deserve the contribution credit (and your agents have the context needed to easily make it happen). Just wait for the maintainers to approve the approach. Once those hooks are built, I can build in ways for my harness to use them. |
|
No worries @dustinspace217 and thank you for building out the harness! The engine change doesn't conflict with your work; rather, it adds another layer that the harness can leverage. Since I’m not currently focusing on multiplayer engine work, I’ll keep reporting any issues I spot as I test my mod. |
|
Ok, I've gotten #7834 reviewed and merged, so this PR can now build on it. I also agree with the suggestion to split this into several PRs, each focusing on a different enhancement. |
|
Thanks both. Now that #7834 is in, I've split this into separate PRs as suggested:
Changes from this PR, mostly from @dustinspace217's review:
I'll close this PR in favour of the split ones. |
What this adds
An opt-in headless, fixed-step simulation mode for running missions unattended and many times, plus the pieces it needs. Nothing changes unless the new flags are passed.
New command-line options:
-headless-unthrottled,-noninteractive,-nomovies.-unthrottled-fixed-dt <s>-max-sim-time <s>-sim-result <path>In a headless run
-start_missiongoes straight through: fiction and command briefing are skipped, red-alert missions pass through the red alert screen, the default loadout is committed throughcommit_pressed(), and the debriefing is left by quitting. A failed commit ends the run and the result saysstart_failed. Modal popups are logged and answered with the abort answer instead of waiting for input (unless a script handles On Dialog Frame).Commits
-seedpreviously seeded only the main RNG. It now also seedsRandomRangeinstances and the threestd::shufflecall sites (ship firepoints, asteroid splits, random message senders), which used a freshrandom_deviceeach time. Without-seedthey behave exactly as before.isActive()lookup per damage event.tools/headless_batch/): a standard-library Python script that runs many headless engine processes in parallel (one workspace, seed and UDP port each) and collects the results. Includes a README and unit tests that need no game files.-unthrottled.Behaviour without the new flags
I read each hunk with that question in mind; the additions are single boolean checks on the normal path (timer read, frame timing,
radar_plot_object,popup()), the hook lookup above, and the unchangedrandom_deviceseeding for the shuffles. No windowed gameplay regression test was run.Testing
python -m unittest tools/headless_batch/test_run_batch.py(16 tests).end-missionends the run at the debriefing (termination: mission_exit), and four runs with seeds A, A, B, B gave identical end-state fingerprints for the equal seeds and different ones for the different seeds (short, quiet mission).START_GAME -> RED_ALERT -> ENTER_GAME -> GAME_PLAYin headless mode.Known limits
-unthrottledwithout-headlessfreezes menu timers and breaks multiplayer timing; it is meant to be used with-headless.-max-sim-time.-(for example-seed -1) as an unknown option and shows its existing message box, which blocks a headless run. Unchanged upstream behaviour; documented in the README.