Skip to content

Sprint 4 Design Pack — GWP-256

Story narrative — Gameplay Designer take

Section titled “Story narrative — Gameplay Designer take”

The publisher splash is a designed gameplay mechanic, not boot-time UX bloat.

The publisher layer in docs/software/runtime/orchestration.md §“Software Publishers & Loading Screens” (L468–676) is one of the deck’s most carefully crafted fiction surfaces. Six licensed publishers + one outside-tier pirate (Setec Astronomy) each have a distinctive 2–3 second loading-screen ritual that is the operator’s first contact with whoever made this software. Edgeware’s character-by-character wordmark assembly with soft keyclicks signals “first-party, polished, safe.” Zaibatsu’s hex-dump-into-glyph-block-mark with white noise signals “underground, possibly criminal, brilliant.” Bureau 9’s silent typewriter classification header signals “shadow government — the room feels colder.” These are diegetic moments. They build the world.

When the publisher splash is missing — when the cart loader skips header parse straight to mission board — operators don’t get to know who made the software they’re using. That sounds tonally minor, but in the capability-model architecture (where carts are the load-bearing collectibility/ritual artifact) it’s a meaningful absence. The publisher beat is the bridge between the physical object (the SD-card-sled clamshell with its label artwork per ADR-0019) and the digital experience (the mission board that follows). Without the splash, the cart load is a black box; with it, the cart load is a brief, characterful theater.

What does the splash do for the operator?

  1. Signals identity. “This is Edgeware. I trust this.” vs “This is Zaibatsu. I’m in for it.” Tonal preparation for the cart’s own aesthetic.
  2. Provides ritual punctuation between cart insertion and gameplay. The physical act of opening the case and slotting the SD-sled deserves a small theatrical response. Without one, the deck feels like a file browser.
  3. Carries metadata invisibly. Publisher (= which slot of trust the operator is mounting), module class header (Bureau 9’s CLASS: FORENSIC ACCOUNTING line), version (v1.3 etc) — all conveyed in a 2-3 second beat that doesn’t feel like a UI dialog.
  4. Sets the audio room tone. The PSG sting establishes channel ownership and amplitude reference for the SFX cues that follow. After Bureau 9’s silence, a single keyclick is louder; after Zaibatsu’s noise burst, normal SFX feels muted. This is intentional cross-cart sonic variation.
  5. Earns attention for the “no-publisher-splash” outside cart variant. Setec Astronomy’s UNLICENSED disclaimer + anagram-rearrangement is meaningful because every other cart performs the publisher-trust ritual; the pirate cart’s deviation reads as transgression. If no cart ships a splash, Setec Astronomy has nothing to deviate from and the joke dies.

Why exist as a hook (not a hard-coded sequence)?

The publishers are fiction-defined; the splashes are art-direction-defined; both should be authored cart-side, not runtime-side. The runtime owns the contract (3-second budget, 30 fps, exclusive PSG access, time-out clamp, SYS skip), not the content. A future cart from a publisher we haven’t designed yet (Phase 2 modules; community carts; sister-deck carts) authors its own splash without touching nOSh runtime code.

Player-facing semantics with worked example

Section titled “Player-facing semantics with worked example”

The contract (what the cart sees):

A cart’s publisher-splash lambda is invoked once per frame for up to 90 frames (3 seconds at 30 fps). Each invocation receives the current frame number (0..89). The lambda returns:

  • nil to continue the splash (more frames to draw)
  • non-nil to signal “splash complete; advance to mission board immediately”

The runtime grants the splash:

  • Exclusive write access to the main 80×25 grid (the text-* and gfx-* primitives)
  • Exclusive write access to the PSG (the psg-* and sound-* primitives)
  • Read access to deck state (operator handle, credits — for splashes that personalize)
  • NO access to CIPHER-LINE (CIPHER is OLED-exclusive per Spec Hygiene Rule 6; CIPHER-LINE continues to render its own ambient ticker independent of the splash)
  • NO access to mission-context primitives (no mission is active during splash)

