picopilot 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audio.d.ts +17 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +315 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/export.d.ts +36 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +257 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/gfx.d.ts +28 -0
- package/dist/commands/gfx.d.ts.map +1 -0
- package/dist/commands/gfx.js +521 -0
- package/dist/commands/gfx.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +151 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +20 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +90 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/minify.d.ts +20 -0
- package/dist/commands/minify.d.ts.map +1 -0
- package/dist/commands/minify.js +129 -0
- package/dist/commands/minify.js.map +1 -0
- package/dist/commands/music.d.ts +13 -0
- package/dist/commands/music.d.ts.map +1 -0
- package/dist/commands/music.js +241 -0
- package/dist/commands/music.js.map +1 -0
- package/dist/commands/playtest.d.ts +29 -0
- package/dist/commands/playtest.d.ts.map +1 -0
- package/dist/commands/playtest.js +631 -0
- package/dist/commands/playtest.js.map +1 -0
- package/dist/commands/run.d.ts +30 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/serve.d.ts +50 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +213 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/sfx.d.ts +13 -0
- package/dist/commands/sfx.d.ts.map +1 -0
- package/dist/commands/sfx.js +254 -0
- package/dist/commands/sfx.js.map +1 -0
- package/dist/commands/tokens.d.ts +36 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +129 -0
- package/dist/commands/tokens.js.map +1 -0
- package/dist/commands/verify.d.ts +57 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +213 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +24 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/engine/audio/index.d.ts +14 -0
- package/dist/engine/audio/index.d.ts.map +1 -0
- package/dist/engine/audio/index.js +14 -0
- package/dist/engine/audio/index.js.map +1 -0
- package/dist/engine/audio/music.d.ts +116 -0
- package/dist/engine/audio/music.d.ts.map +1 -0
- package/dist/engine/audio/music.js +138 -0
- package/dist/engine/audio/music.js.map +1 -0
- package/dist/engine/audio/section.d.ts +34 -0
- package/dist/engine/audio/section.d.ts.map +1 -0
- package/dist/engine/audio/section.js +67 -0
- package/dist/engine/audio/section.js.map +1 -0
- package/dist/engine/audio/sfx.d.ts +141 -0
- package/dist/engine/audio/sfx.d.ts.map +1 -0
- package/dist/engine/audio/sfx.js +506 -0
- package/dist/engine/audio/sfx.js.map +1 -0
- package/dist/engine/cart/cart.d.ts +75 -0
- package/dist/engine/cart/cart.d.ts.map +1 -0
- package/dist/engine/cart/cart.js +153 -0
- package/dist/engine/cart/cart.js.map +1 -0
- package/dist/engine/cart/errors.d.ts +29 -0
- package/dist/engine/cart/errors.d.ts.map +1 -0
- package/dist/engine/cart/errors.js +26 -0
- package/dist/engine/cart/errors.js.map +1 -0
- package/dist/engine/cart/gfx.d.ts +79 -0
- package/dist/engine/cart/gfx.d.ts.map +1 -0
- package/dist/engine/cart/gfx.js +171 -0
- package/dist/engine/cart/gfx.js.map +1 -0
- package/dist/engine/cart/index.d.ts +15 -0
- package/dist/engine/cart/index.d.ts.map +1 -0
- package/dist/engine/cart/index.js +15 -0
- package/dist/engine/cart/index.js.map +1 -0
- package/dist/engine/cart/map.d.ts +46 -0
- package/dist/engine/cart/map.d.ts.map +1 -0
- package/dist/engine/cart/map.js +118 -0
- package/dist/engine/cart/map.js.map +1 -0
- package/dist/engine/config/config.d.ts +74 -0
- package/dist/engine/config/config.d.ts.map +1 -0
- package/dist/engine/config/config.js +103 -0
- package/dist/engine/config/config.js.map +1 -0
- package/dist/engine/config/index.d.ts +7 -0
- package/dist/engine/config/index.d.ts.map +1 -0
- package/dist/engine/config/index.js +7 -0
- package/dist/engine/config/index.js.map +1 -0
- package/dist/engine/gfx/grid.d.ts +51 -0
- package/dist/engine/gfx/grid.d.ts.map +1 -0
- package/dist/engine/gfx/grid.js +116 -0
- package/dist/engine/gfx/grid.js.map +1 -0
- package/dist/engine/gfx/index.d.ts +17 -0
- package/dist/engine/gfx/index.d.ts.map +1 -0
- package/dist/engine/gfx/index.js +17 -0
- package/dist/engine/gfx/index.js.map +1 -0
- package/dist/engine/gfx/label.d.ts +34 -0
- package/dist/engine/gfx/label.d.ts.map +1 -0
- package/dist/engine/gfx/label.js +190 -0
- package/dist/engine/gfx/label.js.map +1 -0
- package/dist/engine/gfx/overlap.d.ts +42 -0
- package/dist/engine/gfx/overlap.d.ts.map +1 -0
- package/dist/engine/gfx/overlap.js +57 -0
- package/dist/engine/gfx/overlap.js.map +1 -0
- package/dist/engine/gfx/palette.d.ts +29 -0
- package/dist/engine/gfx/palette.d.ts.map +1 -0
- package/dist/engine/gfx/palette.js +46 -0
- package/dist/engine/gfx/palette.js.map +1 -0
- package/dist/engine/gfx/png.d.ts +31 -0
- package/dist/engine/gfx/png.d.ts.map +1 -0
- package/dist/engine/gfx/png.js +165 -0
- package/dist/engine/gfx/png.js.map +1 -0
- package/dist/engine/gfx/render.d.ts +42 -0
- package/dist/engine/gfx/render.d.ts.map +1 -0
- package/dist/engine/gfx/render.js +72 -0
- package/dist/engine/gfx/render.js.map +1 -0
- package/dist/engine/index.d.ts +22 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +22 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/pico8/adapter.d.ts +267 -0
- package/dist/engine/pico8/adapter.d.ts.map +1 -0
- package/dist/engine/pico8/adapter.js +37 -0
- package/dist/engine/pico8/adapter.js.map +1 -0
- package/dist/engine/pico8/drive.d.ts +308 -0
- package/dist/engine/pico8/drive.d.ts.map +1 -0
- package/dist/engine/pico8/drive.js +581 -0
- package/dist/engine/pico8/drive.js.map +1 -0
- package/dist/engine/pico8/harness.d.ts +146 -0
- package/dist/engine/pico8/harness.d.ts.map +1 -0
- package/dist/engine/pico8/harness.js +251 -0
- package/dist/engine/pico8/harness.js.map +1 -0
- package/dist/engine/pico8/index.d.ts +21 -0
- package/dist/engine/pico8/index.d.ts.map +1 -0
- package/dist/engine/pico8/index.js +21 -0
- package/dist/engine/pico8/index.js.map +1 -0
- package/dist/engine/pico8/sentinel.d.ts +63 -0
- package/dist/engine/pico8/sentinel.d.ts.map +1 -0
- package/dist/engine/pico8/sentinel.js +86 -0
- package/dist/engine/pico8/sentinel.js.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
- package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-entry.js +11 -0
- package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
- package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
- package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon-main.js +71 -0
- package/dist/engine/pico8/session-daemon-main.js.map +1 -0
- package/dist/engine/pico8/session-daemon.d.ts +68 -0
- package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
- package/dist/engine/pico8/session-daemon.js +264 -0
- package/dist/engine/pico8/session-daemon.js.map +1 -0
- package/dist/engine/pico8/session.d.ts +140 -0
- package/dist/engine/pico8/session.d.ts.map +1 -0
- package/dist/engine/pico8/session.js +250 -0
- package/dist/engine/pico8/session.js.map +1 -0
- package/dist/engine/pico8/shell.d.ts +103 -0
- package/dist/engine/pico8/shell.d.ts.map +1 -0
- package/dist/engine/pico8/shell.js +324 -0
- package/dist/engine/pico8/shell.js.map +1 -0
- package/dist/engine/pico8/supervisor.d.ts +137 -0
- package/dist/engine/pico8/supervisor.d.ts.map +1 -0
- package/dist/engine/pico8/supervisor.js +140 -0
- package/dist/engine/pico8/supervisor.js.map +1 -0
- package/dist/engine/scaffold/index.d.ts +8 -0
- package/dist/engine/scaffold/index.d.ts.map +1 -0
- package/dist/engine/scaffold/index.js +8 -0
- package/dist/engine/scaffold/index.js.map +1 -0
- package/dist/engine/scaffold/scaffold.d.ts +49 -0
- package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
- package/dist/engine/scaffold/scaffold.js +259 -0
- package/dist/engine/scaffold/scaffold.js.map +1 -0
- package/dist/engine/shrinko/adapter.d.ts +167 -0
- package/dist/engine/shrinko/adapter.d.ts.map +1 -0
- package/dist/engine/shrinko/adapter.js +107 -0
- package/dist/engine/shrinko/adapter.js.map +1 -0
- package/dist/engine/shrinko/index.d.ts +13 -0
- package/dist/engine/shrinko/index.d.ts.map +1 -0
- package/dist/engine/shrinko/index.js +13 -0
- package/dist/engine/shrinko/index.js.map +1 -0
- package/dist/engine/shrinko/shell.d.ts +69 -0
- package/dist/engine/shrinko/shell.d.ts.map +1 -0
- package/dist/engine/shrinko/shell.js +156 -0
- package/dist/engine/shrinko/shell.js.map +1 -0
- package/dist/engine/skills/index.d.ts +8 -0
- package/dist/engine/skills/index.d.ts.map +1 -0
- package/dist/engine/skills/index.js +8 -0
- package/dist/engine/skills/index.js.map +1 -0
- package/dist/engine/skills/skills.d.ts +104 -0
- package/dist/engine/skills/skills.d.ts.map +1 -0
- package/dist/engine/skills/skills.js +149 -0
- package/dist/engine/skills/skills.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +48 -2
- package/src/bin.ts +4 -0
- package/src/cli.ts +61 -0
- package/src/commands/audio.test.ts +280 -0
- package/src/commands/audio.ts +410 -0
- package/src/commands/export.test.ts +310 -0
- package/src/commands/export.ts +316 -0
- package/src/commands/gfx-io.test.ts +178 -0
- package/src/commands/gfx.test.ts +470 -0
- package/src/commands/gfx.ts +645 -0
- package/src/commands/init.test.ts +482 -0
- package/src/commands/init.ts +193 -0
- package/src/commands/lint.test.ts +137 -0
- package/src/commands/lint.ts +117 -0
- package/src/commands/minify.test.ts +166 -0
- package/src/commands/minify.ts +156 -0
- package/src/commands/music.test.ts +272 -0
- package/src/commands/music.ts +288 -0
- package/src/commands/playtest.test.ts +317 -0
- package/src/commands/playtest.ts +773 -0
- package/src/commands/run.test.ts +264 -0
- package/src/commands/run.ts +285 -0
- package/src/commands/serve.test.ts +227 -0
- package/src/commands/serve.ts +272 -0
- package/src/commands/sfx.test.ts +349 -0
- package/src/commands/sfx.ts +300 -0
- package/src/commands/tokens.test.ts +212 -0
- package/src/commands/tokens.ts +168 -0
- package/src/commands/verify.test.ts +238 -0
- package/src/commands/verify.ts +256 -0
- package/src/commands/version.test.ts +70 -0
- package/src/commands/version.ts +24 -0
- package/src/engine/audio/index.ts +47 -0
- package/src/engine/audio/music.test.ts +188 -0
- package/src/engine/audio/music.ts +218 -0
- package/src/engine/audio/section.test.ts +124 -0
- package/src/engine/audio/section.ts +79 -0
- package/src/engine/audio/sfx.test.ts +410 -0
- package/src/engine/audio/sfx.ts +694 -0
- package/src/engine/cart/cart.test.ts +304 -0
- package/src/engine/cart/cart.ts +211 -0
- package/src/engine/cart/errors.ts +39 -0
- package/src/engine/cart/fixtures/crlf.p8 +6 -0
- package/src/engine/cart/fixtures/full.p8 +174 -0
- package/src/engine/cart/fixtures/header-only.p8 +2 -0
- package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
- package/src/engine/cart/fixtures/minimal.p8 +10 -0
- package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
- package/src/engine/cart/gfx.ts +195 -0
- package/src/engine/cart/index.ts +27 -0
- package/src/engine/cart/map.ts +134 -0
- package/src/engine/config/config.ts +115 -0
- package/src/engine/config/index.ts +12 -0
- package/src/engine/gfx/grid.test.ts +128 -0
- package/src/engine/gfx/grid.ts +136 -0
- package/src/engine/gfx/index.ts +34 -0
- package/src/engine/gfx/label.test.ts +105 -0
- package/src/engine/gfx/label.ts +229 -0
- package/src/engine/gfx/overlap.test.ts +71 -0
- package/src/engine/gfx/overlap.ts +66 -0
- package/src/engine/gfx/palette.test.ts +35 -0
- package/src/engine/gfx/palette.ts +54 -0
- package/src/engine/gfx/png.test.ts +111 -0
- package/src/engine/gfx/png.ts +196 -0
- package/src/engine/gfx/render.test.ts +180 -0
- package/src/engine/gfx/render.ts +106 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pico8/adapter.ts +303 -0
- package/src/engine/pico8/drive.test.ts +415 -0
- package/src/engine/pico8/drive.ts +729 -0
- package/src/engine/pico8/harness.test.ts +181 -0
- package/src/engine/pico8/harness.ts +354 -0
- package/src/engine/pico8/index.ts +20 -0
- package/src/engine/pico8/sentinel.test.ts +80 -0
- package/src/engine/pico8/sentinel.ts +93 -0
- package/src/engine/pico8/session-daemon-entry.ts +12 -0
- package/src/engine/pico8/session-daemon-main.ts +98 -0
- package/src/engine/pico8/session-daemon.test.ts +212 -0
- package/src/engine/pico8/session-daemon.ts +306 -0
- package/src/engine/pico8/session.test.ts +239 -0
- package/src/engine/pico8/session.ts +321 -0
- package/src/engine/pico8/shell.test.ts +583 -0
- package/src/engine/pico8/shell.ts +412 -0
- package/src/engine/pico8/supervisor.test.ts +105 -0
- package/src/engine/pico8/supervisor.ts +197 -0
- package/src/engine/scaffold/index.ts +15 -0
- package/src/engine/scaffold/scaffold.ts +278 -0
- package/src/engine/shrinko/adapter.ts +241 -0
- package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
- package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
- package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
- package/src/engine/shrinko/index.ts +33 -0
- package/src/engine/shrinko/shell.ts +224 -0
- package/src/engine/shrinko/shrinko.test.ts +182 -0
- package/src/engine/skills/index.ts +14 -0
- package/src/engine/skills/skills.test.ts +376 -0
- package/src/engine/skills/skills.ts +205 -0
- package/src/index.ts +13 -0
- package/src/skills/game-design-reference/SKILL.md +70 -0
- package/src/skills/game-jam/SKILL.md +45 -0
- package/src/skills/picopilot-art/SKILL.md +62 -0
- package/src/skills/picopilot-audio/SKILL.md +194 -0
- package/src/skills/picopilot-code/SKILL.md +97 -0
- package/src/skills/picopilot-code/reference/README.md +42 -0
- package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
- package/src/skills/picopilot-code/reference/platformer.md +119 -0
- package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
- package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
- package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
- package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
- package/src/skills/picopilot-debug/SKILL.md +195 -0
- package/src/skills/picopilot-overview/SKILL.md +52 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `audio render` harness builder (ADR-0009 Part 3): the PURE, CI-testable
|
|
3
|
+
* core that turns a user's cart + a chosen render TARGET into a THROWAWAY cart
|
|
4
|
+
* whose `_update` plays the target, records it via `audio_rec`/`audio_end(1)`,
|
|
5
|
+
* and prints the done-sentinel. This is the audio analogue of the `run`
|
|
6
|
+
* screenshot harness (the `picopilot-debug` recipe): the author writes NO
|
|
7
|
+
* playback code and still gets "render sfx 5 -> a WAV".
|
|
8
|
+
*
|
|
9
|
+
* Kept pure (cart-text in, cart-text + a record window out) so the harness
|
|
10
|
+
* construction + the record-duration heuristic are unit-testable WITHOUT the
|
|
11
|
+
* paid PICO-8 binary; only the actual capture needs a live A/V session (the
|
|
12
|
+
* manual/opt-in tier, ADR-0009).
|
|
13
|
+
*/
|
|
14
|
+
import { Cart } from '../cart/index.js';
|
|
15
|
+
/**
|
|
16
|
+
* A render target: a single SFX slot, a single music PATTERN, or the whole song
|
|
17
|
+
* (pattern 0 onward). This mirrors the audio authoring vocabulary (`sfx from-mml`
|
|
18
|
+
* writes a slot; `music from-patterns` writes an ordered pattern list), so an
|
|
19
|
+
* author renders exactly what they composed.
|
|
20
|
+
*/
|
|
21
|
+
export type RenderTarget = {
|
|
22
|
+
readonly kind: 'sfx';
|
|
23
|
+
readonly index: number;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'pattern';
|
|
26
|
+
readonly index: number;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'song';
|
|
29
|
+
};
|
|
30
|
+
/** The lowest/highest SFX slot a render target may name (finding: 64 slots). */
|
|
31
|
+
export declare const SFX_TARGET_MIN = 0;
|
|
32
|
+
export declare const SFX_TARGET_MAX = 63;
|
|
33
|
+
/** The lowest/highest music PATTERN a render target may name (finding: 64 patterns). */
|
|
34
|
+
export declare const PATTERN_TARGET_MIN = 0;
|
|
35
|
+
export declare const PATTERN_TARGET_MAX = 63;
|
|
36
|
+
/**
|
|
37
|
+
* The default record window (seconds) for a target whose exact play length
|
|
38
|
+
* cannot be derived cheaply (a music pattern / whole song: PICO-8 does not store
|
|
39
|
+
* a per-pattern length, and following loops/flags statically is out of scope for
|
|
40
|
+
* a render harness). A generous window that captures a typical loop; the author
|
|
41
|
+
* overrides it with an explicit duration (ADR-0009: render is a real-time
|
|
42
|
+
* recording, so a fixed window is honest, not a promise of exact length).
|
|
43
|
+
*/
|
|
44
|
+
export declare const DEFAULT_RECORD_SECONDS = 8;
|
|
45
|
+
/** A small tail margin (seconds) appended so the target's final note is not clipped. */
|
|
46
|
+
export declare const RECORD_TAIL_SECONDS = 0.5;
|
|
47
|
+
/**
|
|
48
|
+
* A structured reason a render harness could not be built. Surfaced by the
|
|
49
|
+
* command as incur's `error({code,...})` envelope; never a silent guess.
|
|
50
|
+
*/
|
|
51
|
+
export type HarnessErrorCode =
|
|
52
|
+
/** The target named an SFX/pattern outside the valid 0..63 range. */
|
|
53
|
+
'audio-render-target-out-of-range'
|
|
54
|
+
/** The target SFX slot is empty (all-zero) in the cart: nothing to render. */
|
|
55
|
+
| 'audio-render-target-empty'
|
|
56
|
+
/** The cart could not be parsed (not a valid .p8). */
|
|
57
|
+
| 'audio-render-cart-parse-error';
|
|
58
|
+
/** A structured harness-build failure (a target refusal, not a crash). */
|
|
59
|
+
export declare class HarnessError extends Error {
|
|
60
|
+
readonly code: HarnessErrorCode;
|
|
61
|
+
constructor(code: HarnessErrorCode, message: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The decoded timing of one authored `__sfx__` row: its `speed` (ticks/row) and
|
|
65
|
+
* the number of NON-EMPTY leading rows (its play length in rows). Both come from
|
|
66
|
+
* the friendly text row (finding Part A): header bytes 2..3 = speed, then 32
|
|
67
|
+
* five-nibble notes; a note is silent when its volume nibble is 0.
|
|
68
|
+
*/
|
|
69
|
+
interface SfxTiming {
|
|
70
|
+
readonly speed: number;
|
|
71
|
+
readonly rows: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decodes a `__sfx__` text row into its {@link SfxTiming}. `speed` is header
|
|
75
|
+
* byte [2:4]; `rows` is the count of tracker rows up to and including the last
|
|
76
|
+
* one with a non-zero volume nibble (a trailing silent tail does not extend the
|
|
77
|
+
* audible length). Returns `rows = 0` for an all-silent row.
|
|
78
|
+
*/
|
|
79
|
+
export declare function decodeSfxTiming(row: string): SfxTiming;
|
|
80
|
+
/**
|
|
81
|
+
* The record window (seconds) for a render target, given the cart. For an SFX
|
|
82
|
+
* target the window is derived from that slot's speed + audible row count
|
|
83
|
+
* (`rows * speed` ticks / {@link PICO8_TICKS_PER_SECOND}) plus a tail margin, so
|
|
84
|
+
* the recording covers exactly the target's play length without guessing. For a
|
|
85
|
+
* pattern / whole-song target the length is not cheaply derivable, so the
|
|
86
|
+
* {@link DEFAULT_RECORD_SECONDS} window is used (overridable by the caller).
|
|
87
|
+
*
|
|
88
|
+
* An explicit `override` (the command's `--seconds`) always wins.
|
|
89
|
+
*/
|
|
90
|
+
export declare function recordSeconds(cart: Cart, target: RenderTarget, override?: number): number;
|
|
91
|
+
/** A one-line human description of the target (for the render result + help). */
|
|
92
|
+
export declare function describeTarget(target: RenderTarget): string;
|
|
93
|
+
/** The result of building a render harness: the throwaway cart + its record window. */
|
|
94
|
+
export interface RenderHarness {
|
|
95
|
+
/** The throwaway `.p8` cart text (same `__sfx__`/`__music__`, injected `__lua__`). */
|
|
96
|
+
readonly cartText: string;
|
|
97
|
+
/** The record window in seconds the adapter should let the capture run for. */
|
|
98
|
+
readonly seconds: number;
|
|
99
|
+
/** The number of `_update` frames the harness records for (= seconds * 30fps). */
|
|
100
|
+
readonly frames: number;
|
|
101
|
+
/** The base filename the harness passes to `extcmd("set_filename")`. */
|
|
102
|
+
readonly wavBasename: string;
|
|
103
|
+
/** A human description of what is being rendered (for the result envelope). */
|
|
104
|
+
readonly description: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Builds the throwaway render harness (ADR-0009 Part 3). Takes the user's cart
|
|
108
|
+
* text + a render TARGET (+ an optional record-window override) and produces a
|
|
109
|
+
* new cart with the SAME authored `__sfx__`/`__music__` but a REPLACED `__lua__`
|
|
110
|
+
* whose `_update`:
|
|
111
|
+
* - at frame 1: `extcmd("set_filename", <base>)`, `extcmd("audio_rec")`, plays
|
|
112
|
+
* the target (`sfx(N)` / `music(P)` / `music(0)`);
|
|
113
|
+
* - at frame `frames`: `extcmd("audio_end", 1)` (save to the current folder,
|
|
114
|
+
* which the adapter points at an isolated temp dir);
|
|
115
|
+
* - at frame `frames + 1`: `printh` the done-sentinel so the launcher kills
|
|
116
|
+
* PICO-8 promptly (a cart cannot self-quit, ADR-0006).
|
|
117
|
+
*
|
|
118
|
+
* @throws {HarnessError} an out-of-range/empty target or an unparseable cart.
|
|
119
|
+
*/
|
|
120
|
+
export declare function buildRenderHarness(cartText: string, target: RenderTarget, options?: {
|
|
121
|
+
seconds?: number;
|
|
122
|
+
sentinel?: string;
|
|
123
|
+
wavBasename?: string;
|
|
124
|
+
}): RenderHarness;
|
|
125
|
+
/**
|
|
126
|
+
* Builds the `audio record <cart>` harness: unlike `render`, this captures the
|
|
127
|
+
* cart's OWN playback, so it keeps the cart's `__lua__` and only APPENDS a
|
|
128
|
+
* cooperative recorder tab that starts recording immediately and stops after the
|
|
129
|
+
* window. The recorder runs as an extra code tab so it does not disturb the
|
|
130
|
+
* cart's own `_update`/`_draw` (PICO-8 runs all tabs; the appended top-level code
|
|
131
|
+
* arms the recorder and a hooked `_update` advances the timer).
|
|
132
|
+
*
|
|
133
|
+
* Because a general cart may define its own `_update`, the recorder wraps it: it
|
|
134
|
+
* saves the cart's `_update` and installs one that ticks the record timer then
|
|
135
|
+
* calls the original. This is the record analogue of the debug skill's "add
|
|
136
|
+
* capture lines, run, revert" recipe, done on a THROWAWAY copy.
|
|
137
|
+
*
|
|
138
|
+
* @throws {HarnessError} an unparseable cart.
|
|
139
|
+
*/
|
|
140
|
+
export declare function buildRecordHarness(cartText: string, options?: {
|
|
141
|
+
seconds?: number;
|
|
142
|
+
sentinel?: string;
|
|
143
|
+
wavBasename?: string;
|
|
144
|
+
}): RenderHarness;
|
|
145
|
+
export {};
|
|
146
|
+
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/harness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAC;AAStC;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACrB;IAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GAC9C;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GAClD;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAC;AAE3B,gFAAgF;AAChF,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,wFAAwF;AACxF,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,gBAAgB;AAC3B,qEAAqE;AACnE,kCAAkC;AACpC,8EAA8E;GAC5E,2BAA2B;AAC7B,sDAAsD;GACpD,+BAA+B,CAAC;AAEnC,0EAA0E;AAC1E,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;gBACpB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKnD;AAKD;;;;;GAKG;AACH,UAAU,SAAS;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAoBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAUtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC5B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,YAAY,EACpB,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,CAaR;AAcD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAS3D;AAED,uFAAuF;AACvF,MAAM,WAAW,aAAa;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAkCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAM,GACvE,aAAa,CA4Cf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAM,GACvE,aAAa,CA6Cf"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `audio render` harness builder (ADR-0009 Part 3): the PURE, CI-testable
|
|
3
|
+
* core that turns a user's cart + a chosen render TARGET into a THROWAWAY cart
|
|
4
|
+
* whose `_update` plays the target, records it via `audio_rec`/`audio_end(1)`,
|
|
5
|
+
* and prints the done-sentinel. This is the audio analogue of the `run`
|
|
6
|
+
* screenshot harness (the `picopilot-debug` recipe): the author writes NO
|
|
7
|
+
* playback code and still gets "render sfx 5 -> a WAV".
|
|
8
|
+
*
|
|
9
|
+
* Kept pure (cart-text in, cart-text + a record window out) so the harness
|
|
10
|
+
* construction + the record-duration heuristic are unit-testable WITHOUT the
|
|
11
|
+
* paid PICO-8 binary; only the actual capture needs a live A/V session (the
|
|
12
|
+
* manual/opt-in tier, ADR-0009).
|
|
13
|
+
*/
|
|
14
|
+
import { Cart } from '../cart/index.js';
|
|
15
|
+
import { DONE_SENTINEL, PICO8_FRAMES_PER_SECOND, PICO8_TICKS_PER_SECOND, RECORD_WAV_BASENAME, } from './adapter.js';
|
|
16
|
+
import { SFX_ROW_LENGTH } from '../audio/sfx.js';
|
|
17
|
+
/** The lowest/highest SFX slot a render target may name (finding: 64 slots). */
|
|
18
|
+
export const SFX_TARGET_MIN = 0;
|
|
19
|
+
export const SFX_TARGET_MAX = 63;
|
|
20
|
+
/** The lowest/highest music PATTERN a render target may name (finding: 64 patterns). */
|
|
21
|
+
export const PATTERN_TARGET_MIN = 0;
|
|
22
|
+
export const PATTERN_TARGET_MAX = 63;
|
|
23
|
+
/**
|
|
24
|
+
* The default record window (seconds) for a target whose exact play length
|
|
25
|
+
* cannot be derived cheaply (a music pattern / whole song: PICO-8 does not store
|
|
26
|
+
* a per-pattern length, and following loops/flags statically is out of scope for
|
|
27
|
+
* a render harness). A generous window that captures a typical loop; the author
|
|
28
|
+
* overrides it with an explicit duration (ADR-0009: render is a real-time
|
|
29
|
+
* recording, so a fixed window is honest, not a promise of exact length).
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_RECORD_SECONDS = 8;
|
|
32
|
+
/** A small tail margin (seconds) appended so the target's final note is not clipped. */
|
|
33
|
+
export const RECORD_TAIL_SECONDS = 0.5;
|
|
34
|
+
/** A structured harness-build failure (a target refusal, not a crash). */
|
|
35
|
+
export class HarnessError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'HarnessError';
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** An all-zero `__sfx__` row: the canonical "empty slot" (mirrors section.ts). */
|
|
44
|
+
const EMPTY_SFX_ROW = '0'.repeat(SFX_ROW_LENGTH);
|
|
45
|
+
/** Reads slot `index`'s row from a `__sfx__` body, or undefined if absent/empty. */
|
|
46
|
+
function sfxRow(sfxBody, index) {
|
|
47
|
+
if (sfxBody === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
const rows = sfxBody.split(/\r?\n/).filter((l) => l.length > 0);
|
|
50
|
+
const row = rows[index];
|
|
51
|
+
if (row === undefined ||
|
|
52
|
+
row.length !== SFX_ROW_LENGTH ||
|
|
53
|
+
row === EMPTY_SFX_ROW) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return row;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decodes a `__sfx__` text row into its {@link SfxTiming}. `speed` is header
|
|
60
|
+
* byte [2:4]; `rows` is the count of tracker rows up to and including the last
|
|
61
|
+
* one with a non-zero volume nibble (a trailing silent tail does not extend the
|
|
62
|
+
* audible length). Returns `rows = 0` for an all-silent row.
|
|
63
|
+
*/
|
|
64
|
+
export function decodeSfxTiming(row) {
|
|
65
|
+
const speed = Number.parseInt(row.slice(2, 4), 16) || 1;
|
|
66
|
+
let lastAudible = 0;
|
|
67
|
+
for (let r = 0; r < 32; r++) {
|
|
68
|
+
const noteStart = 8 + r * 5;
|
|
69
|
+
// Note layout: PP (pitch, 2) W (waveform, 1) V (volume, 1) E (effect, 1).
|
|
70
|
+
const vol = Number.parseInt(row.charAt(noteStart + 3), 16);
|
|
71
|
+
if (!Number.isNaN(vol) && vol > 0)
|
|
72
|
+
lastAudible = r + 1;
|
|
73
|
+
}
|
|
74
|
+
return { speed, rows: lastAudible };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The record window (seconds) for a render target, given the cart. For an SFX
|
|
78
|
+
* target the window is derived from that slot's speed + audible row count
|
|
79
|
+
* (`rows * speed` ticks / {@link PICO8_TICKS_PER_SECOND}) plus a tail margin, so
|
|
80
|
+
* the recording covers exactly the target's play length without guessing. For a
|
|
81
|
+
* pattern / whole-song target the length is not cheaply derivable, so the
|
|
82
|
+
* {@link DEFAULT_RECORD_SECONDS} window is used (overridable by the caller).
|
|
83
|
+
*
|
|
84
|
+
* An explicit `override` (the command's `--seconds`) always wins.
|
|
85
|
+
*/
|
|
86
|
+
export function recordSeconds(cart, target, override) {
|
|
87
|
+
if (override !== undefined)
|
|
88
|
+
return override;
|
|
89
|
+
if (target.kind === 'sfx') {
|
|
90
|
+
const row = sfxRow(cart.getSection('sfx'), target.index);
|
|
91
|
+
if (row !== undefined) {
|
|
92
|
+
const { speed, rows } = decodeSfxTiming(row);
|
|
93
|
+
const ticks = rows * speed;
|
|
94
|
+
const seconds = ticks / PICO8_TICKS_PER_SECOND + RECORD_TAIL_SECONDS;
|
|
95
|
+
// A degenerate (0-row) slot still records a short window rather than 0s.
|
|
96
|
+
return Math.max(seconds, RECORD_TAIL_SECONDS + 0.5);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return DEFAULT_RECORD_SECONDS;
|
|
100
|
+
}
|
|
101
|
+
/** The Lua expression that PLAYS the target once, at harness start. */
|
|
102
|
+
function playCall(target) {
|
|
103
|
+
switch (target.kind) {
|
|
104
|
+
case 'sfx':
|
|
105
|
+
return `sfx(${target.index})`;
|
|
106
|
+
case 'pattern':
|
|
107
|
+
return `music(${target.index})`;
|
|
108
|
+
case 'song':
|
|
109
|
+
return 'music(0)';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** A one-line human description of the target (for the render result + help). */
|
|
113
|
+
export function describeTarget(target) {
|
|
114
|
+
switch (target.kind) {
|
|
115
|
+
case 'sfx':
|
|
116
|
+
return `sfx ${target.index}`;
|
|
117
|
+
case 'pattern':
|
|
118
|
+
return `music pattern ${target.index}`;
|
|
119
|
+
case 'song':
|
|
120
|
+
return 'the whole song (music 0)';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validates a render target against the cart's authored content, refusing an
|
|
125
|
+
* out-of-range index or an empty SFX slot (nothing to render) with a structured
|
|
126
|
+
* {@link HarnessError} rather than recording silence.
|
|
127
|
+
*/
|
|
128
|
+
function validateTarget(cart, target) {
|
|
129
|
+
if (target.kind === 'sfx') {
|
|
130
|
+
if (target.index < SFX_TARGET_MIN || target.index > SFX_TARGET_MAX) {
|
|
131
|
+
throw new HarnessError('audio-render-target-out-of-range', `SFX target ${target.index} is out of range ${SFX_TARGET_MIN}..${SFX_TARGET_MAX}.`);
|
|
132
|
+
}
|
|
133
|
+
if (sfxRow(cart.getSection('sfx'), target.index) === undefined) {
|
|
134
|
+
throw new HarnessError('audio-render-target-empty', `SFX slot ${target.index} is empty in this cart, there is nothing to render. Author it first with \`sfx from-mml ${target.index} "<mml>"\`.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (target.kind === 'pattern') {
|
|
138
|
+
if (target.index < PATTERN_TARGET_MIN ||
|
|
139
|
+
target.index > PATTERN_TARGET_MAX) {
|
|
140
|
+
throw new HarnessError('audio-render-target-out-of-range', `music pattern target ${target.index} is out of range ${PATTERN_TARGET_MIN}..${PATTERN_TARGET_MAX}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Builds the throwaway render harness (ADR-0009 Part 3). Takes the user's cart
|
|
146
|
+
* text + a render TARGET (+ an optional record-window override) and produces a
|
|
147
|
+
* new cart with the SAME authored `__sfx__`/`__music__` but a REPLACED `__lua__`
|
|
148
|
+
* whose `_update`:
|
|
149
|
+
* - at frame 1: `extcmd("set_filename", <base>)`, `extcmd("audio_rec")`, plays
|
|
150
|
+
* the target (`sfx(N)` / `music(P)` / `music(0)`);
|
|
151
|
+
* - at frame `frames`: `extcmd("audio_end", 1)` (save to the current folder,
|
|
152
|
+
* which the adapter points at an isolated temp dir);
|
|
153
|
+
* - at frame `frames + 1`: `printh` the done-sentinel so the launcher kills
|
|
154
|
+
* PICO-8 promptly (a cart cannot self-quit, ADR-0006).
|
|
155
|
+
*
|
|
156
|
+
* @throws {HarnessError} an out-of-range/empty target or an unparseable cart.
|
|
157
|
+
*/
|
|
158
|
+
export function buildRenderHarness(cartText, target, options = {}) {
|
|
159
|
+
let cart;
|
|
160
|
+
try {
|
|
161
|
+
cart = Cart.parse(cartText);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new HarnessError('audio-render-cart-parse-error', `cart does not parse: ${e instanceof Error ? e.message : String(e)}`);
|
|
165
|
+
}
|
|
166
|
+
validateTarget(cart, target);
|
|
167
|
+
const seconds = recordSeconds(cart, target, options.seconds);
|
|
168
|
+
const frames = Math.max(1, Math.round(seconds * PICO8_FRAMES_PER_SECOND));
|
|
169
|
+
const sentinel = options.sentinel ?? DONE_SENTINEL;
|
|
170
|
+
const wavBasename = options.wavBasename ?? RECORD_WAV_BASENAME;
|
|
171
|
+
const play = playCall(target);
|
|
172
|
+
// Replace __lua__ with the record harness; keep every OTHER section (crucially
|
|
173
|
+
// __sfx__/__music__) byte-identical so the target plays exactly as authored.
|
|
174
|
+
const lua = [
|
|
175
|
+
'-- picopilot audio render harness (throwaway; ADR-0009).',
|
|
176
|
+
'-- Real-time recording of playback, NOT an offline export.',
|
|
177
|
+
't=0',
|
|
178
|
+
'function _update()',
|
|
179
|
+
' t+=1',
|
|
180
|
+
` if t==1 then extcmd("set_filename","${wavBasename}") extcmd("audio_rec") ${play} end`,
|
|
181
|
+
` if t==${frames} then extcmd("audio_end",1) end`,
|
|
182
|
+
` if t==${frames + 1} then printh("${sentinel}") end`,
|
|
183
|
+
'end',
|
|
184
|
+
'function _draw() cls() print("rendering "..t,0,0,7) end',
|
|
185
|
+
'',
|
|
186
|
+
].join('\n');
|
|
187
|
+
cart.setSection('lua', lua);
|
|
188
|
+
return {
|
|
189
|
+
cartText: cart.serialize(),
|
|
190
|
+
seconds,
|
|
191
|
+
frames,
|
|
192
|
+
wavBasename,
|
|
193
|
+
description: describeTarget(target),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Builds the `audio record <cart>` harness: unlike `render`, this captures the
|
|
198
|
+
* cart's OWN playback, so it keeps the cart's `__lua__` and only APPENDS a
|
|
199
|
+
* cooperative recorder tab that starts recording immediately and stops after the
|
|
200
|
+
* window. The recorder runs as an extra code tab so it does not disturb the
|
|
201
|
+
* cart's own `_update`/`_draw` (PICO-8 runs all tabs; the appended top-level code
|
|
202
|
+
* arms the recorder and a hooked `_update` advances the timer).
|
|
203
|
+
*
|
|
204
|
+
* Because a general cart may define its own `_update`, the recorder wraps it: it
|
|
205
|
+
* saves the cart's `_update` and installs one that ticks the record timer then
|
|
206
|
+
* calls the original. This is the record analogue of the debug skill's "add
|
|
207
|
+
* capture lines, run, revert" recipe, done on a THROWAWAY copy.
|
|
208
|
+
*
|
|
209
|
+
* @throws {HarnessError} an unparseable cart.
|
|
210
|
+
*/
|
|
211
|
+
export function buildRecordHarness(cartText, options = {}) {
|
|
212
|
+
let cart;
|
|
213
|
+
try {
|
|
214
|
+
cart = Cart.parse(cartText);
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
throw new HarnessError('audio-render-cart-parse-error', `cart does not parse: ${e instanceof Error ? e.message : String(e)}`);
|
|
218
|
+
}
|
|
219
|
+
const seconds = options.seconds ?? DEFAULT_RECORD_SECONDS;
|
|
220
|
+
const frames = Math.max(1, Math.round(seconds * PICO8_FRAMES_PER_SECOND));
|
|
221
|
+
const sentinel = options.sentinel ?? DONE_SENTINEL;
|
|
222
|
+
const wavBasename = options.wavBasename ?? RECORD_WAV_BASENAME;
|
|
223
|
+
const existing = cart.getSection('lua') ?? '';
|
|
224
|
+
// Append the recorder AFTER the cart's own code. It wraps _update so it works
|
|
225
|
+
// whether or not the cart defines one, and never edits the cart's own logic.
|
|
226
|
+
const recorder = [
|
|
227
|
+
'',
|
|
228
|
+
'-->8',
|
|
229
|
+
'-- picopilot audio record harness (throwaway; ADR-0009).',
|
|
230
|
+
"-- Records the cart's own audio; real-time capture, NOT an offline export.",
|
|
231
|
+
'__pp_rec_update=_update',
|
|
232
|
+
'__pp_rec_t=0',
|
|
233
|
+
'function _update()',
|
|
234
|
+
' __pp_rec_t+=1',
|
|
235
|
+
` if __pp_rec_t==1 then extcmd("set_filename","${wavBasename}") extcmd("audio_rec") end`,
|
|
236
|
+
` if __pp_rec_t==${frames} then extcmd("audio_end",1) end`,
|
|
237
|
+
` if __pp_rec_t==${frames + 1} then printh("${sentinel}") end`,
|
|
238
|
+
' if __pp_rec_update then __pp_rec_update() end',
|
|
239
|
+
'end',
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
cart.setSection('lua', existing + recorder);
|
|
243
|
+
return {
|
|
244
|
+
cartText: cart.serialize(),
|
|
245
|
+
seconds,
|
|
246
|
+
frames,
|
|
247
|
+
wavBasename,
|
|
248
|
+
description: 'the running cart',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../../../src/engine/pico8/harness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PICO-8 engine seam: launch + capture over the user's PICO-8 binary
|
|
3
|
+
* (ADR-0006, native `pico8 -x` path). Barrel re-export so the command layer and
|
|
4
|
+
* later PICO-8-gated features (`audio render`, `export`) import from one place.
|
|
5
|
+
*
|
|
6
|
+
* - `adapter`: the `Pico8Adapter` interface, `Pico8Result`, the structured
|
|
7
|
+
* `pico8-not-found` value, and the run-report shapes.
|
|
8
|
+
* - `sentinel`: the pure sentinel-watch core (CI-testable without the binary).
|
|
9
|
+
* - `shell`: the v1 adapter shelling out to native `pico8 -x`, with an
|
|
10
|
+
* injectable spawn seam for isolation-testing.
|
|
11
|
+
*/
|
|
12
|
+
export * from './adapter.js';
|
|
13
|
+
export * from './drive.js';
|
|
14
|
+
export * from './harness.js';
|
|
15
|
+
export * from './sentinel.js';
|
|
16
|
+
export * from './session.js';
|
|
17
|
+
export * from './session-daemon.js';
|
|
18
|
+
export * from './session-daemon-main.js';
|
|
19
|
+
export * from './shell.js';
|
|
20
|
+
export * from './supervisor.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PICO-8 engine seam: launch + capture over the user's PICO-8 binary
|
|
3
|
+
* (ADR-0006, native `pico8 -x` path). Barrel re-export so the command layer and
|
|
4
|
+
* later PICO-8-gated features (`audio render`, `export`) import from one place.
|
|
5
|
+
*
|
|
6
|
+
* - `adapter`: the `Pico8Adapter` interface, `Pico8Result`, the structured
|
|
7
|
+
* `pico8-not-found` value, and the run-report shapes.
|
|
8
|
+
* - `sentinel`: the pure sentinel-watch core (CI-testable without the binary).
|
|
9
|
+
* - `shell`: the v1 adapter shelling out to native `pico8 -x`, with an
|
|
10
|
+
* injectable spawn seam for isolation-testing.
|
|
11
|
+
*/
|
|
12
|
+
export * from './adapter.js';
|
|
13
|
+
export * from './drive.js';
|
|
14
|
+
export * from './harness.js';
|
|
15
|
+
export * from './sentinel.js';
|
|
16
|
+
export * from './session.js';
|
|
17
|
+
export * from './session-daemon.js';
|
|
18
|
+
export * from './session-daemon-main.js';
|
|
19
|
+
export * from './shell.js';
|
|
20
|
+
export * from './supervisor.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/engine/pico8/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure sentinel-watch core (ADR-0006): scan a stdout stream line by line and
|
|
3
|
+
* decide WHEN to kill PICO-8. A cart cannot self-quit the app, so it signals
|
|
4
|
+
* "done" by printing a sentinel line via `printh`; the launcher kills the moment
|
|
5
|
+
* it matches. This is strictly better than a blind timeout (no guessing the
|
|
6
|
+
* duration), which stays only as a backstop in the shell layer.
|
|
7
|
+
*
|
|
8
|
+
* Kept PURE and stream-shaped on purpose: it is the load-bearing logic and the
|
|
9
|
+
* one piece testable in CI WITHOUT the paid PICO-8 binary. The shell layer feeds
|
|
10
|
+
* it real child stdout; a test feeds it a fake array of lines.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Splits a raw stdout chunk into complete lines plus a trailing remainder,
|
|
14
|
+
* so sentinel matching works on whole lines even when the OS delivers stdout in
|
|
15
|
+
* arbitrary chunks. The remainder (an unterminated final line) is carried to the
|
|
16
|
+
* next chunk by the caller.
|
|
17
|
+
*
|
|
18
|
+
* Handles both `\n` and `\r\n`; the `\r` is stripped so a sentinel matches
|
|
19
|
+
* regardless of platform line endings.
|
|
20
|
+
*/
|
|
21
|
+
export declare function splitLines(chunk: string): {
|
|
22
|
+
lines: string[];
|
|
23
|
+
rest: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Scans `lines` for the `sentinel` and reports whether it appeared and at which
|
|
27
|
+
* index. A match means the caller should kill PICO-8 now (exit reason
|
|
28
|
+
* `sentinel`); no match means keep streaming until the backstop or a natural
|
|
29
|
+
* exit. The comparison is an EXACT line match (the cart prints the sentinel on
|
|
30
|
+
* its own line), so incidental substrings in other output never trip it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findSentinel(lines: readonly string[], sentinel?: string): {
|
|
33
|
+
matched: boolean;
|
|
34
|
+
index: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A stateful line accumulator for a streaming run: feed it raw stdout chunks;
|
|
38
|
+
* it buffers partial lines, exposes the complete lines seen so far, and reports
|
|
39
|
+
* the moment the sentinel is matched. This is the exact glue the shell adapter
|
|
40
|
+
* drives (feed chunks, stop on `sentinelSeen`), and a test drives it with
|
|
41
|
+
* synthetic chunks to assert prompt-kill vs. no-match without a real process.
|
|
42
|
+
*/
|
|
43
|
+
export declare class SentinelWatcher {
|
|
44
|
+
private readonly sentinel;
|
|
45
|
+
private buffer;
|
|
46
|
+
private readonly linesSeen;
|
|
47
|
+
private matched;
|
|
48
|
+
constructor(sentinel?: string);
|
|
49
|
+
/**
|
|
50
|
+
* Feeds one raw stdout chunk. Returns `true` the first time the sentinel is
|
|
51
|
+
* matched (the caller should kill PICO-8 then); once matched it stays matched.
|
|
52
|
+
*/
|
|
53
|
+
push(chunk: string): boolean;
|
|
54
|
+
/** Whether the sentinel has been seen on a complete line so far. */
|
|
55
|
+
get sentinelSeen(): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The full captured stdout as text (complete lines plus any unterminated
|
|
58
|
+
* trailing buffer), for the run report's `printh`. The sentinel line itself
|
|
59
|
+
* is KEPT (it is real cart output); the command decides whether to show it.
|
|
60
|
+
*/
|
|
61
|
+
get text(): string;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=sentinel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/sentinel.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAOzE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,QAAQ,GAAE,MAAsB,GAC9B;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,CAGnC;AAED;;;;;;GAMG;AACH,qBAAa,eAAe;IAKf,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJrC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAEK,QAAQ,GAAE,MAAsB;IAE7D;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAU5B,oEAAoE;IACpE,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED;;;;OAIG;IACH,IAAI,IAAI,IAAI,MAAM,CAMjB;CACD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DONE_SENTINEL } from './adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* The pure sentinel-watch core (ADR-0006): scan a stdout stream line by line and
|
|
4
|
+
* decide WHEN to kill PICO-8. A cart cannot self-quit the app, so it signals
|
|
5
|
+
* "done" by printing a sentinel line via `printh`; the launcher kills the moment
|
|
6
|
+
* it matches. This is strictly better than a blind timeout (no guessing the
|
|
7
|
+
* duration), which stays only as a backstop in the shell layer.
|
|
8
|
+
*
|
|
9
|
+
* Kept PURE and stream-shaped on purpose: it is the load-bearing logic and the
|
|
10
|
+
* one piece testable in CI WITHOUT the paid PICO-8 binary. The shell layer feeds
|
|
11
|
+
* it real child stdout; a test feeds it a fake array of lines.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Splits a raw stdout chunk into complete lines plus a trailing remainder,
|
|
15
|
+
* so sentinel matching works on whole lines even when the OS delivers stdout in
|
|
16
|
+
* arbitrary chunks. The remainder (an unterminated final line) is carried to the
|
|
17
|
+
* next chunk by the caller.
|
|
18
|
+
*
|
|
19
|
+
* Handles both `\n` and `\r\n`; the `\r` is stripped so a sentinel matches
|
|
20
|
+
* regardless of platform line endings.
|
|
21
|
+
*/
|
|
22
|
+
export function splitLines(chunk) {
|
|
23
|
+
const parts = chunk.split('\n');
|
|
24
|
+
const rest = parts.pop() ?? '';
|
|
25
|
+
return {
|
|
26
|
+
lines: parts.map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l)),
|
|
27
|
+
rest,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Scans `lines` for the `sentinel` and reports whether it appeared and at which
|
|
32
|
+
* index. A match means the caller should kill PICO-8 now (exit reason
|
|
33
|
+
* `sentinel`); no match means keep streaming until the backstop or a natural
|
|
34
|
+
* exit. The comparison is an EXACT line match (the cart prints the sentinel on
|
|
35
|
+
* its own line), so incidental substrings in other output never trip it.
|
|
36
|
+
*/
|
|
37
|
+
export function findSentinel(lines, sentinel = DONE_SENTINEL) {
|
|
38
|
+
const index = lines.findIndex((l) => l === sentinel);
|
|
39
|
+
return { matched: index !== -1, index };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A stateful line accumulator for a streaming run: feed it raw stdout chunks;
|
|
43
|
+
* it buffers partial lines, exposes the complete lines seen so far, and reports
|
|
44
|
+
* the moment the sentinel is matched. This is the exact glue the shell adapter
|
|
45
|
+
* drives (feed chunks, stop on `sentinelSeen`), and a test drives it with
|
|
46
|
+
* synthetic chunks to assert prompt-kill vs. no-match without a real process.
|
|
47
|
+
*/
|
|
48
|
+
export class SentinelWatcher {
|
|
49
|
+
sentinel;
|
|
50
|
+
buffer = '';
|
|
51
|
+
linesSeen = [];
|
|
52
|
+
matched = false;
|
|
53
|
+
constructor(sentinel = DONE_SENTINEL) {
|
|
54
|
+
this.sentinel = sentinel;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Feeds one raw stdout chunk. Returns `true` the first time the sentinel is
|
|
58
|
+
* matched (the caller should kill PICO-8 then); once matched it stays matched.
|
|
59
|
+
*/
|
|
60
|
+
push(chunk) {
|
|
61
|
+
const { lines, rest } = splitLines(this.buffer + chunk);
|
|
62
|
+
this.buffer = rest;
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
this.linesSeen.push(line);
|
|
65
|
+
if (line === this.sentinel)
|
|
66
|
+
this.matched = true;
|
|
67
|
+
}
|
|
68
|
+
return this.matched;
|
|
69
|
+
}
|
|
70
|
+
/** Whether the sentinel has been seen on a complete line so far. */
|
|
71
|
+
get sentinelSeen() {
|
|
72
|
+
return this.matched;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The full captured stdout as text (complete lines plus any unterminated
|
|
76
|
+
* trailing buffer), for the run report's `printh`. The sentinel line itself
|
|
77
|
+
* is KEPT (it is real cart output); the command decides whether to show it.
|
|
78
|
+
*/
|
|
79
|
+
get text() {
|
|
80
|
+
const all = this.buffer.length > 0
|
|
81
|
+
? [...this.linesSeen, this.buffer]
|
|
82
|
+
: this.linesSeen;
|
|
83
|
+
return all.join('\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=sentinel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel.js","sourceRoot":"","sources":["../../../src/engine/pico8/sentinel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;;GAUG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAC/B,OAAO;QACN,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChE,IAAI;KACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC3B,KAAwB,EACxB,WAAmB,aAAa;IAEhC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;IACrD,OAAO,EAAC,OAAO,EAAE,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK,EAAC,CAAC;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,eAAe;IAKE;IAJrB,MAAM,GAAG,EAAE,CAAC;IACH,SAAS,GAAa,EAAE,CAAC;IAClC,OAAO,GAAG,KAAK,CAAC;IAExB,YAA6B,WAAmB,aAAa;QAAhC,aAAQ,GAAR,QAAQ,CAAwB;IAAG,CAAC;IAEjE;;;OAGG;IACH,IAAI,CAAC,KAAa;QACjB,MAAM,EAAC,KAAK,EAAE,IAAI,EAAC,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1B,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED,oEAAoE;IACpE,IAAI,YAAY;QACf,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED;;;;OAIG;IACH,IAAI,IAAI;QACP,MAAM,GAAG,GACR,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;YACrB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC;YAClC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QACnB,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC;CACD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detached daemon process ENTRY (ADR-0011, prd US #6). `playtest start`
|
|
3
|
+
* spawns this file (via `process.execPath [+ execArgv] <this> <configJson>`, so
|
|
4
|
+
* it re-uses the exact loader the parent ran under, dev `tsx` or built `dist`) as
|
|
5
|
+
* a DETACHED background process that owns the live driven PICO-8 for the session's
|
|
6
|
+
* lifetime. It just decodes its JSON config and hands off to {@link daemonMain};
|
|
7
|
+
* all the logic lives in `session-daemon-main.ts` (so it stays unit-testable).
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=session-daemon-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-daemon-entry.d.ts","sourceRoot":"","sources":["../../../src/engine/pico8/session-daemon-entry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detached daemon process ENTRY (ADR-0011, prd US #6). `playtest start`
|
|
3
|
+
* spawns this file (via `process.execPath [+ execArgv] <this> <configJson>`, so
|
|
4
|
+
* it re-uses the exact loader the parent ran under, dev `tsx` or built `dist`) as
|
|
5
|
+
* a DETACHED background process that owns the live driven PICO-8 for the session's
|
|
6
|
+
* lifetime. It just decodes its JSON config and hands off to {@link daemonMain};
|
|
7
|
+
* all the logic lives in `session-daemon-main.ts` (so it stays unit-testable).
|
|
8
|
+
*/
|
|
9
|
+
import { daemonMain } from './session-daemon-main.js';
|
|
10
|
+
daemonMain(process.argv.slice(2));
|
|
11
|
+
//# sourceMappingURL=session-daemon-entry.js.map
|