• src/doors/syncconquer/doo

    From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Wednesday, July 08, 2026 01:33:00
    https://gitlab.synchro.net/main/sbbs/-/commit/cbea88cb7ee78a73e6657384
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncalert: TODO note 640x384 downscale + the DECSSDT status-line fix

    The engine is 640x400 (80x25) but SyncTERM defaults to an 80x24 / 640x384 canvas (its status line takes the 25th row), so every image tier downscales 0.96x and RA's single-pixel vertical bars drop -- text/gauges degrade;
    crisp only at a true 640x400. Record the proper fix inline: drop the status line with DECSSDT (CSI 0 $ ~), first saving the client's current setting via DECRQSS (DCS $ q $ ~ ST) so door exit can restore it. Notes the separate, smaller one-row sixel reserve too.

    Co-Authored-By: Claude Fable 5 <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 Wednesday, July 08, 2026 02:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b454a7de6ceaefa915be41a3
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/termgfx/term.c term.h Log Message:
    syncalert: hide the SyncTERM status line for a true 640x400 canvas

    SyncTERM defaults to an 80x24 / 640x384 drawing area (its status line takes
    the 25th row), so a 640x400 game fractionally downscales 0.96x and RA's single-pixel vertical bars drop -- text and gauges look degraded; it's
    crisp only at a real 640x400.

    Hide the status line at entry with DECSSDT (CSI 0 $ ~ = "no status
    display"), emitted BEFORE the canvas probe so the probe reports the
    reclaimed 640x400 and every image tier then renders 1:1. The client's
    current setting is queried first with DECRQSS (DCS $ q $ ~ ST); door_io_
    pump() captures the reply (a raw-byte scan, since the DCS reply is
    otherwise swallowed) and door_term_restore() puts it back on exit -- or the usual "indicator" default if the terminal doesn't answer the query.
    Terminals with no status line / no DECSSDT ignore all of it.

    Added to termgfx as a reusable feature (term.c/term.h): the termgfx_term_status_off string, termgfx_term_status_set() restore builder,
    and termgfx_term_parse_status() reply parser -- so SyncDuke/SyncDOOM can reclaim the row the same way.

    Co-Authored-By: Claude Fable 5 <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 Thursday, July 09, 2026 01:55:00
    https://gitlab.synchro.net/main/sbbs/-/commit/9545ba78544c17c9656d25b7
    Modified Files:
    src/doors/syncconquer/door/door_input.c door_io.c door_io.h
    Log Message:
    syncconquer: move onto termgfx keymode, arming the missing settle window

    Replaces the door's own copy of the terminal key-mode negotiation with termgfx/keymode.h (cf67bccb2a, thrown-29-throw): the evdev/kitty enable and restore sequences, the evdev-wins precedence, the kitty CSI-u parameter decoder, the evdev->ASCII table, and the modifier classifier.

    This fixes a real defect. SyncConquer enabled SyncTERM physical key
    reports without an enable-time settle window, which the other doors all
    had. SyncTERM re-reports every key held at the instant reports are
    enabled -- typically the Enter still down from selecting the door off the
    BBS menu -- so the door acted on a keypress the player never made, before
    the first frame was drawn. Press edges are now dropped for 500ms after enabling; releases and modifier tracking are honored throughout, so no
    key can stick down.

    The state moves to door_io.c (it sees the CTDA reply that enables evdev
    and owns the leave path that undoes it) and door_input.c reaches it
    through door_io_keymode(). door_io_now_ms() is exported so the settle
    check runs in the same monotonic clock domain as the pacing.

    No change to the bytes on the wire, the key->action map, or the modifier keycodes recognized (29/97 Ctrl, 42/54 Shift, 56/100 Alt -- the table
    this door already had is the one termgfx now carries, verified identical
    byte for byte across all four doors).

    ---
    þ 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 Thursday, July 09, 2026 01:55:00
    https://gitlab.synchro.net/main/sbbs/-/commit/3ad14c32961933b9483230d5
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/syncduke/syncduke.h syncduke_door.c syncduke_io.c src/doors/syncduke/tests/test_evdev.c test_keymap.c test_kitty.c
    Log Message:
    doors: restore the terminal on the hangup path, not just on a clean exit

    SyncDuke and SyncConquer both _exit(0) straight out of their hangup
    handler, on the reasoning that "the socket is already dead". That holds
    for a WRITE-side hangup. It does not hold for a read-side one: an EOF on
    read leaves the write direction open, which is exactly what happens when
    a user closes their client. The door then vanished without undoing what
    it had turned on, and the next thing the BBS ran inherited a terminal in SyncTERM physical-key mode -- reports on, translated keys suppressed --
    plus mouse tracking, a hidden cursor and no autowrap.

    Both now run their terminal restore before _exit(). Each restore is
    idempotent and its drain is bounded, so a genuinely dead socket costs a
    failed write and nothing more. A re-entrancy guard handles the recursion
    this creates on a dead socket, where the restore's flush calls back into
    the hangup handler.

    This adopts what syncretro (sr_door_hangup) and syncdoom (whose hangup
    sets a flag and leaves via exit(), so atexit runs the restore) already
    did, so all four doors now hand the terminal back on every exit path.

    syncduke_term_restore() loses its static linkage and gains an idempotence guard; the three input tests, which link syncduke_door.c without
    syncduke_io.c, stub it.

    Verified: syncduke_hangup() provably runs the restore before _exit and
    survives the dead-socket recursion (forked probe against the real syncduke_door.c), and a half-close -- client shutdown(SHUT_WR), door
    still able to write -- makes a door emit the full restore and exit 0.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Friday, July 10, 2026 03:28:00
    https://gitlab.synchro.net/main/sbbs/-/commit/312794de0530321312bcf568
    Modified Files:
    src/doors/syncconquer/door/soundio_termgfx.cpp
    Log Message:
    syncalert: fix the build -- pass the music `loop` argument termgfx requires

    commit b042fa1da1 (ripe-12-dies) gave termgfx_audio_music_play() and termgfx_audio_music() a trailing `loop` argument, and updated the two
    call sites it knew about -- src/doors/syncdoom/i_termmusic.c and src/doors/syncduke/syncduke_stubs.c. It missed syncconquer, whose door
    had landed the day before in f5578ea57f (foam-43-clip), so this door's
    three call sites in door/soundio_termgfx.cpp have been one argument
    short ever since:

    error C2660: 'termgfx_audio_music_play': function does not take 3 arguments
    error C2660: 'termgfx_audio_music': function does not take 8 arguments

    That is a hard compile error on every platform, not just Windows -- the syncalert door has not built anywhere since b042fa1da1.

    Pass loop=1, as both siblings do. It is also what this door already
    assumes: SoundImp_Sample_Status() runs its own wall-clock duration model precisely so the terminal can loop the score forever while Red Alert's ThemeClass::Still_Playing() is told the song "ended" and advances to the
    next track (see the "(looping) path" comments on as_music).

    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 Friday, July 10, 2026 23:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/5936da4d1c60ec677eff815a
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncalert: play FMV cutscenes by default (-NOMOVIES now opt-in)

    The door forced -NOMOVIES into the engine argv, which suppressed Red
    Alert's VQA intro/campaign cutscenes. That suppression was intentional
    early on, but the movies actually stream fine over the terminal: the VQA
    frames route through the game page-flip into door_present(), and the
    movie loop keeps pumping the door's input/pacing (so they're skippable
    and don't stall the connection).

    Stop forcing the flag. -NOMOVIES is now injected only when the sysop
    sets the SYNCALERT_NOMOVIES environment variable, so cutscenes play by
    default. The door-arg neutering (sanitizing door-owned tokens out of the engine's view) is unchanged.

    Cutscenes are silent for now -- the no-video build links the null VQA
    audio backend; wiring VQA audio into termgfx is a separate follow-up.

    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 Friday, July 10, 2026 23:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/0edf7a3435f121d8097ee0d6
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncalert: keep the PPM tier out of the F4 cycle when JXL is available

    The uncompressed PPM image tier is only a useful fallback for a SyncTERM
    build that lacks JPEG XL -- and a raw P6 carries no colorspace tag, so
    SyncTERM decodes it as BT.709 rather than our sRGB and the palette looks
    wrong. Real SyncTERM has JXL, so cycling into PPM with F4 was only ever a surprise with off colors.

    Add it to the F4 tier list only when JXL is not available. sa_auto_tier()
    still selects PPM as the automatic fallback for a no-JXL SyncTERM, so that niche keeps working.

    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 Friday, July 10, 2026 23:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/530d43ff4b90871bb306feac
    Modified Files:
    src/doors/syncconquer/door/door_input.c
    Log Message:
    syncalert: make the map edge-scroll reachable on cell-granular terminals

    Red Alert scrolls the tactical map when the cursor reaches a viewport
    edge. On a cell-granular terminal (SyncTERM) the mouse mapper placed the
    cursor at each cell's centre, so the last row/column centre landed ch/2
    or cw/2 px short of the far canvas edge -- inside RA's edge-scroll zone's
    blind spot -- and the map wouldn't scroll down or right. It only worked
    in the sixel tier by accident: its reserved bottom row let the coordinate
    clamp snap to the true edge.

    Snap the far edge cells (last row/column) to the image-rect edge in cell
    mode so scroll-down/right is reachable in every tier. Pixel-reporting
    clients (SGR-Pixels 1016) already reach the edges natively and are left
    alone. The near edges (first row/column) deliberately keep the cell
    centre -- snapping the top row to y=0 would drag menu-tab clicks into the scroll-up zone and make the tabs unclickable.

    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 Friday, July 10, 2026 23:41:00
    https://gitlab.synchro.net/main/sbbs/-/commit/e2935fc86b51162e3ac36734
    Modified Files:
    src/doors/syncconquer/door/door_input.c door_io.c door_io.h
    Log Message:
    syncalert: add Ctrl-S stats overlay and fix door hotkeys in all key modes

    Ctrl-S toggles a live debug overlay: graphics tier, frame rate, wire
    throughput (K/Mbps), pacing depth, keyboard-event mode, mouse
    granularity, client charset, RTT, and canvas/grid/cell geometry. It's
    drawn as one condensed line on the bottom text row -- the one placement
    that survives every tier (sixel can't paint over its reserved bottom row; JXL/PPM and the text tiers layer a text overlay on top). The overlay does
    NOT bypass the frame-dedupe: on an unchanged frame it refreshes only the
    line (change-detected) rather than re-encoding the whole image, which is
    what made an earlier top-corner overlay flicker under sixel.

    Also fix the door-level Ctrl+letter hotkeys (Ctrl-D/F/U/P/S). They were
    only decoded on the legacy raw-byte input path, so under the negotiated
    evdev (SyncTERM) or kitty (Windows Terminal) keyboard mode -- where a
    modified key arrives as a CSI/evdev report, not a raw control byte -- they
    were silently swallowed during play (the same trap F4 already worked
    around). Route all three input paths through one door_io_hotkey()
    dispatcher; a commit flag fires the action on the press edge only while swallowing repeat/release, so a held key can't re-toggle or leak the
    letter into 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 Saturday, July 11, 2026 04:50:00
    https://gitlab.synchro.net/main/sbbs/-/commit/bdd485515c8db099897fc76a
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncalert: merge REDALERT.INI instead of clobbering it

    door_setup_engine_paths() rewrote the per-user REDALERT.INI from scratch on every launch with only a [Paths] section. That wiped the engine's own
    persisted settings (music/sound volume, etc.) each session, and left
    [Intro] PlayIntro absent -- so it defaulted true, Special.IsFromInstall
    stayed true, and the door auto-started a Soviet campaign game on Normal
    every session instead of showing the main menu.

    Now merge with xpdev's INI reader/writer: read the existing file, (re)write only the runtime [Paths] UserPath/DataPath, and seed [Intro] PlayIntro=false ONLY when it is absent. So a fresh install boots to the main menu (with the Allies/Soviet and difficulty choosers), a sysop's or the engine's own later values survive, and iniWriteFile() rewinds+truncates so everything else in
    the file is preserved. Existing broken per-user INIs self-heal on the next launch.

    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 Saturday, July 11, 2026 04:50:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8d9808e2b1b3f0c5ab2fe126
    Added Files:
    src/doors/syncconquer/door/vqaaudio_termgfx.cpp
    Modified Files:
    src/doors/syncconquer/CMakeLists.txt PROVENANCE.md src/doors/syncconquer/vanilla/redalert/init.cpp src/doors/termgfx/audio_mgr.c audio_mgr.h
    Log Message:
    syncalert: FMV cutscene audio over the terminal

    Red Alert's VQA cutscenes played silent in the door. Anim_Init() enabled captions and EVA but never VQAOPTF_AUDIO, so the VQA loader decoded no
    audio at all; and the vendored common/vqaaudio_null.cpp stubbed the whole
    audio backend. This gives the cutscenes their dialog and effects.

    New door/vqaaudio_termgfx.cpp replaces the vqaaudio_null stub (swapped in
    the door CMake). It accumulates each decoded VQA audio block and ships it
    to the client through a new termgfx streaming path -- Store, Load, then
    A;Queue onto the SyncTERM music-channel FIFO -- so the chunks play back
    to back. Decode is never paused: the player's frame display rides the
    same loop, so pausing it to throttle audio would stutter the video;
    instead a throttle bounds how far the client FIFO is allowed to run
    ahead of playback.

    A/V pacing rides the door's true monotonic wall clock (door_io_now_ms),
    driving BOTH the movie's VQA_GetTime (which the drawer paces frames on)
    and the audio-ship throttle, so video and audio stay locked in real
    time. The vendored 60Hz TimerClass is deliberately not used for this:
    its tick count is delta_ms / (1000/60), and 1000/60 truncates to 16, so
    it runs ~4% fast -- enough to race the video ahead of the real-time
    audio over a minute or two. This mirrors the OpenAL backend, whose
    VQA_GetTime derives from the audio device's real playback position.

    The new shared termgfx API termgfx_audio_stream_chunk() / termgfx_audio_stream_stop() (audio_mgr.c) is reusable by the other
    doors. Vendored engine change recorded in PROVENANCE.md (#14).

    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 Saturday, July 11, 2026 15:00:00
    https://gitlab.synchro.net/main/sbbs/-/commit/969a17630973a69b89d980ad
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/syncmoo1/syncmoo1_input.c src/doors/syncretro/syncretro_audio.c syncretro_audio.h syncretro_input.c syncretro_io.c src/doors/termgfx/apc.c apc.h audio.c audio.h audio_mgr.c audio_mgr.h caps.c caps.h
    Log Message:
    doors: adopt SyncTERM APC blob media (audio + video), no cache files

    SyncTERM's blob APC verbs (Deuce, ef5b7f98a3) carry base64 media inline and decode straight to a slot/screen with no cache file, gated on CTerm version
    = 1.329. This teaches termgfx and all five doors to use them when the client is new enough, falling back to the existing cache path otherwise.

    Detection (termgfx/caps.{c,h}):
    - termgfx_caps_cterm_version() reads the version from the DA1 reply
    ("ESC[=67;84;101;114;109;MAJ;MIN;...c", 67.. spells "Cterm"): MAJ*1000+MIN.
    - Named thresholds TERMGFX_CTERM_VER_{PPM=1002,BLOB=1329} replace scattered
    magic numbers (syncdoom's inline copy now calls the shared helper).

    Audio (termgfx/audio.c, audio_mgr.c):
    - termgfx_audio_load_blob_file()/_load_blob() build A;LoadBlob (WAV/OGG).
    - stream_chunk() ships inline when the manager's blob_ok is set, else the
    legacy Store+Load with rotating cache names. Wins where audio actually
    streams: syncconquer FMV and syncretro game audio (its OGG chunks now skip
    the cache ring). The SFX/music doors keep the upload-once cache (better for
    repeated sounds) -- blob_ok is set there only for uniformity.

    Video (termgfx/apc.c):
    - termgfx_apc_image() gains a blob mode: C;DrawJXLBlob/DrawPPMBlob inline vs.
    the per-frame C;S Store + Draw. Wired in the JXL/PPM-tier doors
    (syncconquer, syncduke, syncdoom). Sixel is already inline, unaffected.

    Each door's DA1 handler enables the audio/video blob flags at >= 1329. The stats overlay shows a blob token only when actually in use (JXL/PPM tier inline, or retro's "a-blob" audio stream); moo1 has none.

    #256 workaround (non-blob clients): the JXL/PPM frame cache filenames were fixed strings, so two SyncTERM windows of one dialing-entry -- which share a cache dir -- collided. Salt the name per session with the door's PID via termgfx_session_salt() (xpdev GetCurrentProcessId): syncalert_<pid>.jxl etc. Only the cache path is affected; blob clients write no file. (syncterm #256.)

    Compile-verified across all five doors; the blob paths need SyncTERM >= 1.329 to live-test, and the fallback is unchanged so older clients are unaffected.

    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 Saturday, July 11, 2026 18:22:00
    https://gitlab.synchro.net/main/sbbs/-/commit/28800fb9114d146eaf591669
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncalert: fit-toggle fixes and a tighter stats overlay

    Three fixes to the Ctrl-F Aspect<->Fill display toggle and the Ctrl-S
    stats overlay:

    - Clearing: door_fit_toggle() forced a repaint but not a screen clear,
    so switching Fill (full-width) -> Aspect (narrower, centered) left the
    old wider frame's pixels bleeding through the newly-exposed side
    margins. Request a clear, as door_tier_cycle() already does when the
    image re-centers.

    - Fill centering: Fill set the emitted width to the whole canvas width
    and centered on that, but door_emit_sixel then clamps sixel width to
    DOOR_SCALE_MAX. On a canvas wider than the cap the image landed
    flush-left with a lopsided right margin. Apply the same cap in
    door_calc_rect (sixel only) before centering so it stays symmetric.

    - Overlay: add a "fill" token (shown only in Fill mode, like "blob"),
    drop the space before the rate unit, round bytes/frame to KB, and
    remove the redundant trailing space, so the widest line (JXL with
    both blob and fill) fits one 80-column row instead of truncating.

    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 Sunday, July 12, 2026 05:11:00
    https://gitlab.synchro.net/main/sbbs/-/commit/01acd2e0d563845cd750a33a
    Modified Files:
    src/doors/syncconquer/door/video_termgfx.cpp vqaaudio_termgfx.cpp
    Log Message:
    syncconquer: software fills + full-volume cutscenes (both doors)

    Two fixes in the door's shared terminal video/audio backend, each
    affecting both syncalert (Red Alert) and syncdawn (Tiberian Dawn).

    video_termgfx.cpp -- force AllowHardwareBlitFills off. The engine's GraphicViewPortClass::Fill_Rect routes fills of >= 32x32 px to the video surface's hardware FillRect() when AllowHardwareBlitFills is set and the surface IsHardware (which video surfaces are flagged as). The door's null backend surface is video_null.cpp's VideoSurfaceDummy, whose FillRect()
    is a no-op -- so every large fill was silently dropped. Dialog
    BACKGROUNDS (a full-dialog Fill_Rect is large) vanished while their
    borders and gadgets (smaller fills / Draw_Rect, always software) still rendered, so the screen behind showed through. There is no real hardware surface here, so route every fill through the software path.

    vqaaudio_termgfx.cpp -- play cutscene audio at full channel volume. The
    FMV audio and the game theme share the terminal music channel;
    VQA_StartAudio flushes the channel queue but did not reset its volume,
    so a cutscene inherited whatever (possibly low or muted) music volume
    the theme left. A briefing shown right after the menu was thus
    (near-)inaudible even though its audio decoded and shipped fine. A
    cutscene's audio is mixed dialog+SFX+music, not "music", so it now
    plays at full channel volume regardless of the music-volume slider.

    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 Sunday, July 12, 2026 05:11:00
    https://gitlab.synchro.net/main/sbbs/-/commit/5ff3d0595588fdef25824877
    Added Files:
    src/doors/syncconquer/door/door_help.c door_help.h
    Modified Files:
    src/doors/syncconquer/CMakeLists.txt PROVENANCE.md src/doors/syncconquer/door/door_input.c door_io.c door_io.h src/doors/syncconquer/vanilla/redalert/scroll.cpp src/doors/syncconquer/vanilla/tiberiandawn/intro.cpp scroll.cpp
    Log Message:
    syncconquer: keyboard scroll, key-help card, F4 fix, TD side-select

    Keyboard/terminal-usability batch across both doors (SyncAlert / Red
    Alert and SyncDawn / Tiberian Dawn).

    Keyboard map scrolling (both titles): the arrow keys scroll the tactical
    map, Home/End jump to the top/bottom edge, and a held key scrolls
    continuously by polling Down() -- kitty/evdev report key releases, so the
    hold state is real; a legacy tap terminal with no key-up scrolls one cell
    per received tap. The mouse edge-scroll auto path is also slowed to a quarter-cell per tick so it glides instead of flying (the door runs
    several game ticks per presented frame). Handled in ScrollClass::AI,
    ahead of Keyboard_Process, so it behaves the same in both engines.

    Numpad decoding: the kitty numpad now respects NumLock (off =
    Home/End/arrows, on = digits) instead of always folding to digits, and
    the SyncTERM evdev path gains the missing numpad Home/End/PgUp/PgDn/Ins/
    Del keycodes (it already had the numpad arrows). Numpad navigation now
    works in both terminals.

    F4 double-fire (Windows Terminal / kitty): kitty reports F4 as "CSI S"
    twice per press -- a bare press and a "CSI 1;1:3S" release. The 'S'
    handler cycled the graphics tier on both edges; it now parses the kitty
    event type and cycles on the press only. The rxvt ESC[14~ path is
    hardened the same way.

    Key-bindings help card (Ctrl-K or F1): a door-side centered panel listing
    the game commands and the door/terminal keys, since the engine has no
    key-list screen. Esc closes it, and the present loop freezes the frame
    under it so it doesn't flicker in Sixel. F1/Esc are handled in emit_key,
    the one point every keyboard mode funnels through.

    TD side selection: Choose_Side was mouse-only. Left/Right now pick
    GDI/Nod, Tab toggles a highlighted (outlined) side, and Enter/Space
    confirms it.

    Vendored edits are recorded in PROVENANCE.md (#29-31); the door-side
    files (door_help.*, door_io.*, door_input.c) are ours.

    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 Windows@VERT to Git commit to main/sbbs/m on Monday, July 13, 2026 00:36:00
    https://gitlab.synchro.net/main/sbbs/-/commit/54de3085923bf9535feec2ad
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: write the INI the ENGINE looks for, per title (syncdawn)

    SyncDawn replayed Tiberian Dawn's from-install path on every launch -- the
    long intro, then Choose_Side, then straight into a mission -- instead of reaching the main menu.

    door_setup_engine_paths() hardcoded "REDALERT.INI" as the file it writes into the per-user -home dir, for both titles. But each engine asks PathsClass::Init() for its OWN config filename -- redalert/startup.cpp passes "REDALERT.INI", tiberiandawn/startup.cpp passes "CONQUER.INI" -- and
    PathsClass looks for exactly that name. Under syncdawn the door was therefore writing a file the engine never opens, so [Intro] PlayIntro=false never
    reached it, Get_Bool("Intro", "PlayIntro", true) defaulted true, and Special.IsFromInstall fired every session (tiberiandawn/startup.cpp).

    Add DOOR_ENGINE_INI to the per-title identity block (CONQUER.INI for TD, REDALERT.INI for RA) and write that.

    It hid behind a door that otherwise worked. TD takes its mixes from door_engine_data_dir() rather than the INI's [Paths] DataPath, so the game
    data loaded fine and nothing looked wrong -- while PlayIntro, and the savegame location, silently never reached the engine.

    It also LOOKED platform-specific, and wasn't. With no INI beside argv[0] the engine's ini.Save() falls back to PathsClass::User_Path(): ~/.config/vanilla-conquer/vanillatd on POSIX, %APPDATA%\Vanilla-Conquer\vanillatd on Windows. That write persists for the *nix BBS user -- so from the second run on, Linux found PlayIntro=false and behaved, quietly papering over the bug -- but not for the Windows service account, where every run was thus a first run. That fallback is wrong for a door either way: it is ONE file shared by every caller, the very thing -home exists to prevent, and it is where TD's savegames had been going instead of
    the player's own directory.

    Verified on both hosts: data/user/<n>/tibdawn/CONQUER.INI is now created with our [Paths]/[Intro] plus the engine's own persisted [Mouse]/[Video] settings (the merge-don't-clobber path holding), and syncdawn boots to the main menu. The REDALERT.INI previously left in a tibdawn/ home is inert -- TD never read it -- and can be deleted.

    ---
    þ 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 Monday, July 13, 2026 21:37:00
    https://gitlab.synchro.net/main/sbbs/-/commit/561cd5b998b75665f085c391
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke_io.c src/doors/syncmoo1/syncmoo1.h syncmoo1_input.c syncmoo1_io.c src/doors/syncretro/syncretro.h syncretro_input.c syncretro_io.c
    Log Message:
    doors: size the image to what the terminal will DRAW, not to its window

    SyncDOOM (and SyncDuke) rendered all black in an xterm bigger than its default size. The image was fine; xterm was throwing it away whole.

    A sixel whose DECLARED raster exceeds the terminal's graphics limit is not clipped -- xterm discards the entire image (graphics_sixel.c, GetExtent: "raster width %d > max %d" -> finished_parsing + return, nothing drawn). The limit is maxGraphicSize, which defaults to 1000x1000. So any window whose canvas is wider than ~1000px got a sixel xterm refused outright, and the
    screen stayed black; the default small window stayed under the limit and worked, which is exactly the reported symptom.

    And the terminal TELLS us the limit -- we were discarding the answer. XTSMGRAPHICS (ESC[?2;1S) reports Min(window, graphics_max) -- the biggest
    sixel it will accept -- and every door already sends the query as part of termgfx_term_probe. But xterm answers BOTH that and ESC[14t, and the doors either preferred the ESC[14t window size or never parsed the graphics reply at all, so they fitted the image to a window the terminal would not draw into.

    The graphics geometry is now authoritative in every door that uses termgfx, whichever order the replies arrive in:

    SyncDOOM used XTSMGRAPHICS only as a fallback when ESC[14t was silent --
    so on xterm it fitted the 1920x1080 window, emitted a 1280x800
    sixel, and got it discarded. Now prefers it.

    SyncDuke fitted the sixel to the ESC[14t window (syncduke_update_outsize)
    even though syncduke_canvas_*() already preferred the graphics
    geometry for JXL -- and its OUT_W_MAX is 1024, itself over
    xterm's limit. Now fits the canvas.

    SyncMOO1 never parsed the reply at all (only ESC[14t). Added, and it
    outranks the window.

    SyncRetro likewise never parsed it -- and worse, its F4 handler assumed "no
    terminal SENDS a CSI S as input", so the XTSMGRAPHICS reply (CSI
    ? 2;0;W;H S) fell through as an F4 KEYPRESS and silently cycled
    the render tier at startup on any terminal that answers it. The
    '?' private intro now routes it to the canvas handler instead,
    fixing both.

    SyncConquer already landed on the right value, but only because the 't'
    reply happened to arrive before the 'S' one. Made explicit rather
    than left to ordering.

    Verified by driving SyncDOOM through a simulated xterm (answering ESC[14t with 1920x1080, ESC[?2;1S with 1000x1000, DA1 with sixel): it now sizes to win=1000x1000 and emits a 1000x625 image, inside the limit. The old code
    fitted the window and produced 1280x800 -- discarded.

    Pre-existing, not a regression from the recent sixel-scaling work: the width path is unchanged by it. It surfaced now because that work is what got xterm tested at size.

    ---
    þ 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 Monday, July 13, 2026 21:37:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d45db994a03ca93e4f80858b
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke_input.c src/doors/syncmoo1/syncmoo1_io.c src/doors/syncretro/syncretro_io.c src/doors/termgfx/geometry.h
    Log Message:
    doors: assume xterm's 1000x1000 sixel ceiling when nothing advertises one

    The previous commit made the terminal's XTSMGRAPHICS graphics geometry authoritative, which was right but useless against the terminal that motivated it: xterm ships with window operations DISABLED, so it answers NEITHER ESC[14t NOR the XTSMGRAPHICS query. With no reply to act on, the doors fell back to sizing the image from cols*cell -- the only geometry they can still get -- and a large window yields a large estimate.

    From the reported session's log: cols=369 rows=90, cell=6x13, so win=2214x1170 (369*6 x 90*13, derived, not reported) and image 1024x640. That sixel declares a raster wider than xterm's maxGraphicSize, which defaults to 1000x1000, and xterm does not clip an oversized image -- it aborts the parse and draws
    NOTHING (graphics_sixel.c: GetExtent -> finished_parsing + return). Hence a black screen in any big xterm window, and a correct picture in a small one.

    So the ceiling has to be assumed rather than queried. TERMGFX_SIXEL_SAFE_MAX (1000) caps the canvas in every termgfx door whenever no graphics geometry was advertised. A terminal that can take more says so and gets the bigger image; a silent one now gets a picture instead of a void.

    Verified against both shapes of terminal, driving SyncDOOM through a pty: replaying the reported xterm (answers nothing, 369x90 grid, 6x13 cell) it now sizes win=1000x1000 -> image 1000x625, inside the limit, where before it built 1024x640 and had it discarded; and a SyncTERM that answers XTSMGRAPHICS with its real 640x400 canvas is UNCHANGED at image 640x384, since the cap never engages when the terminal advertises.

    ---
    þ 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 Monday, July 13, 2026 21:37:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b42e26c050046c896233f631
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/syncdoom/syncdoom.c src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/syncmoo1/syncmoo1_io.c src/doors/syncretro/syncretro_io.c src/doors/termgfx/geometry.c geometry.h
    Log Message:
    doors: clamp the IMAGE to the graphics ceiling, not the canvas

    The previous commit stopped the black screen by capping the canvas to xterm's 1000x1000 default when the terminal advertises no graphics geometry. That
    fixed the rendering and broke the centering: the canvas is also what the image is CENTERED AGAINST, so a 1000-wide image was centered inside a 1000-wide "canvas" and pinned to the left of a 2214-wide xterm window. Doom sat centered under foot (which reports a canvas) and hugged the left edge under xterm.

    Two different quantities were being conflated. The graphics geometry is a
    limit on what the terminal will DRAW -- exceed it and the sixel is discarded whole. The canvas is the area to CENTER IN, and it is the real terminal, however big. Clamp the fitted image to the former; keep measuring against the latter.

    termgfx_geom_gfx_clamp() shrinks a fitted image to the ceiling preserving aspect, and every door now applies it to the FIT result while centering
    against the true canvas. SyncDOOM and SyncDuke clamp the fit inputs directly (they build the fit themselves); SyncMOO1, SyncRetro and SyncConquer clamp the fitted image after the fact, which leaves their page/reserve math untouched.

    Verified by driving SyncDOOM through a pty against both shapes of terminal: replaying the reported xterm (2214x1170, advertises nothing) it now emits
    image 1000x625 @607,266 -- inside the ceiling AND centered, since
    (2214-1000)/2 = 607 -- where the previous commit produced a correctly-sized image pinned at the left. A SyncTERM that advertises its 640x400 canvas is unchanged at image 640x384, the cap never engaging.

    ---
    þ 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 Monday, July 13, 2026 21:37:00
    https://gitlab.synchro.net/main/sbbs/-/commit/a6db70e358aa19e785fb7553
    Modified Files:
    src/doors/syncconquer/door/door_input.c src/doors/syncdoom/syncdoom.c Log Message:
    syncdoom,syncconquer: resolve a lone ESC on a timer, not on the next key

    Pressing Escape in xterm did nothing until another key was pressed -- and if that key was Escape too, both arrived at once and the menu opened and
    instantly closed again.

    ESC is ambiguous: the Escape key and the first byte of every escape sequence (arrows are ESC[A) are the same 0x1b. The parser cannot classify it until it sees whether a byte follows, so it parks in the ESC state -- and neither door ever left that state on its own. The pending ESC was only flushed when the
    NEXT byte arrived, which is exactly the reported behavior.

    A real escape sequence arrives in one burst, so a follow-up byte is either there immediately or it is never coming. Deliver the pending ESC once that window (50ms) has elapsed, on a pump that reads nothing.

    SyncTERM hid this: it reports Escape as an evdev/kitty key event, never a bare 0x1b, so the byte path is never entered. A legacy-key terminal like xterm
    takes the byte path every time.

    SyncDuke (SYNCDUKE_ESC_MS) and SyncMOO1 (SM_ESC_MS) already did this; SyncDOOM and SyncConquer never got it. SyncRetro needs nothing -- a lone ESC is not a key on a RetroPad, and it says so.

    Not live-verified: the doors exit within a second without a real door socket, so the pty harness cannot hold one open long enough to drive a keystroke through it. The mechanism is a direct port of the one shipping in SyncDuke and SyncMOO1.

    ---
    þ 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 Tuesday, July 14, 2026 21:04:00
    https://gitlab.synchro.net/main/sbbs/-/commit/736866fe916b7f150db638f8
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: scale the image to the exact canvas on any terminal

    The sixel/image size clamp only trusted an XTSMGRAPHICS-derived ceiling (g_canvas_is_gfx), falling back to a conservative 1000px "geometry-less
    xterm" floor otherwise -- so Windows Terminal, which reports an exact pixel canvas via ESC[4;h;wt but doesn't answer XTSMGRAPHICS, had its image
    clamped to 1000px and rendered small in a large window. Trust the exact
    canvas (g_px_exact) whenever any terminal reports it -- WT, SyncTERM, any modern sixel terminal -- keeping the 1000px floor only for genuinely geometry-less terminals (xterm with window-ops off never sends ESC[4t, so g_px_exact stays false and it's still protected). Large pixel canvases
    (e.g. 2700x1440) only ever come from non-SyncTERM terminals anyway, so the prior effectively-SyncTERM-only gating was exactly backwards.

    Also add a geometry-provenance tag to the Ctrl-S stats bar (S/X/G =
    SyncTERM detected / exact-px canvas / XTSMGRAPHICS gfx ceiling), which made this diagnosable at a glance.

    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 Tuesday, July 14, 2026 21:04:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d9d8d36916532df76f9616c2
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: Phase-2 dirty-rect JXL rendering (~2.5x bandwidth cut)

    On the JXL tier, only re-send the parts of the map that actually changed between frames instead of re-encoding the whole 640x400 canvas every time.

    Each changed frame is diffed against the previous one at 16x16-tile granularity; the dirty tiles are coalesced into a small set of tight
    bounding boxes (4-connected component labeling plus a short-gap merge
    pass), and each box is shipped as its own pixel-positioned JXL blob (DrawJXLBlob) that composites in place over the retained frame. A frame
    that changes too much (>= DR_FALLBACK_PCT of tiles) or produces too many
    boxes falls back to a normal full-frame encode, as does the first frame,
    a palette change, or any fit/geometry change.

    Live-measured on real play this drops a moving map from 50+ KB/frame to
    under 20 KB/frame. The path is gated by a data/<door>/dirtyrect flag that
    is re-checked about every 1.5s, so it can be toggled on and off mid-
    session without relaunching the door.

    Stats bar (Ctrl-S) tweaks that go with it: KB/f now reports the recent
    windowed per-frame cost (from the ~1s bits/s + fps windows) instead of
    the lifetime cumulative average, so toggling dirty-rect shows an
    immediate drop; and the redundant cell-size field was removed and KB/f
    moved to the front of the row so it no longer gets chopped.

    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 Friday, July 17, 2026 02:53:00
    https://gitlab.synchro.net/main/sbbs/-/commit/44e5ea5042f6098243ea2a83
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: sixel-tier dirty-rect rendering (partial-sixel backend)

    Extend the Phase-2 dirty-rect path to the sixel tier -- the non-JXL
    fallback population: xterm / mlterm / wezterm / Windows Terminal, plus a
    no-JXL SyncTERM. The framebuffer differ + coalescer are hoisted out of
    the WITH_JXL guard into a shared dr_diff_coalesce() that both backends
    call; only the per-tier emit differs.

    Sixel can only be placed at a character cell (unlike a pixel-exact JXL
    blob), so each coalesced box is snapped OUT to cell boundaries and its
    top-left cell is addressed with the cursor, then re-emitted as its own
    partial sixel that composites over the frame the client still holds. The
    box height is rounded up to a 6-px sixel band to dodge SF #258 (SyncTERM mis-decodes a partial last band). On SyncTERM the palette registers
    persist from the prior full frame so boxes ship palette-less; other sixel terminals reset registers per image, so each box re-sends the palette.

    Same data/<door>/dirtyrect flag and ~1.5s live re-check as the JXL path,
    and the same full-frame fallback on a large or over-fragmented change --
    so it is never worse than a full frame. The saving scales with the static (unchanged) fraction: largest on a built-up map, near zero on a sparse animating scene. The ratio is lower than JXL's because sixel is
    cell-snapped and compresses locally rather than globally, and it is
    smallest on a huge canvas (Windows Terminal) where every box inflates and
    the palette re-sends per box. Live-tested in Windows Terminal (sixel
    tier): composites cleanly, frame size swings down with map development.

    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 Friday, July 17, 2026 02:53:00
    https://gitlab.synchro.net/main/sbbs/-/commit/c59d086d6d5b7c5cd4270cce
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: omit the charset field from the stats bar in graphics tiers

    The Ctrl-S debug bar's utf8/cp437 indicator only matters in the text
    tiers (half/quadrant/sextant), where the glyph encoding is actually used
    to render the frame. In the image tiers (sixel/JXL/PPM) it is just noise
    on an already-long row, so show it only for the text tiers. The leading
    space is folded into the field (as blob/fill already do) so suppressing
    it leaves no gap.

    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 Friday, July 17, 2026 02:53:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b3ce4522b8b3e4474a4d840b
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: default dirty-rect on, sysop-toggle via <door>.ini [video]

    Dirty-rect was gated behind a data/<door>/dirtyrect touch-file as A/B-test scaffolding -- off unless the file existed, re-checked live so it could be toggled mid-session. Now that both backends (JXL + sixel) are proven, make
    it the default and retire the dev flag.

    It is ON unless a sysop sets "[video] dirty_rect = false" in the door's
    config .ini (syncalert.ini / syncdawn.ini beside the binary) -- the escape hatch for a terminal that mis-composites the partial updates, needing no rebuild. The .ini path + bool read is factored into a shared
    door_config_bool() (also used by the existing [game] movies setting), and
    the value is read once and cached since it is queried every present.

    Validated the [video] dirty_rect read (absent file -> default on; = false
    off; = true -> on; key absent -> default) against xpdev's iniReadBool.

    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 Friday, July 17, 2026 02:57:00
    https://gitlab.synchro.net/main/sbbs/-/commit/4184e41f337e65d5459ba907
    Modified Files:
    src/doors/syncconquer/door/vqaaudio_termgfx.cpp
    Log Message:
    syncconquer: give FMV cutscene audio headroom (was clipping)

    A mission-end / briefing FMV distorted -- the mixed dialog+SFX+music track
    was played at A;Volume 100 (0 dB, linear-amplitude percent), alone on the just-stopped movie channel, straight into the mixer's tanh soft-clip knee (termgfx README gain model). Play it a few dB down via a named
    MOVIE_AUDIO_VOL (70, ~-3 dB) instead of 100: still well above any muted-music level, so briefing/victory dialog stays clear, but with headroom against the saturator. Fixed cutscene level, independent of the music slider as before.

    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 Friday, July 17, 2026 21:23:00
    https://gitlab.synchro.net/main/sbbs/-/commit/9e71f98f98f9e52b17c77de7
    Modified Files:
    src/doors/syncconquer/door/soundio_termgfx.cpp vqaaudio_termgfx.cpp src/doors/syncdoom/i_termmusic.c i_termsound.c src/doors/syncduke/syncduke_stubs.c src/doors/syncmoo1/syncmoo1_audio.c syncmoo1_music.c src/doors/syncmoo1/tests/test_audio.c src/doors/syncretro/test_audio_bytes.golden src/doors/termgfx/audio.c audio.h audio_mgr.c audio_mgr.h audio_stream.c
    Log Message:
    termgfx: express SyncTERM audio volume in decibels, not percent

    SyncTERM's mixer is dB-native -- channels open at a -12 dB base and gains
    are 10^(dB/20) -- and A;Volume/A;Queue take a `<float>dB` value verbatim
    and UNCLAMPED. The audio APIs sent bare 0..100 percentages instead, which
    clamp at unity, are integer-only, and encode real dB gain-staging as
    opaque numbers: SD_MUSIC_MAXV 20 (= -14 dB), "queue at 25% (~ -12 dB)",
    "100% ran loops up to 12 dB hot". A percentage also cannot ask for boost.

    Move the whole termgfx audio stack to dB. The A;Volume/A;Queue builders
    and every audio_mgr entry point (sfx, loops, music, stream) take a dB
    level; the internal music-unity and loop-base trims become explicit dB;
    and termgfx_db_from_pct() does the one faithful percent->dB conversion
    (100 -> 0 dB, <=0 -> the -60 dB floor).

    This pass is behavior-preserving. Every door call site is wrapped so the
    wire carries exactly the dB SyncTERM used to compute from the old
    percent, so nothing changes loudness -- verified by ear across all five
    doors and by the regenerated APC goldens (V=100 -> V=0.0dB = unity). The per-door perceptual re-mappings -- readable TRIM_DB constants, a slider
    that moves linearly in dB, and boost above unity where wanted -- follow separately, one door at a time.

    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 Saturday, July 18, 2026 03:02:00
    https://gitlab.synchro.net/main/sbbs/-/commit/bb8096e4d445077c979888b5
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: use termgfx's shared mouse handshake

    Delegate the DEC 1003/1006/1016 enable/restore, the SGR-Pixels latch and
    the DECRPM reply to termgfx/mouse instead of open-coding them here. The
    emitted bytes and the coordinate mapping are unchanged -- door_input.c
    and the door_io_* accessors keep their behaviour; only their backing
    state moves into a termgfx_mouse_t. First consumer of the extraction.

    ---
    þ 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 Tuesday, July 21, 2026 01:49:00
    https://gitlab.synchro.net/main/sbbs/-/commit/17dad6930d25c946b9cc999d
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/termgfx/term.c term.h Log Message:
    syncconquer: don't trust xterm's text-area size as a sixel ceiling

    Both C&C titles rendered an all-black screen in a MAXIMIZED xterm (sixel
    mode) while a small xterm window and Foot played fine. Cause: xterm
    discards a sixel whose declared raster exceeds its maxGraphicSize
    (~1000x1000) whole rather than clipping it, and door_calc_rect() was
    trusting the terminal's exact pixel canvas (ESC[14t) as the sixel
    ceiling whenever it was reported -- but xterm's ESC[14t answer is its
    big TEXT-AREA size (~1900px maximized), not its graphics ceiling. So the
    door emitted a 1833x1146 sixel that xterm dropped entirely. The later click-to-see-buttons was the dirty-rect path emitting small (<1000)
    partial sixels that squeaked under the ceiling. (SyncDuke/SyncDOOM clamp
    to the safe ceiling unless XTSMGRAPHICS is reported and never had this; syncconquer alone trusted the exact canvas, to fill WT/Foot windows.)

    Identify xterm via XTVERSION and resolve the sixel ceiling in precedence
    order: a reported XTSMGRAPHICS ceiling, else an exact canvas ONLY when
    not xterm (Windows Terminal / Foot / SyncTERM render well past 1000px),
    else the safe ~1000px floor. xterm now clamps to 1000x625 and renders;
    other terminals keep their full window size. The negotiated geometry log
    line and Ctrl-S stats gain an xterm marker.

    Adds termgfx_term_parse_xtversion() to the shared termgfx/term.{c,h} -- a rolling-window raw scan of the XTVERSION reply (DCS >|<name>(<ver>) ST),
    a sibling of termgfx_term_parse_status() -- so the doors that roll their
    own terminal I/O can identify xterm. This ports into syncconquer's
    private terminal-I/O copy the fix the shared termgfx_termio.c already
    carries.

    ---
    þ 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 Wednesday, July 22, 2026 20:38:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8fd7b6f2ca768062cab36f9e
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: subset the sixel palette on register-resetting terminals

    Select SIXEL_PAL_USED for the full-frame and dirty-box sixel paths on non-SyncTERM clients; SyncTERM keeps FULL-on-change / NONE-reuse
    unchanged. Matches the termgfx present path.

    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 Thursday, July 23, 2026 00:08:00
    https://gitlab.synchro.net/main/sbbs/-/commit/dd3251e868afce35dcc6005f
    Modified Files:
    src/doors/syncconquer/door/door_input.c door_io.c door_io.h door_node.c door_node.h xtrn/syncalert/syncalert.example.ini xtrn/syncdawn/syncdawn.example.ini
    Log Message:
    syncconquer: end the game when the player has stopped playing

    The second door to adopt the shared idle-user clock, and the reference for the ones with no JS lobby. Same reasoning as SyncRetro: the BBS's own "Maximum Inactivity" is structurally inert here, because it is reset by any socket traffic and DSR frame pacing makes the terminal answer ~10x/second on its own. Presence has to be judged on real input, which only the door can tell apart from its own pace-acks.

    Warn-then-terminate, with the countdown on the bottom row and the same wording the sibling door uses. Both keyboard AND mouse feed the clock, and mouse
    MOTION counts, not just clicks -- Red Alert is mouse-driven, so a player commanding units with no keystrokes at all must not be judged absent. Configured by -i<seconds> or the door's own [idle] section, defaulting to 10 minutes; the argument wins when given, so a launcher can pass -i0 to excuse an exempt user.

    Input that answers the countdown is CONSUMED rather than delivered. "Press any key" must not also fire whatever that key does -- F1 would open the help card, Esc the Options menu, a click would issue an order to the selected unit. The hook therefore sits above the F1/Esc handling in emit_key(), and the release edge still falls through so no key is left stuck down.

    Three things live testing found that no offline run would have:

    - The overlay never erased itself. banner_live() simply stopped drawing, and
    since the game picture does not reach the margins, nothing repainted those
    cells -- a dismissed countdown sat on screen indefinitely. It now records
    what it drew and wipes those rows. This fixes the page and who's-online
    notices too, which had the same fate anywhere the image does not cover.
    - Wiping is not enough on its own. door_io_present() skips the whole overlay
    draw on a deduped frame, so an erase queued while the game sat at a static
    menu was never emitted. A pending erase now keeps the overlay "active" for
    the one extra frame it takes to flush.
    - ...and the wipe clears a whole row, which on a top-anchored banner cuts a
    black band through the game UI. So it also forces a full repaint, letting
    the next present redraw the picture over those rows.

    The countdown is bottom-anchored while the multi-row page/who's-online banners stay on top, so it lands where this door already puts its Ctrl-S stats strip, and where the sibling door puts the same countdown. The strip yields the row while a notice is up and is forced back when it clears, since it otherwise repaints only when its own text changes.

    -i is parsed AND neutered in door_sanitize_argv(). That matters more here than in the sibling: this door really does hand argv to Vanilla Conquer's Parse_Command_Line(), so an unstripped flag could misfire an engine option.

    A repaint request cannot simply clear g_have_last, incidentally: the overlay draws near the END of a present, and that same call then sets g_have_last = 1 and swallows it. It lives in its own flag, consumed at the top of the NEXT present -- and forces a whole frame rather than a dirty-rect patch, because
    the wipe changed the screen without changing the framebuffer, so a diff
    against the unchanged previous frame would find nothing to send.

    Verified offline against both binaries (-i fires to the millisecond, -i0 never does, the ini alone works with no -i, and an explicit -i overrides it), and on a live node for the parts capture mode cannot show: the countdown renders and clears, and mouse motion dismisses it (with the pointer over a focused window -- a terminal reports no motion to an unfocused one).

    KNOWN ROUGH EDGE, left as-is: overlapping notices still leave artifacts, e.g.
    a node message arriving while the who's-online listing is up. The repaint machinery above assumes one banner at a time, and a second notice replacing a live one does not re-run the wipe for the rows the first occupied.

    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 Friday, July 24, 2026 23:28:00
    https://gitlab.synchro.net/main/sbbs/-/commit/00b8ac4aab71c2dfdceb256d
    Modified Files:
    src/doors/syncconquer/door/door_io.c xtrn/syncalert/syncalert.example.ini xtrn/syncdawn/syncdawn.example.ini
    Log Message:
    syncconquer: turn away a terminal that cannot draw the game

    An RTS at 80x25 in half-block glyphs is something to look at, not to play
    in, but a terminal that answered the capability probe without pixel
    operations was auto-dropped into that tier and left to work out for
    itself why the game was unusable. Gate the present path on the shared
    verdict (termgfx/gfxgate.h): say so plainly and return to the BBS, after holding the notice long enough to be read -- a keypress or [text] no_graphics_pause seconds, whichever comes first. [text] no_graphics and no_graphics_file let the sysop word it.

    The auto tier ladder loses two rungs that the gate makes unreachable, and
    one of them was wrong anyway. CTDA capability 4 is a single bit meaning
    "pixel operations", covering sixel and PPM together (cterm.adoc), so no SyncTERM can offer PPM without also offering sixel: the PPM rung could
    only ever be taken by a SyncTERM that had NOT advertised pixel
    operations, which is one predating cterm's PPM support by years and
    unable to decode DrawPPM at all. Both tiers remain reachable through the
    F4 cycle, where choosing one is deliberate.

    An explicit F4 choice still bypasses the gate, and a terminal that never answers device-attributes still plays: silence is not a refusal.

    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 Friday, July 24, 2026 23:28:00
    https://gitlab.synchro.net/main/sbbs/-/commit/dd0ebc1455b32147f12300db
    Modified Files:
    src/doors/syncconquer/door/door.h door_io.c
    Log Message:
    syncconquer: drop the PPM tier

    No terminal could reach it. CTDA capability 4 is a single bit meaning
    "pixel operations", covering sixel and PPM together (cterm.adoc), so a
    SyncTERM reporting PPM always reports sixel too -- and sixel was tested
    first. The only client that could have fallen through to PPM was a
    SyncTERM that had NOT advertised pixel operations, which predates cterm's
    PPM support by two years and cannot decode DrawPPM at all. cterm gained
    sixel in 2018 and PPM in 2022; there has never been a build with one and
    not the other.

    That left PPM as a manual F4 choice for the two-year window of SyncTERM
    builds having PPM but not yet JXL (2024) -- where it is strictly worse
    than the sixel those same clients already have: 640x400 ships about a
    megabyte a frame against sixel's ~150KB, and a raw P6 carries no
    colorspace tag, so SyncTERM decodes our sRGB pixels as BT.709 and the
    colors come out wrong.

    Removes the tier, its encoder, and its buffers. Sixel and JXL are now the
    only pixel tiers, which is what the dirty-rect paths already assumed.

    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 Saturday, July 25, 2026 23:10:00
    https://gitlab.synchro.net/main/sbbs/-/commit/380a07db7630ebed03b09572
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: use the shared dirty-rect diff

    The third and last copy of it. This door marked a 16px tile grid from two
    index planes, labelled 4-connected components, merged boxes within a gap
    and gave up past a coverage or fragmentation budget -- the same code as syncretro and termgfx_termio, agreeing with the latter on every constant
    that mattered (128 components, gap 2, 45%) and differing only in what a
    tile spans.

    That is what ../../termgfx/dirty.h parameterises, so the diff half goes
    and dr_diff_coalesce becomes a call. Its own post-merge cap of 16 boxes survives as cfg.max_boxes; nothing else about the behaviour changes,
    including which frames fall back to a full one.

    What stays here is what the shared module deliberately does not take: the mapping from tile boxes to something drawable, which in this door means
    two backends -- JXL blobs placed pixel-exact, sixel snapped to character
    cells.

    Both binaries build; neither has been run. This door needs game data and a terminal to exercise, so syncalert and syncdawn want a play-test before
    this is trusted.

    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 Saturday, July 25, 2026 23:10:00
    https://gitlab.synchro.net/main/sbbs/-/commit/f8589e01f493bd50dec0ce90
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: patch a palette change instead of repainting the frame

    The same gate the sibling doors carried: any change to the palette forced
    a whole frame, on both backends. The reasons were real -- the JXL backend
    bakes the palette into each frame's RGB pixels, so a fade moves every
    affected pixel's colour while leaving the index buffer the diff looks at untouched; and sixel colour registers are shared with what is already
    drawn, so redefining one recolours the screen under the parts a patch
    would not repaint.

    Both are about which pixels a moved entry actually affects, and that is computable. Record WHICH entries moved and let the shared diff treat a
    tile drawn with one of them as dirty even when its indices are identical (dirty.h's `stale`); the gate then has nothing left to protect. A change
    that reaches no displayed pixel at all -- an engine rewrites its palette
    freely and a scene draws with a fraction of it -- now de-dupes instead,
    and is deferred rather than dropped: that path updates no state, so the
    entries stay pending for the first frame that draws with them.

    A SyncTERM box carries no palette by design, so the first box of such a
    frame carries exactly the entries that moved. Every other terminal resets registers per image and its used-colour subset already covers it; the JXL backend carries pixels and needs nothing.

    Measured on the sibling that could be measured -- Flight of the Amazon
    Queen over Windows Terminal -- this gate was 267 frames carrying 77% of everything the door sent, after an earlier fix had already halved the
    total. This door's content is different (a strategy map with a static
    sidebar, not an adventure scene) so the share will differ, but the gate
    is the same and so is the reasoning.

    UNVERIFIED beyond compiling: syncalert and syncdawn need game data and a terminal, and neither was run. Worth a play-test on both, watching for a
    region that keeps a stale colour -- that is what this class of change
    breaks when it is wrong.

    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 Sunday, July 26, 2026 01:15:00
    https://gitlab.synchro.net/main/sbbs/-/commit/f690ef1228fe8942ab89f543
    Modified Files:
    src/doors/syncconquer/door/door_help.c
    Log Message:
    syncconquer: line up the last help row, and name one key to close

    The music-volume row was indented a column further than every other row
    in the card, so its description sat one past the column the two-column formatter puts every other description on. Moving the row one column left
    lands it back on that column, beside "This help", "Who's online" and
    "Page a node". The key keeps a space of its own indent so "+ / -" does
    not read as another Ctrl chord in a block of them.

    The closing line offered three ways out where one will do. It now reads
    "ESC to Close", centered the way the title bar is -- computed from the
    panel's inner width rather than hand-counted spaces, so it stays centered
    if either the text or the width changes. Nothing is lost by not naming
    the other two: F1 and Ctrl-K appear in the body as "This help", and all
    three still close the card.

    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 Sunday, July 26, 2026 01:15:00
    https://gitlab.synchro.net/main/sbbs/-/commit/7ee248d3313b59a9f9970a60
    Modified Files:
    src/doors/syncconquer/door/door_io.c
    Log Message:
    syncconquer: map the mouse against where the sixel actually lands

    Clicking the top of a sidebar button did nothing on a sixel terminal;
    the click had to land noticeably lower to register. SyncTERM looked fine,
    which pointed suspicion at mouse granularity -- pixel reports there, cell reports elsewhere -- and that was the wrong trail.

    A sixel is placed by CURSOR ADDRESS, so its true top-left is the addressed cell's pixel: the centering offset rounded DOWN to the cell grid. The mouse mapper inverted the offset the centering math ASKED for instead, so every
    click was biased by the remainder. On a 1330x1480 canvas with 20px cells a 1330x831 image is centred at y=314 but drawn at y=300, and those 14 pixels
    are about 7 game pixels of upward bias -- comfortably the top of a button.
    JXL is placed by pixel offset and needs no correction, which is the real
    reason SyncTERM escaped: it draws on the JXL tier, and at 640x400 its sixel tier happens to round to zero anyway.

    The mapper now derives its origin from the addressed cell on the sixel
    tier, and keeps the pixel offset on JXL. Measured against the door's own geometry across three cell sizes -- SyncTERM 8x16, Windows Terminal 20x20
    and foot's 13px cell -- the error is now zero on both axes in each.

    The geometry line also reports where the mapper believes the image starts.
    It already reported where the image was PUT; the two being different was
    the entire bug, and nothing on that line could have shown it.

    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 Sunday, July 26, 2026 15:23:00
    https://gitlab.synchro.net/main/sbbs/-/commit/6b95aa66c0744940c58c7541
    Modified Files:
    src/doors/syncconquer/door/door_io.c src/doors/termgfx/stats.c stats.h termgfx_termio.c src/doors/termgfx/test/test_stats.c
    Log Message:
    termgfx: one spelling for the mouse mode, and say which field is the turn key

    Two doors printed a "<kbd>/<x>" token in the same place on the Ctrl-S row
    where x meant different things: SyncConquer's second field is the MOUSE granularity, the shared one's is the TURN-KEY model. "kitty/cell" and "kitty/nat" are a character apart and unrelated, which makes the field
    worse than absent -- a reader cannot tell which door they are looking at
    from the shape of it.

    The turn model now says so: " kitty turn=nat". The mouse mode gets its own shared token, " mouse=px" or " mouse=cell", and termgfx_termio carries it
    too -- only when the door asked for a mouse, so a keyboard-only door does
    not advertise an input it never reads. Until now the termgfx doors showed
    the mouse mode nowhere on the row, only in a per-report trace, so
    answering "is this terminal pixel-granular?" for SyncSCUMM meant opening SyncConquer to find out.

    That question has an answer worth recording: Windows Terminal does not implement DEC 1016, in either 1.24 or 1.25, so a click there is quantised
    to a cell -- about 10 game pixels on a 20px cell. Nothing door-side
    changes that; the systematic bias that WAS ours to fix is fixed.

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

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