From 2ed5eed26121563e1397dc74d08752fd840a2cb4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 09:07:22 -0400 Subject: [PATCH] feat(godot): --shot-sequence per-frame capture for verifying paced animation Dumps one PNG per _Process frame (auto-advancing past input waits), so a time-based sleep-paced effect can be verified as distinct frames. Confirmed the SC0000 opening now steps through paced AE*/character/CG frames instead of jumping straight to the final state. Co-Authored-By: Claude Opus 4.8 --- docs/tools-reference.md | 1 + godot/Main.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/tools-reference.md b/docs/tools-reference.md index d11f105..75f51a3 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -103,6 +103,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after - `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue. - `--boot` — run SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` before the scene, so scene-assumed boot state (chiefly INIT2's gfx handle array) is present. **Needed for the gfx CGs to render** (without it the opening event CGs collapse/drift). e.g. `godot --path godot -- --boot`. - `--shot [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames. +- `--shot-sequence [--frames N]` — dump one PNG per rendered frame (`frame_0000.png…`, default N=180 ≈ 3s @60fps) then quit, auto-advancing past input waits. Verifies **time-based (sleep-paced) effects** — e.g. the opening `AE*` burst — as distinct frames, which a single `--shot` cannot. CPU/IO-heavy by design (a PNG every frame); a dev diagnostic, not a normal run. e.g. `godot --path godot -- --boot --shot-sequence out/seq --frames 300`. ## Asset resolution / graphics diff --git a/godot/Main.cs b/godot/Main.cs index abd1c21..b209008 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -30,6 +30,9 @@ public partial class Main : Godot.Control private int _shotSettleTarget = 3; // --shot-settle : settle N frames before grabbing (to // capture mid-tween — the anim clock keeps running while parked) private bool _shotDone; + private string? _seqDir; // --shot-sequence : dump one PNG per frame (verify paced anim) + private int _seqFrames = 180; // --frames : how many frames to dump (default ~3s @60fps) + private int _seqIdx; public override void _Ready() { @@ -87,6 +90,8 @@ public partial class Main : Godot.Control if (userArgs[i] == "--shot" && i + 1 < userArgs.Length) _shotPath = userArgs[i + 1]; if (userArgs[i] == "--shot-page" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _shotPage); if (userArgs[i] == "--shot-settle" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _shotSettleTarget); + if (userArgs[i] == "--shot-sequence" && i + 1 < userArgs.Length) _seqDir = userArgs[i + 1]; + if (userArgs[i] == "--frames" && i + 1 < userArgs.Length) int.TryParse(userArgs[i + 1], out _seqFrames); if (userArgs[i] == "--seed" && i + 1 < userArgs.Length) { var kv = userArgs[i + 1].Split('='); @@ -128,12 +133,26 @@ public partial class Main : Godot.Control // --shot: auto-advance up to (but not past) the target page, then _Process captures + quits. if (_shotPath != null) _ = Task.Run(async () => { while (!_done) { if (_host.IsWaiting && _host.Pages < _shotPage) _host.SignalInput(); await Task.Delay(1); } }); + // --shot-sequence: auto-advance past every input wait so the paced burst isn't blocked on a click. + if (_seqDir != null) + _ = Task.Run(async () => { while (!_done) { if (_host.IsWaiting) _host.SignalInput(); await Task.Delay(1); } }); } public override void _Process(double delta) { _lastDelta = delta; if (!_selftest && _vm != null) Recomposite(); // retained per-frame compositor (surface+object model) + // --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be + // verified as distinct frames, not just the final state. Captures after Recomposite; quits when full. + if (_seqDir != null && _seqIdx < _seqFrames && !_done) + { + System.IO.Directory.CreateDirectory(_seqDir); + var fimg = GetViewport().GetTexture().GetImage(); + fimg.SavePng($"{_seqDir}/frame_{_seqIdx:0000}.png"); + _seqIdx++; + if (_seqIdx >= _seqFrames) { GD.Print($"SEQ saved {_seqIdx} frames -> {_seqDir}"); GetTree().Quit(0); } + return; + } // --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it. if (_shotPath != null && !_shotDone && (_host.Pages >= _shotPage && _host.IsWaiting || _done)) {