A mcrfpy.Timer stamped its epoch (last_ran) from GameEngine::runtime -- an
sf::Clock reading wall time -- at creation and every control method, while
GameEngine::step() ticks timers against simulation_time. The two clocks are
in unrelated frames, so a timer's FIRST fire under step() depended on
process-startup wall timing: headless replay was nondeterministic. Steady
state looked fine because the first fire re-stamps last_ran into the
simulation frame; only the first fire was contaminated.
Add a timer_now() helper that returns getSimulationTime() when headless and
runtime otherwise, and route all 9 epoch reads in PyTimer.cpp through it.
Windowed mode is unchanged; headless timers are now a pure function of the
step() sequence.
Surfaced by the #381 screenshot oracle: timer-driven snippets 048 and 191
rarely flipped their PNG. Post-fix both are byte-identical across 15 runs
and the full snippet oracle is 272/272 stable; suite 621/621.
Regression test asserts exact fire counts and callback runtimes across three
scenarios (interval boundaries, deterministic first fire, one-shot).
closes#383
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTh2ZW7bd3XSd9qK86Z2CE