• src/doors/syncscumm/door/

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 17 02:57:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/54648707ec2bc73ecb4afca3
    Added Files:
    src/doors/syncscumm/door/audio_term.cpp audio_term.h src/doors/syncscumm/test/test_sst_io_audio.c test_sst_io_audio_backlog.c test_sst_io_audio_ini_headroom.c test_sst_io_audio_ini_off.c test_sst_io_audio_ini_tune.c test_sst_io_audio_static.c test_sst_io_audio_tone.c test_sst_io_audio_underrun.c
    Modified Files:
    .gitlab-ci-unix.yml docs/superpowers/plans/2026-07-16-syncscumm-m4.md src/doors/syncretro/CMakeLists.txt M4_AUDIO.md syncretro_audio.h test_audio_bytes.c src/doors/syncscumm/DESIGN.md build.sh src/doors/syncscumm/door/CMakeLists.txt module.mk sst_io.c sst_io.h syncscumm.cpp video_term.cpp src/doors/syncscumm/syncscumm.example.ini src/doors/syncscumm/test/subtitles.sh unit_sst_io.sh src/doors/termgfx/audio_stream.c audio_stream.h chunk.c src/doors/termgfx/test/CMakeLists.txt test_audio_stream.c
    Log Message:
    syncscumm: stream the game's audio to SyncTERM

    Speech, effects and music, over the existing door connection. A terminal
    that cannot play audio is untouched: silent, subtitles auto-on, not one
    audio byte on the wire.

    ScummVM sums every voice, effect and synthesized track into one mixer, so
    one pull covers every engine and every sound type -- there is no useful per-sound boundary to hook, and music arrives already rendered. The pull
    rides the wall clock rather than the game loop, so the game's pacing can neither starve nor flood the stream and audio cannot drift against video;
    it ticks from both pollEvent() and the present path, because the intro
    drives updateScreen() in a tight loop without pumping events, which is
    exactly the cutscene the audio exists for.

    The terminal is asked what it can decode, and the answer drives subtitles:
    auto now means "will this player actually hear it", which the sysop's
    switch also decides. Audio and video share one staged queue with nothing reordered; the door reports audio's own share of it as backlog, so the
    drop rule fires on real audio congestion rather than on a large frame.

    Three things had to be found by listening and then measured, and all three
    had a plausible wrong answer first:

    Dialogue dropped out for seconds at a time. The streaming module flushes
    when its cushion releases and at stop, and otherwise leaves writing to the door's loop -- which only flushed while presenting a frame. A comic panel
    is a still: drawn once, talked over. Nothing drawn, nothing flushed, so
    chunks sat staged while the module blamed a link that was idle.

    It popped ten times a second. Each chunk is its own Opus stream, so every boundary carried an encoder delay and a decoder ramp with nothing to lap against. Chunking is not the problem -- SyncTERM's queues are for exactly
    this -- doing it ten times a second was. 250ms chunks put the boundary
    rate back at chance and cut a third of the payload.

    The loudest scene tore. The mix hard-clips there, a codec reconstructs
    flat tops with ringing that overshoots, and libsndfile's short reader
    wraps instead of clamping -- so an overshoot came back as a full-scale
    sign flip, thousands of them. Merely loud passages never overshoot. The
    stream is attenuated before the encoder and the channel plays at unity,
    which nets to the gain it always had: the headroom simply arrives before
    the encoder rather than after the decoder, where it could do nothing.

    Sysops get [audio] for the rest: enabled, quality, volume, chunk_ms,
    prebuffer, headroom, channels.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 17 21:23:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/eed018326540c2d520a0e796
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/syncscumm.example.ini
    Log Message:
    syncscumm: trim BASS -6 dB to sit with the family, add + / - volume

    BASS streams ScummVM's WHOLE pre-mixed output (music+SFX+speech, balanced inside ScummVM) as one channel near unity, whereas SyncDOOM/SyncDuke trim
    music ~11-14 dB under SFX and let one-shots ride SyncTERM's -12 dB base.
    So at volume 100 (0 dB) BASS ran ~9 dB louder than the other doors.

    Drop the default channel volume 100 -> 50 (-6 dB); atop the -3 dB
    pre-encode headroom that lands the net at ~-9 dB -- close to the family,
    still clear for dialogue.

    BASS also had no volume control. Wire + / - to the shared stream module's volume step, exactly as SyncRetro already does: + louder, - quieter, and
    0 = off. This is safe because the door forwards no keyboard input to
    ScummVM (BASS is mouse-driven, pollEvent only ever emits QUIT), so these
    keys were being dropped anyway -- nothing is taken from the game.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/76924f4416ccce0581fa7dcd
    Modified Files:
    src/doors/syncscumm/door/sst_io.c
    Log Message:
    syncscumm: US spelling in the mouse mapper's comments

    Two doc comments ported into sst_mouse_report() carried British "centre"/"CENTRES" from syncconquer's source; the repo convention is US English. Comment-only, no code or behavior change.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/ac8f5c4ae47a26463806661d
    Modified Files:
    src/doors/syncscumm/door/syncscumm.cpp
    Log Message:
    syncscumm: deliver mouse events to ScummVM

    Drain the session's input FIFO in pollEvent(): motion becomes
    EVENT_MOUSEMOVE (and warpMouse() so the existing compositor draws the
    cursor there), buttons become L/M/R button events, wheel notches become EVENT_WHEELUP/DOWN. Beneath a Steel Sky is now mouse-playable -- click to
    walk and to drive the verb/inventory interface. Keyboard follows.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/95b64092fc4857364a412249
    Modified Files:
    src/doors/syncscumm/door/syncscumm.cpp
    Log Message:
    syncscumm: deliver keyboard events to ScummVM

    Translate the queued key codes into Common::KeyState -- arrows, F1-F9, Enter/Esc/Backspace/Tab/Home/End/PageUp-Dn/Insert/Delete and printable
    ASCII, with Ctrl/Alt/Shift modifiers -- and emit EVENT_KEYDOWN/KEYUP.
    The Global Main Menu (save/load/quit/options), cutscene skips and
    save-name entry are now reachable, completing M3 input.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/465b69d75e64a110152f6b8b
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_io_present_pending.c
    Log Message:
    syncscumm: clear the present-pending flag past the dedupe early-return

    cc6f2373ec added a retry for a present() frame a static screen
    stranded: a gated call retains the frame (g_pending_idx/g_pending_pal, g_present_pending=1) and sst_io_tick() re-invokes sst_io_present()
    with it once the gate clears. The clear (g_present_pending = 0) sat
    AFTER the whole-frame dedupe early-return, so a retry whose retained
    frame happened to equal g_last_fb took the dedupe return and never
    reached the clear.

    Repro: a gate defers frame A, retaining it; while still congested the
    engine presents frame B that is byte-identical to g_last_fb (e.g. a
    cursor blink-and-revert -- video_term.cpp's dirty check keys off the
    engine's own dirty/cursor state, not what was actually sent); since
    idx != g_pending_idx the retain overwrites g_pending_idx with B (==
    g_last_fb); the gate clears and sst_io_tick() retries with
    g_pending_idx, which now dedupes out. From then on sst_io_tick()
    re-invokes sst_io_present() every poll forever on an idle static
    screen: a 64000-byte memcmp per poll, plus a stats redraw + wire flush
    every poll if the Ctrl-S bar is on.

    Move the clear to fire for every call that gets past BOTH defer gates
    (pacing and backpressure), before the g_need_st handling. Past both
    gates the frame is no longer stranded -- it either goes out below or
    turns out identical to what the terminal already shows (dedupe); in
    both cases whatever was retained for it has been handled. A later
    frame that defers again is re-retained by the gate code, unaffected by
    clearing here.

    Also factor the retain snippet (memcpy idx/pal into g_pending_*, set g_present_pending=1) that was duplicated verbatim at both gates into a
    single sst_present_retain() helper, called at both call sites, so the
    two copies can no longer drift out of hand-sync.

    Adds a convergence regression to test_sst_io_present_pending.c: gate a
    retry of the exact frame just sent (so the retained frame equals
    g_last_fb), let sst_io_tick() retry it into the dedupe path, and
    assert sst_io_test_present_pending() reads 0 afterward, plus that a
    further tick is a true no-op. Verified RED against the pre-fix
    sst_io.c (assertion failure) and GREEN with this change.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/4ede5029451a751b9ed6746d
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_mouse.c
    Log Message:
    syncscumm: make the top canvas edge reachable with a cell-granular mouse

    On a terminal that reports mouse position by text cell (not SGR-Pixels),
    the cell mapper placed the pointer at the cell's CENTER, which sits ch/2
    px below the top canvas edge, so game y=0 was unreachable. Beneath a
    Steel Sky drops its inventory bar only when the pointer reaches y=0, so
    the bar never appeared on a cell-granular sixel client. The mapper
    already snapped the LAST row/col out to the far image edge; snap the
    FIRST row/col to the near edge too, so the whole play area -- top edge
    included -- is reachable. (syncconquer deliberately leaves the top short
    for Red Alert's scroll-up zone; this door wants the opposite, so the
    divergence is intentional.) No effect under SGR-Pixels, where the report already carries exact canvas pixels.

    Also log each mouse report under SYNCSCUMM_TRACE (raw cell/pixel coords,
    the mode read, the fitted image rect, and the game coords produced) so a
    "can't reach the edge" or "stale cursor" report can be diagnosed from a
    node trace without a live capture.

    Cell-edge reachability is now asserted in test_sst_mouse (first row -> y=0, last row/col -> far edge).

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 03:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/cf716c8c543841719d32b3fe
    Modified Files:
    src/doors/syncscumm/door/sst_io.c src/doors/syncscumm/test/test_sst_io_bottom_dirty.c
    Log Message:
    syncscumm: check sixel bottom-strand before emitting any dirty box

    sst_dirty_sixel_present()'s bottom-strand `return 0` (full-frame
    fallback, added in the previous commit) lived inside the box-emit
    loop, and boxes are written to the wire as each one succeeds in
    raster order. A frame that dirties an earlier box AND a later
    bottom-strand box -- the real stale-cursor case always does: an
    erase box above the cursor's old spot plus the bottom box itself --
    already had the earlier box's bytes on the wire by the time the
    bottom box hit `return 0`. The caller then saw a 0 return, believed
    nothing was sent, and ALSO emitted a full frame, so the wire carried
    a partial dirty box plus a full frame on exactly the frames this fix
    targets. That violates the clean-abandon invariant the parallel JXL
    present function documents: never stack a full frame on top of
    already-sent partial rects. Visually harmless (the full frame
    overwrites) but wasteful on the hot path.

    Make the fallback atomic: run a pre-pass over every coalesced box,
    computing its display rect + clamped height exactly as the emit loop
    does, and return 0 immediately -- before a single out_put() -- if any
    emittable box (rw > 0) strands. A box with rw <= 0 is the existing
    empty-width `continue` case and must not itself count as stranding.
    To avoid duplicating the ~25 lines of NN-mapping/cell-snap/vstep math
    in two loops, factor it into a new static helper,
    sst_box_display_rect(), called from both the pre-pass and the emit
    loop; the emit loop no longer needs its own inner strand check, since
    the pre-pass already guarantees no box in the frame strands.

    Tighten test_sst_io_bottom_dirty.c to match: it used to scan for the
    largest sixel raster Pv and tolerate an earlier, smaller raster
    header ahead of the full frame. With the atomic fix a bottom-strand
    present() must put exactly ONE raster header on the wire (the full
    frame), so the test now counts headers and asserts cnt == 1 for both
    the bottom-strand case and the ordinary dirty-rect case. Confirmed
    this tightened assertion goes RED (cnt == 1 fails, two headers seen)
    when compiled against the previous commit's pre-pass-less sst_io.c,
    and GREEN after this fix.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 18 23:14:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/1bcb26e37dfaf1c30db70452
    Modified Files:
    src/doors/syncscumm/door/sst_io.c sst_io.h syncscumm.cpp
    Log Message:
    syncscumm: stop the AGI ego with numpad-5 (map KP5 to KEYCODE_KP5)

    The numpad center key was unmapped, so in an AGI game (Space Quest 0)
    pressing it to halt the walking ego leaked the ASCII '5' into the text
    parser instead. AGI's keyboard handler maps Common::KEYCODE_KP5 to AGI_KEY_STATIONARY (engines/agi/keyboard.cpp) -- the canonical "stop" --
    but the door never produced that keycode: the evdev decoder had no case
    for keycode 76 (KP5), so it fell through to termgfx_evdev_ascii() and
    emitted '5'.

    Add SST_KEY_KP5 and wire it at all three decode/emit sites: the evdev
    switch (keycode 76), the kitty keypad path (KP_BEGIN, 57427 -- the
    NumLock-off center key), and the SST_KEY -> Common::KEYCODE event
    builder. The numpad now respects the game's directional/stop mapping the
    way the other keypad keys already did (KP8=up, KP2=down, ...), so AGI
    walk/stop works from a real numpad. Toggle-walk itself (one arrow press
    walks until stopped) is AGI's native behavior, unchanged.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 05:16:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/73b281013ebe1322927bd2aa
    Modified Files:
    src/doors/syncscumm/door/sst_io.c
    Log Message:
    syncscumm: capture door stderr behind a touch-file gate

    On a BBS-launched door the process's stderr is dead, so ScummVM's own diagnostics -- including the fatal error() an engine prints just before
    it aborts -- are recorded nowhere. That is what made the Cascade Quest Sound->Volume crash hard to pin down before the release-mode fix.

    Wire up sst_plat_redirect_stderr() (added with the Win32 build but not previously called) behind a touch-file gate, matching the existing
    trace and wirecap gates: create <data-dir>/syncscumm/stderr (SBBSDATA
    for a real door; ./syncscumm-stderr in a dev/standalone run) and stderr
    is redirected to /tmp/syncscumm.<pid>.stderr. Off by default -- absent
    the touch-file it opens nothing. It sits before resolve_fd()'s headless early-return so a boot test can arm it too.

    Going through the sst_plat abstraction keeps the platform #ifdef out of sst_io.c.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 05:16:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/db6bd14f9746e4a6dcb86e88
    Modified Files:
    src/doors/syncscumm/door/sst_io.c sst_io.h syncscumm.cpp
    Log Message:
    syncscumm: add mixer-volume knobs and a Ctrl-G Global Main Menu key

    Two door-wide conveniences for reaching audio balance and menus from any terminal.

    Mixer volumes: new syncscumm.ini "[audio]" knobs music_volume,
    speech_volume and sfx_volume (0-100 percent) set ScummVM's own per-
    channel mixer levels, so a title whose soundtrack overbears its dialogue
    can ship a rebalanced default. These are ScummVM's linear 0-255 channels
    (what its GMM Volume sliders show), distinct from the "[audio] volume"
    knob, which scales the whole pre-mixed stream. Applied to kSessionDomain
    by resolveVolumes(), mirroring resolveSubtitles().

    Global Main Menu key: ScummVM's "MENU" action defaults to the keyboard
    "Menu" key, which no terminal sends, so the GMM (Volume/Save/Load/Quit)
    was unreachable outside SCUMM. F5 stays the game's own menu; a reserved
    Ctrl-G is intercepted in all three input paths (kitty, legacy byte, and SyncTERM evdev) and delivered to the engine as EVENT_MAINMENU, which
    opens the GMM in every engine, on every terminal -- a control byte, not
    a modified function key. Ctrl-G is free in the non-SCUMM engines; in
    SCUMM it takes over the "very fast mode" toggle while Ctrl-F "fast mode"
    still works. Configurable via "[input] menu_key = ctrl-<letter>" (or
    "off"); default ctrl-g.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Wed Jul 22 16:22:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/85eee29699c3bd67950ab783
    Modified Files:
    src/doors/syncscumm/door/CMakeLists.txt
    Log Message:
    syncscumm: link libsndfile when termgfx finds it via find_package CONFIG

    The Linux build transcribes termgfx's external link deps across the seam
    to ScummVM's hand-rolled link (build.sh appends them to config.mk's
    LIBS). For libsndfile, door/CMakeLists.txt reconstructed the -lsndfile
    flag from termgfx's INTERFACE_LINK_LIBRARIES, and that reconstruction
    had a hole:

    - if(TARGET SndFile::sndfile) is false in the parent scope -- an
    imported target created by find_package() inside the termgfx
    add_subdirectory() is directory-scoped and not visible one level up
    in door/; and
    - the bare-"sndfile" regex fallback does not match the target-name
    string "SndFile::sndfile" that the CONFIG path leaves in
    INTERFACE_LINK_LIBRARIES.

    So on any host where termgfx finds libsndfile via find_package(SndFile
    CONFIG) -- Arch and other distros that ship SndFileConfig.cmake (which
    CMake prefers over pkg-config), and vcpkg -- both branches miss, sndfile_libs.txt is written empty, build.sh's [ -s ] check skips it, and
    no -lsndfile reaches the link. But termgfx still compiled with TERMGFX_WITH_SNDFILE, so libtermgfx.a references sf_*, and the door fails
    to link with undefined references to sf_format_check / sf_open_virtual / sf_command / sf_writef_short / sf_close from termgfx/audio.c. Hosts that
    find libsndfile via pkg-config (bare "sndfile" in the link interface)
    were unaffected, which is why it went unseen.

    Add a safety net keyed on the real invariant -- termgfx compiled with TERMGFX_WITH_SNDFILE means libtermgfx.a references sf_*, so the final
    link needs libsndfile. The compile definition is readable on the regular termgfx target from any scope, so when neither existing branch produced a token, fall back to -lsndfile. No effect on the working pkg-config path
    (the existing branch already sets it).

    Reported by Accession on IRC: the door failed to link on Arch with
    exactly those undefined sf_* references, and build/libs/sndfile_libs.txt
    was confirmed empty.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:10:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/a971badeea5084c7eda2b3d9
    Modified Files:
    src/doors/syncscumm/door/video_term.cpp
    Log Message:
    syncscumm: capture the frames the encoder saw, for exact replay

    Comparing two builds of the encode path on GAMEPLAY has no good answer otherwise. A cutscene is deterministic and can just be replayed by both,
    but it is full-motion video: most of its frames legitimately exceed the
    patch budget, so it measures the one case none of this work can improve.
    Actual play is where the patch path earns its keep -- and a point-and-click game does nothing without a human in it, while no human walks the same
    route twice closely enough to see a ten-percent difference.

    So capture what the encoder was handed. A touch-file writes every
    (indices, palette) pair passed to termgfx_termio_present() to /tmp/<door>.<pid>.frames; replaying that file through two builds gives byte-identical input where the only variable left is the code. One session played once, and the corpus stays valid for the next change too.

    Deliberately the pair passed to present() rather than the game surface:
    the quantized overlay path substitutes its own indices and palette, and a measurement of the encoder wants exactly what the encoder got.

    Gated by a touch-file, not an env var, like the trace and wirecap gates
    next to it -- the BBS execvp()s the door with no shell, so a variable set before launch never arrives. Absent, nothing is opened and nothing is
    written. Buffered, so a capture cannot turn present() into a synchronous
    write on the frame path.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:10:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/a11cddf07ac34e7c2a33c51a
    Added Files:
    src/doors/syncscumm/door/help_term.cpp help_term.h
    Modified Files:
    src/doors/syncscumm/door/module.mk syncscumm.cpp video_term.cpp src/doors/termgfx/termgfx_termio.c termgfx_termio.h
    Log Message:
    syncscumm: a key-help card, on Ctrl-K and F1

    This was the only termgfx door with no help of any kind, and the games it
    hosts are the ones that need it most: SCUMM v1/v2 (Maniac Mansion) has no
    menu bar to stumble into, so a caller who wanted OUT had nothing to find.
    The one key that mattered -- the GMM hotkey, which is where Quit lives --
    was announced only to stderr, and a BBS-launched door has its stderr
    discarded, so the single place it was written down was invisible in
    exactly the situation it was needed.

    The card names the menu key the sysop configured (not a hardcoded
    Ctrl-G), the graphics tier and stats keys, and the blunt exit.

    That exit is Ctrl-Q, or Ctrl-C, and it deserves its own note. On a
    terminal in a native key mode -- kitty, or the evdev protocol SyncTERM negotiates, so what a caller here almost always has -- the bare letter is deliberately left alone so it can reach ScummVM's text entry, and only
    the CTRL form quits; only a terminal with neither protocol falls back to
    the raw byte path where the letter itself is reserved. Quitting saves
    nothing on the way out, but ScummVM's autosave is on at its five-minute
    default and the door does not override it, so what is lost is the last
    few minutes rather than the session. Told only "no save", a caller would
    assume the worse of the two, so the card says both and points at the menu
    for a real save.

    A dedicated key, not "any unmapped key" the way syncretro does it:
    syncretro owns its entire binding table, so "unbound" is a fact it knows,
    while this door is a HOST that forwards nearly every key to ScummVM and
    cannot see whether the engine consumed one. Worse, ScummVM's save/load
    dialogs take free text, so a catch-all would fire on every letter of a save-game name. Ctrl-K is what syncconquer chose and is free in SCUMM;
    F1 matches the other doors' muscle memory. NOT Ctrl-H: that is Backspace,
    which SCUMM needs for name entry, and terminals disagree about whether Backspace sends 0x08 or 0x7f.

    The panel is plain positioned ANSI with no box-drawing glyphs (those
    split CP437 vs UTF-8 across SyncTERM and Windows Terminal), the door
    stops presenting frames while it is up so the picture cannot repaint over
    it, and any key takes it down -- a help page you cannot get out of is
    worse than no help page.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:32:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/bbce3bed65f582e57c55e129
    Modified Files:
    src/doors/syncscumm/door/video_term.cpp
    Log Message:
    syncscumm: the framedump gate goes through the door's platform seam

    MSVC has no <unistd.h>, no access()/F_OK and no getpid(), so the frame
    capture added in a971badeea broke the windows-x86 build with video_term.cpp(17,10): error C1083. Use termgfx_plat_file_exists() and termgfx_plat_getpid(), as termgfx_termio.c's identically-shaped trace and wirecap touch-file gates already do.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net