The runtime enforces:

  • 3-second hard ceiling. If the lambda hasn’t returned non-nil by frame 90, the runtime cuts to mission board regardless. No misbehaved cart can hold the device.
  • SYS-skip. If the operator presses SYS at any frame, the splash terminates immediately. (Per industry convention; impatient-operator affordance.)
  • Pre-mission-board ordering. Splash runs after cartridge_load_v2() completes header parse and FFI binding, before mission board generation. The cart’s PUBLISHER_SPLASH lambda sees a fully-loaded NoshAPI; it does not see registered cell types, mission templates, or phase handlers (those are mission-context concerns).

Worked example: Edgeware splash for ICE Breaker (NeonGrid, actually — ICE Breaker is Zaibatsu, but let’s use Edgeware as the cleanest example since they publish NeonGrid).

(publisher-splash
(lambda (frame)
(cond
;; Frame 0: clear screen
((= frame 0)
(text-clear)
nil)
;; Frames 1-26: assemble "EDGEWARE SYSTEMS CO., LTD." character by character
;; (one char every 2 frames, 13 chars in "EDGEWARE SYSTEMS" + spaces, 26 frames total)
((<= frame 26)
(let ((char-idx (/ frame 2))
(msg "EDGEWARE SYSTEMS CO., LTD."))
(when (= (% frame 2) 0)
(text-putc (+ 27 char-idx) 12 (string-ref msg char-idx))
(sfx-keyclick)))
nil)
;; Frame 27: render the subline
((= frame 27)
(text-puts 25 14 "A KINOSHITA ELECTRONICS COMPANY")
nil)
;; Frames 28-30: clean tone via Channel A
((= frame 28)
(sound-tone 0 440 12)
nil)
((<= frame 60)
nil) ; let the tone ring out
;; Frame 60+: done
(else t))))

Total runtime: ~2 seconds. Returns t at frame 60 to signal complete; runtime advances to mission board. Edgeware’s signature: restrained, corporate, exactly what you’d expect from first-party software.

The Bureau 9 splash as the hardest case — it’s the silent one (no PSG output at all). Validates that a cart can ship an audio-quiet splash; the runtime must not assume the splash uses sound. Bureau 9’s PUBLISHER_SPLASH is mostly text-puts calls at calibrated frame intervals with sfx-silence (or no PSG calls at all) — and sound-silence should be honored across the splash.

Acceptance criteria expanded (≥6 testable items)

Section titled “Acceptance criteria expanded (≥6 testable items)”
  1. kn86-emulator/src/cartridge.c gains publisher_splash_run(SystemState *state, CartHandle *handle) — additive entry point called after cartridge_load_v2() header parse and FFI binding, before mission template registration. Returns void; the function blocks until splash completes or times out, then returns to caller.
  2. kn86-emulator/src/nosh_lisp_bridge.c registers a publisher-splash Lisp primitive that the cart-side (publisher-splash <lambda>) form binds to. The primitive stores the lambda in a per-cart slot (CartHandle.publisher_splash_fn or equivalent); publisher_splash_run invokes it per-frame.
  3. Per-frame loop invokes the lambda at 30 fps (33.33 ms per frame nominal; SDL frame pacing). Frame counter passed as integer arg. Lambda returns Fe nil → continue; Fe truthy → terminate splash early.
  4. 3-second hard ceiling enforced. If lambda hasn’t terminated by frame 90, runtime breaks the loop and returns regardless. Test: a misbehaved cart that always returns nil does not hang the deck — the function returns within 3 seconds + epsilon.
  5. SYS-key skip. During the splash loop, the runtime polls input. If SYS is pressed at any frame (using the existing input-dispatch surface), splash terminates immediately. Test: SDL synthetic SYS keypress at frame 30 → function returns by frame 31.
  6. Exclusive PSG access during splash. Runtime suppresses any other audio source (CIPHER-LINE OLED is unaffected; primary 80×25 grid is owned by the cart’s lambda). Test: an attempt to play a non-cart-initiated SFX during splash is no-op.
  7. CIPHER-LINE remains independent. OLED-side rendering loop continues normally during splash; if CIPHER has scheduled fragments, they tick. Test: schedule a CIPHER fragment, run a splash, verify the fragment renders on CIPHER-LINE per its normal cadence (not gated on splash completion).
  8. Sample cart authored. One launch cart (recommend icebreaker.lsp — it’s already the hero cart) gets a PUBLISHER_SPLASH lambda matching the Zaibatsu Digital splash spec from orchestration.md L519–532 (hex-dump-into-glyph-mark with noise burst). Per the cartridge author UX, this is end-to-end authoring proof that the contract works.
  9. tests/test_publisher_splash.c covers:
    • Full 90-frame run (lambda returns nil for 90 frames; runtime cuts at 90).
    • Early abort at frame 30 (lambda returns t).
    • Time-out clamp (misbehaved lambda; runtime cuts at 90 regardless).
    • SYS-key skip mid-splash.
    • PSG isolation (non-cart audio attempts during splash are no-op).
    • CIPHER-LINE pass-through (OLED render continues during splash).
    • Lambda receives correct frame counter (0..89 inclusive).
  10. Cart-load latency budget unchanged at 3s splash + ~200 ms template parse. Document in the cart-load timing notes if any.

