Skip to content

Capture: after Studio closes or crashes mid-batch, it offers to capture the clips the game never got to (#20, part of #545) - #1171

Merged
ccoventry merged 7 commits into
devfrom
feat/capture-recovery-20
Oct 30, 2026
Merged

ccoventry merged 7 commits into
devfrom
feat/capture-recovery-20

Conversation

@ccoventry

Copy link
Copy Markdown
Owner

Release note: New: if DoD Studio closed or crashed during a capture batch and the game didn't finish it, Studio offers to capture the clips that are missing the next time it opens.

Closes #20
Part of #545 (part 2's catch-up is finished here; part 4, the dev-build job object, is still open).

What changes

#760 already caught up on a batch that ran while Studio was closed: the batch's manifest (manifests\session_*.json, outcome planned) records each highlight's id, its demo and the project, and the next start checks the takes and marks them Captured. This PR adds the parts that were still missing.

  1. Clips the batch never got to. When the game closed or crashed before the end, some clips have no take. After the catch-up, Studio asks once:

    Capture the rest?
    A capture batch stopped before the end while DoD Studio was closed. 1 of 4 clips are on disk and marked Captured; 3 clips were not captured.

    Capture them now? Studio opens the project run3.dodstudio, ticks just those highlights and starts a capture batch.

    [Capture them now] [Not now]

    (With nothing on disk: "...while DoD Studio was closed, and none of its 4 clips are on disk.")
    Capture them now opens the batch's project (the unsaved-changes prompt first, if another one is open), ticks only the missing highlights, and runs Start Capture Batch with all its usual checks.

  2. A half-recorded clip is no longer counted as captured. When the game is killed mid-clip, HLAE stops the video and sound.wav at the same moment. A take that FFmpeg stops writing mid-clip still counts as a full capture #1105's video-vs-sound check can't see that, and the catch-up has no markers to go by, so the partial take used to count as Captured and was never offered again. HLAE fills in the wav's data size when a recording stops, so a size still unset over real sound means the clip never finished. Such a take is now "cut short when the game closed", left on disk and offered for capture. A watched batch already tells partial takes apart from its markers (A clip recording when the game closes is no longer marked Captured; Last Batch says the game closed (part of #1081, #1105) #1124).

  3. While the game may still be capturing. When Studio reopens with the game still open, it waits for the game to close before it checks (as before), and now says so with a toast: "The game may still be capturing a batch from before DoD Studio closed. Studio checks its clips and marks them Captured once the game closes." Until then, Start Capture Batch refuses with: "Not started: the game may still be capturing a batch from before DoD Studio closed. Wait for the game to close, or close it, and start again." Without the refusal, a new batch would run in that open game (Start Capture Batch runs in the game that is already open instead of relaunching it (#725) #1075) and stop the old one.

  4. Tidy-up. The stray .autosave.json removal in capture_engine.rs goes (nothing ever wrote that file; the manifest is the recovery record), and docs/features.md describes recovery (3.5) and the manifest's outcomes (section 9).

Where

  • studio/src/batch_resume.js (new) + tests: which highlights have no take, ticking just those, the question and the start.
  • studio/src/batch_catch_up.js: the waiting state (isBatchStillRunningInGame), the toast, the offer after the newest unfinished batch.
  • studio/src/capture_pane.js: the refusal as the first check of Start Capture Batch, and startCaptureFromOffer (the Start button's click, every check included).
  • studio/src-tauri/src/batch_catch_up.rs: unfinished takes; the end-of-wait event is sent even when it found nothing, so the refusal always lifts.
  • native/src/hlcr/take_length.rs: is_unfinished_take; the wav header parsing is shared with wav_seconds.
  • main.js: two arguments on the existing catch-up wiring.

Decisions

  • No separate .autosave.json lockfile. The planned manifest already is one: it is written before the game starts, it lists every clip with its highlight ids and project, and it is rewritten when the batch ends. A second file would only repeat it. ([Capture Studio] Autosave lockfile recovery #20 asked for the lockfile; the owner's 2026-09-28 note said recovery means resending the rest of the schedule, which this does.)
  • No live reconnect to a game still running. Studio waits for the game to close, as Studio marks clips captured after a batch that ran while it was closed (part of #545) #760 did. Reading the game's events pipe would make the hook think Studio saw BATCH_COMPLETE, so the game would no longer quit itself (A batch in a game that's already open links up to the game at once, not at its first marker #1126), and the live progress state (block correlation, markers, ETA) was in the Studio that closed. The hook ends the game about 5 s after the batch, so the marks come at most a few seconds later than a reconnect would give.
  • One question, for the newest unfinished batch. Older ones are still marked and toasted, but only one "Capture the rest?" is asked per start.
  • A batch with no project file is only offered when its highlights are in the open queue (at start-up it is empty, so in practice only batches started from a project are offered).
  • The refusal is a check after the click, like the other Start checks, not a greyed-out button. It is rare, it lifts by itself when the game closes, and the footer line says why.
  • "The game may still be capturing" is worded as "may": the wait starts for any open hl.exe, including one the user started themselves.
  • Unfinished takes are judged only by HLAE's sound.wav. OBS and AGR takes have none, so a killed-game partial take there still counts as captured (as before).

Test done

  • rustup run 1.98.1 cargo fmt --all --check: clean. Clippy (1.98.1, workspace minus goldsrc-hooks and web-analyzer, --all-targets -D warnings): clean. cargo test --workspace --locked --exclude goldsrc-hooks --exclude web-analyzer after merging dev: all pass (desktop-studio 289, native 1035; new: 3 in take_length, 1 in batch_catch_up).
  • npm run test:unit: 1114 passed (new: 11 in batch_resume.test.js, 5 in batch_catch_up.test.js; main_js_budget passes). Playwright app-boot.spec.js + batch-results.spec.js: 13 passed.
  • Live, POST, Brain account (checked before each launch, game lock held), my own Studio build with its own settings folder (DS_LIVE_APPDATA, never committed; the user's settings.json hash a6fa3173794f9c2f before and after). Direct to video, 1280x720 at 120 fps, Seek Between Clips, hook DLL built from dev. Four 3k highlights (BAR 13:09, 14:17, 16:47, 29:23) from wsod25_grp5_h1_m00cat.dem on D:.
Run What I did What happened
1 (a) Closed Studio (WM_CLOSE, "Close DoD Studio") as clip 2 started; reopened at once with the game still capturing The game kept capturing. On reopen: the "may still be capturing" toast; Start Capture Batch (another project open) refused with the line above. The game quit itself 5 s after BATCH_COMPLETE (hook log batch_end ... quitting the game); 1 s later: "4 of 4 clips are on disk. 4 highlight(s) marked Captured. Marked them in the project run1.dodstudio." The project file on disk: the 4 highlights Captured, the 4 takes in its take index. Start no longer refused.
2 (b) Killed Studio by its PID as clip 2 started; reopened after the game had finished The game survived and finished; it quit itself. On reopen: "4 of 4 ... marked Captured ... run2.dodstudio"; file checked as in run 1.
3 (resume) Killed Studio, then the game, 6 s into clip 2 (a 27 s clip) Before the unfinished-take fix the catch-up said 2 of 4 (the 10 s partial take counted). With it: activity log "...chain_01_b1 was cut short when the game closed: not counted as captured. Its partial take is left at ...", toast "1 of 4", then "Capture the rest?" (1 of 4, 3 not captured). Capture them now: run3 opened, the batch's manifest had exactly the 3 missing highlights, the batch ran: "All 3 takes verified on disk. 3 highlight(s) marked Captured." Saved: all 4 Captured; the partial take is not in the take index.

Also checked with a hand-made planned manifest whose takes don't exist: "none of its ... clips" wording, and Not now leaves everything as it was (manifest rewritten unwatched, so it isn't asked again).

Not tested: PRE (no hook or engine change here), OBS/AGR takes, a batch with no project file, the "Capture them now" path when another project with unsaved changes is open (it goes through Open Recent's own prompt).

Notes

  • The close prompt's local-build note ("started from npm run tauri dev: closing it closes the game too") also shows for a debug exe started directly, where the game survives. Not changed here (A capture batch outlives DoD Studio closing, and Studio catches up when it reopens #545 part 4).
  • Seen while testing, not from this PR: two Studios running at once share the dodstudio-analyzer pipe, so one Studio answered another one's game ("Analysis server: the game asked for ...").

🤖 Generated with Claude Code

ccoventry and others added 5 commits October 30, 2026 00:56
…d hold Start while the old batch may still run (#20)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…autosave.json removal, document recovery (#20)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…isk (#20)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t counted as captured (#20)

The game and HLAE stop the video and sound.wav at the same moment, so the
video-vs-audio check can't see it; the wav's data size left unset can.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry ccoventry linked an issue Oct 30, 2026 that may be closed by this pull request
The offer can come at any time, when the old batch's game closes. A
second themedConfirm replaces the open one and its caller never hears
back, so the offer waits until no dialog is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccoventry

Copy link
Copy Markdown
Owner Author

Review (review-night)

Verdict: ready to merge, now that f3e8b11 is pushed. I found no false positives on good takes. One dialog collision is fixed.

is_unfinished_take on good takes

  • Real takes: I ran the PR's own function (a temporary example, not committed) over every HLAE sound.wav on this machine. That is 55 takes:

    • every D:\dodstudio-* test folder,
    • C:\DICEWSOD25\...\02_HLAE_Raw,
    • the repo's local\, excluding the two folders Studio's settings point at.

    54 read as finished. The one "unfinished" take is D:\dodstudio-545\takes\session_20261030_013346\dodstudio_chain_01_b1\take0000, the partial take from this PR's run 3 (10.4 s of a 27 s clip), which is the case it is meant to catch.

  • Over 4 GB: a filled-in size can't be 0 and can't exceed what follows the header (it wraps, or saturates at 0xFFFFFFFF, under the real length), so it isn't flagged. A clip would need hours of 16-bit stereo to get there anyway.

  • WAVE_FORMAT_EXTENSIBLE, LIST/fact/JUNK before data: data_chunk_in walks chunks by their sizes (pad byte included), so it doesn't matter how big fmt is or what comes before data. Chunks after data leave the size below what follows the header, so those aren't flagged either.

  • No data in the first 4 KB, RF64, not RIFF/WAVE: None, read as not unfinished.

  • FFmpeg-written wavs: a seekable FFmpeg output fills the size in. A non-seekable one (size 0xFFFFFFFF) would be flagged, but take folders only hold HLAE's own sound.wav, and the check runs only on take folders.

  • OBS and AGR: no sound.wav, so never judged.

  • Older takes and blocks already rendered: only blocks in a planned manifest being caught up are checked, and rendered blocks are skipped.

  • Startup cost: one header read (4 KB) per captured block of each planned manifest, and only when there is one. It doesn't grow with the size of the library.

The refusal

It holds only while the backend is waiting, which needs both an open hl.exe and a planned manifest from before Studio closed. It lifts when that wait's event comes, which is now sent even when it found nothing. If the listener never registered, it never holds.

It can block a game the user opened themselves for something else. That happens when Studio last closed with a planned manifest left behind (a crash, or a batch that died after the manifest was written) and the user starts Studio with their own game open. The footer says to close the game, and a batch started in that game (#1075) would mix with the unchecked one anyway. The PR documents this ("may"); I left it alone.

Switching projects

openProject is Open Recent's route: the unsaved-changes prompt first, false on cancel (then nothing starts). Its rescan offer is awaited inside loadProjectFile, so it never overlaps the batch's own checks.

Fix pushed (f3e8b11)

Problem: the offer is the first themedConfirm nobody clicked for. It can arrive at any moment: when the old batch's game closes, or at start-up beside an open-from-Explorer load. A second themedConfirm overwrites the open one, so that caller's promise never resolves and the user's click answers the offer instead.

Fix:

  • themed_confirm.js exports isThemedConfirmOpen().
  • batch_resume.offer waits (500 ms polls) until no dialog is open before asking.
  • One new test.

Ran

  • npm run test:unit: 1115 passed.
  • Playwright app-boot + batch-results: 13 passed.
  • cargo test -p native --lib take_length: 10 passed.

I did not run the game: the take check is HLAE's wav writer, which is the same on both installs, and the 54 real finished takes above cover the false-positive question.

Nit

The refusal's "or close it" could name what to close. "the game" is right for every case it fires in, so this isn't blocking.

🤖 Generated with Claude Code

@ccoventry
ccoventry merged commit 3947502 into dev Oct 30, 2026
5 checks passed
@ccoventry
ccoventry deleted the feat/capture-recovery-20 branch October 30, 2026 08:14
ccoventry added a commit that referenced this pull request Oct 30, 2026
Import-line conflicts in batch_catch_up.js and capture_pane.js (#1171's
batch resume) keep both sides. batch_resume.js, new from #1171, takes its
file name from fileNameOf like everything else, so shared_helpers_only.test.js
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Capture Studio] Autosave lockfile recovery

1 participant