Cross-references (cart specs that consume + ADRs that constrain)

Section titled “Cross-references (cart specs that consume + ADRs that constrain)”
  • docs/software/runtime/orchestration.md §“Software Publishers & Loading Screens” (L468–676) — design source. The 6 licensed publishers + Setec Astronomy each have a fully-specified splash; this implementation is the runtime that lets those specs ship.
  • docs/software/runtime/orchestration.md §“Technical Implementation” (L668–680) — has a stub C macro example that GWP-261’s doc sweep replaces with the Lisp form. Coordinate with GWP-261 — both PRs touch related surface; landing order matters (GWP-256 ships the runtime + Lisp form, GWP-261 documents it).
  • ADR-0005 (FFI surface)publisher-splash is a new primitive; should it be added to the Tier 1 (all-carts) primitives table? Recommend yes, as a special case under a new “Lifecycle hooks” subsection. ADR amendment may be in scope for this PR.
  • ADR-0001 (embedded-lisp-scripting-layer) — confirms cart authoring is Lisp; the splash form authors here.
  • ADR-0017 (coprocessor) — PSG calls during splash route through the coprocessor on the prototype; emulator-internal on the desktop. The contract is identical from the cart’s perspective. Cross-reference for the implementing engineer.
  • ADR-0019 (cartridge storage) — splash runs after the SD-mount and cartridge_load_v2; doesn’t see the SD path. No interaction.
  • All 14+1 cart design bibles under docs/software/cartridges/modules/ — each will eventually need a PUBLISHER_SPLASH lambda (follow-on tasks per cart). This task ships the contract + one sample; the rest get filed as GWP-256-followup-icebreaker etc.
  1. Cart with no PUBLISHER_SPLASH form. cartridge_load_v2 finds no lambda registered. Runtime should skip the splash entirely (no frame loop, no time consumed) — publisher_splash_run returns immediately. Pre-Phase-2 community carts may not author a splash; the deck must handle them gracefully. Default fallback: show a 1-frame “GENERIC PUBLISHER” placeholder text (UNKNOWN PUBLISHER centered) for ~500 ms with no audio? Or skip entirely? Recommendation: skip entirely. The fiction is “carts have publishers”; absence is a community-cart signal that should be visible by not having the ritual, not by a runtime-faked stub.
  2. Cart’s lambda errors out (Fe panic, division by zero, nil-arg). Runtime should terminate the splash loop, log to debug overlay, and continue to mission board. Don’t crash the deck on a malformed splash. Test: a deliberately-broken splash lambda → splash terminates with logged error → mission board appears.
  3. Splash blocks beyond 33ms in a single frame. A heavy lambda (e.g., a Zaibatsu-style hex dump that paints all 80×25 cells per frame) might exceed the 33ms frame budget. The 3-second wall clock should be enforced regardless of frame count — i.e., if 3 seconds elapse in 60 frames because each frame took 50ms, runtime still cuts at 3s wall. Time-budget enforcement should be wall-clock based, not frame-count based. (Or both — whichever fires first.)
  4. Operator inserts a cart while CIPHER is mid-fragment. CIPHER-LINE doesn’t pause; the OLED ticker continues. The splash on the main grid runs in parallel. This is the cockpit-voice-recorder effect described in cipher-voice.md — verify the fragment doesn’t get truncated or restarted.
  5. Splash for the Null cart (the diagnostic / Cipher-escape cart). Per Spec Hygiene Rule 6, Null is the only cart sanctioned to render CIPHER on the main grid. Its splash could render CIPHER glyphs on the main 80×25 — but that’s not “publisher splash” semantics; that’s Null’s gameplay surface. Recommend: Null’s splash is conventional (Edgeware-style — Edgeware is Null’s publisher per orchestration.md L489); the CIPHER-escape mechanic is a runtime mode separate from splash. Document this in Null’s design bible.
  • Files owned: kn86-emulator/src/cartridge.c (additive — publisher_splash_run helper). kn86-emulator/src/nosh_lisp_bridge.c (additive — publisher-splash primitive registration).
  • Files added-to: kn86-emulator/tests/test_publisher_splash.c (new). carts/icebreaker.lsp (additive — sample lambda for Zaibatsu splash). Possibly docs/adr/ADR-0005-ffi-surface.md Amendment Log if Lifecycle hooks subsection added.
  • Files NOT touched: display.c, font.c, psg.c (per task constraint — splash is a cart-driven sequence using existing primitives, not a new render path). oled.c (CIPHER-LINE remains independent). main.c event loop (splash is a blocking helper inside cart-load, not an event-loop mode).
  • Expected PR size: ~120 lines cartridge.c (publisher_splash_run + per-frame loop + timing + skip handling), ~40 lines nosh_lisp_bridge.c (primitive registration), ~80 lines Zaibatsu sample lambda in icebreaker.lsp, ~200 lines tests. Single C engineer + Gameplay Design consult, ~1 day with TDD.
  • Test strategy: TDD. Author the 7 test cases first (frame counter, full run, early abort, time-out clamp, SYS skip, PSG isolation, CIPHER pass-through). Implement to pass. The Zaibatsu sample lambda is the end-to-end proof; review by playing it on the desktop emulator.
  • Dispatch shape: single C engineer + Gameplay Design consult on the Zaibatsu sample’s frame-by-frame faithfulness to the orchestration.md spec. Dependent on GWP-242 (gfx-* primitives) and GWP-243 (sound-* primitives) per the task’s Dependencies — both shipped per ADR-0005’s 2026-04-25 amendment, so dependency is clear.
  • Watch for: the SYS-key skip is the trickiest part — the splash loop has to poll input without re-entering the normal event-dispatch path. Recommend: a stripped-down input poll that only watches for SYS, ignores everything else, doesn’t update the input ring or recency counters. The Tier 1 input primitives are not for the splash; this is a meta-concern.
  1. Sample-cart choice. Zaibatsu splash for ICE Breaker (per the worked example above) or a simpler Edgeware splash for a less-load-bearing cart? Recommendation: Zaibatsu for ICE Breaker — it exercises the full PSG + animation envelope and validates the contract under stress. Confirm.
  2. No-splash-cart behavior. Skip entirely (recommended) vs show a UNKNOWN PUBLISHER placeholder vs configurable per-cart-flag. Recommendation: skip entirely (preserves the “absence is meaningful” framing). Confirm.
  3. publisher-splash Lisp primitive in ADR-0005? Recommend amending ADR-0005 with a new “Lifecycle hooks” subsection covering this primitive + future hooks (mission-end debrief, cart-eject cleanup, etc.). Confirm or defer to a separate ADR amendment task.
  4. Splash-skip key: SYS (per task body) vs ENT vs any-key. Industry convention varies. Recommendation: SYS, because the operator’s right index finger naturally rests there and SYS is the conventional “system-control” key on this layout. Confirm.
  5. Time-budget enforcement: frame-count, wall-clock, or both? Recommendation: both — whichever fires first. Defends against both runaway lambdas (slow per-frame) and pathological 90-frame loops.