• src/doors/syncduke/syncdu

    From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Saturday, June 27, 2026 03:03:00
    https://gitlab.synchro.net/main/sbbs/-/commit/20ddeb0b1d9ad114fa1b4131
    Added Files:
    src/doors/syncduke/syncduke_log.c
    Modified Files:
    src/doors/syncduke/CMakeLists.txt syncduke.h syncduke_config.c syncduke_door.c syncduke_input.c syncduke_io.c src/doors/syncduke/tests/test_keymap.c
    Log Message:
    syncduke: debug log, crash handler, and resilient door I/O

    Add an optional file debug log (syncduke_log.c), independent of the BBS's stderr/syslog capture -- a door that drops back to the BBS otherwise leaves nothing logged, and WER minidumps aren't configured on this host:
    - enabled via syncduke.ini [debug] log, env SYNCDUKE_LOG, or -log <path>; a
    relative path lands in each player's per-user dir. Timestamped, flushed
    per line so a crash/hangup still leaves the tail on disk.
    - installs a last-resort crash handler (SetUnhandledExceptionFilter on
    Windows, SIGSEGV/etc on *nix) that records the fault to the log, plus an
    atexit marker -- so hangups, clean exits, and crashes are all captured.
    - hangup() and the send/recv error paths log the reason incl. WSA codes.

    Make transient socket errors non-fatal: send()/recv() returning WSAENOBUFS
    or WSAEINTR are backpressure (retry next tick), not a dead client. A burst
    of large frames (e.g. holding a key, which defeats the frame de-dupe) can surface WSAENOBUFS, which previously dropped the player back to the BBS.

    Also: clamp the mouse-sensitivity slider floor to 1 (0 = no turn at all),
    and wire the git build-info (synchronet_gitinfo) + the syncduke_record_enabled / [game] record plumbing used by the menu and demo changes that follow.

    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, June 28, 2026 00:21:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8c2fab334d40bc78a9954d36
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c src/doors/termgfx/term.c
    Log Message:
    syncduke: center the sixel/image in the terminal canvas (port from syncdoom)

    SyncDUKE drew its image at the top-left (ESC[H), while SyncDOOM centers it.
    The difference only shows on terminals that honor the cursor as the sixel origin
    (e.g. Windows Terminal); SyncTERM ignores the cursor under DECSDM ?80l and anchors 0,0, so both doors look top-left there.

    Port SyncDOOM's approach: probe the cell-pixel size (ESC[16t) in addition to the
    window pixels (ESC[14t), then center the out_w x out_h image in the pixel canvas
    and position the cursor at the resulting cell (ESC[row;colH). Without a cell size
    (SyncTERM doesn't answer ESC[16t) we fall back to the top-left -- byte-for-byte the old behavior -- so the main platform is unchanged. The mouse steering now derives its center column and half-width from the same shared geometry helper, so
    pointer-to-turn mapping tracks the centered image instead of assuming a top-left,
    8px-cell layout.

    - termgfx/term.c: add ESC[16t to the shared probe (SyncTERM ignores it).
    - syncduke_input.c: parse the ESC[16t reply; mouse steer uses image geometry.
    - syncduke.h: cell-size accessors + syncduke_image_geometry().
    - syncduke_io.c: syncduke_image_geometry() + center the image-tier cursor.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 00:21:00
    https://gitlab.synchro.net/main/sbbs/-/commit/9702f5ad2884a73ef51a3a35
    Modified Files:
    src/doors/syncduke/syncduke_io.c
    Log Message:
    syncduke: aspect-preserve the sixel tier (fit, don't stretch to fill)

    The sixel tier sized the encoded frame to out_w x out_h -- the terminal's pixel canvas capped at 640x480 -- and nearest-scaled Duke's native 320x200 into it, which is 4:3 on a tall terminal: the game came out vertically stretched (e.g. encoded 320x240 at >=80x50 instead of ~320x200).

    Run the sixel through the same termgfx_geom_fit/center as the JXL tier: fit 320x200 into the real graphics canvas (XTSMGRAPHICS via syncduke_canvas_w/h, not
    the rows*16 text-area estimate) preserving 8:5, capped at 640 wide, then encode at 1/scale of that. The displayed sixel is now a constant 640x400 letter-boxed and centered, exactly like SyncDOOM.

    Side effects, all good:
    - no vertical stretch (keeps 8:5 on tall terminals);
    - the prior 80x50/60 over-hang is gone -- a fit is <= canvas by construction,
    so the frame can no longer scale past the real ~640x400 SyncTERM canvas;
    - ~18% less sixel bandwidth on tall terminals (320x198 vs 320x240).

    Both image tiers now share termgfx_geom_fit, lining SyncDuke up with SyncDOOM ahead of wiring Doom to the same helper.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 00:21:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b19267dfa434ca3aa49aff8f
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c
    Log Message:
    syncduke: mouse-steer around the actual displayed image, not out_w

    The mouse steer mapped the pointer column to a turn rate around the center of an
    out_w x out_h image -- but the displayed image is now the fitted/centered sixel (or the JXL fill), and out_w no longer describes it. On a wide SyncTERM the sixel
    anchors top-left (cursor ignored under ?80l) yet the steer assumed canvas-center,
    so the neutral point was off.

    present() now records the displayed image's actual horizontal center column and half-width each frame -- accounting for tier and terminal (SyncTERM sixel top-left
    vs JXL/elsewhere centered) -- and the steer reads that (syncduke_hsteer). Replaces
    syncduke_image_geometry, whose out_w-based centering is gone.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 22:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/39aeecbc95d186a70ce76291
    Added Files:
    src/doors/syncduke/syncduke_net.c
    Modified Files:
    src/doors/syncduke/CMakeLists.txt syncduke_stubs.c
    Log Message:
    syncduke: LAN co-op Ä fill the mmulti seam with a UDP transport

    The vendored Build/Duke netcode (game.c getpackets() + the move-FIFO
    lockstep) was intact; only the mmulti transport seam was stubbed
    single-player. syncduke_net.c fills it for 2-player LAN co-op:

    - getpacket/sendpacket carry raw game packets between two door
    instances over UDP, reporting the sender's player index from the
    source address (broadcast when other<0).
    - initmultiplayers does a tiny master/join handshake to set the
    mmulti globals (numplayers / myconnectindex / the connect list)
    before the engine reads them.
    - env-configured, single-player-safe when unset:
    SYNCDUKE_NET=master SYNCDUKE_NET_PORT=<port> -> player 0
    SYNCDUKE_NET=join SYNCDUKE_NET_PEER=host:port -> player 1
    - own UDP (no new dep): Chocolate-Duke's ENet mmulti is the protocol
    oracle but the ENet lib was removed from the tree; this matches
    SyncDOOM's from-scratch transport.

    Validated end-to-end with two live instances on the loopback driven
    into a shared E1L1 cooperative level (slash warp args /v1 /l1 /c2 --
    the engine's parser only honors '/' options, not '-'): the full Duke
    netgame negotiation flows bidirectionally (weapon order, version/CRC
    match, names), both pass every waitforeverybody() sync barrier, both
    complete enterlevel() (ready2send=1), and the move-FIFO lockstep then
    pumps -- ~320 per-tic sync/input packets each way with the simulation
    stepping in lockstep (ototalclock advancing tic for tic on both).

    Sampled packet logging (first 16 then every 64th, gated on
    SYNCDUKE_LOG) is left in as netplay bring-up diagnostics.
    genericmultifunction stays a no-op stub.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 22:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/e4d6c705328c0cc1c7b5f486
    Modified Files:
    src/doors/syncduke/syncduke_io.c
    Log Message:
    syncduke: bound the sixel encode to its buffer (fix full-res co-op SIGSEGV)

    The full-res sixel opt-in (5cbc69c24) encodes sxh=sdh without the half-res /2, so
    on a large canvas (sdw at the 1024 width cap, sdh ~691 with the aspect/stretch fit)
    sxw*sxh exceeded the fixed syncduke_scaled[1024*640] buffer and overran into the
    adjacent globals -- g_out got clobbered with palette bytes, and the next out_put
    memcpy faulted (SIGSEGV in syncduke_present, seen at co-op level entry). Root-caused
    from the core: &g_out sits 48 bytes past the buffer end.

    Hard-bound the encoded area to the buffer (sxw*sxh <= OUT_W_MAX*OUT_H_MAX) so it can
    never overrun again, and raise OUT_H_MAX 640->768 so a full-res encode on a maxed
    window isn't needlessly shrunk by that clamp.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 22:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/19a27fb62e5f9fde54490d6d
    Modified Files:
    src/doors/syncduke/syncduke_config.c
    Log Message:
    syncduke: per-node debug-log filename so co-op nodes don't clobber one log

    Two co-op doors run by the same user on different nodes share one per-user dir, so
    both wrote the same [debug] log and clobbered it (which made the SIGSEGV above hard
    to read). Tag the configured log filename with the node number from sbbs_my_node()
    (termgfx; reads SBBSNNUM that SBBS sets for every external program, falling back to
    SBBSNODE's trailing digits) -> syncduke.<node>.log. Respects [debug] log on/off; no
    node env (dev/standalone) leaves the name unchanged. sbbs_node.c is already in the
    termgfx library SyncDuke links, so this is just an #include.

    Co-Authored-By: Claude Opus 4.8 <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, June 28, 2026 22:01:00
    https://gitlab.synchro.net/main/sbbs/-/commit/b8388f75e39aeb6749a6c0df
    Modified Files:
    src/doors/syncduke/syncduke_config.c xtrn/CLAUDE.md
    Log Message:
    syncduke: put the debug log in data/syncduke/; document SBBS env vars

    A bare [debug] log filename now goes in <SBBSDATA>/syncduke/ (the door's shared data
    dir, alongside the games registry) rather than the per-user dir, so co-op nodes'
    logs sit together and don't clutter user storage. Uses the SBBSDATA env var SBBS
    sets for external programs (no hardcoded path, no new door arg); still node-tagged
    via sbbs_my_node(); an explicit path or a dev run (no SBBSDATA) is unchanged.

    Also document in xtrn/CLAUDE.md the five env vars SBBS exports to external programs
    (SBBSCTRL/SBBSDATA/SBBSEXEC/SBBSNODE/SBBSNNUM), the dir-vs-number distinction, and
    the don't-derive / dev-fallback caveats.

    Co-Authored-By: Claude Opus 4.8 <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 Monday, June 29, 2026 22:33:00
    https://gitlab.synchro.net/main/sbbs/-/commit/3a6d1ff6e6a1fa1743752be8
    Modified Files:
    src/doors/syncduke/syncduke_stubs.c src/doors/termgfx/audio_mgr.c audio_mgr.h
    Log Message:
    syncduke: 3D distance attenuation for looping ambiences (projector et al.)

    The engine re-pans every active positional voice each frame (sounds.c
    calls FX_Pan3D), so ambiences like the E1L1 cinema projector should swell
    as the player approaches. FX_Pan3D was a no-op stub, freezing each loop at
    the volume it had when first triggered -- the projector started at vol=7 (player far away) and stayed inaudible no matter how close you walked.

    - termgfx_audio_loop_volume(): update a running loop's channel volume live,
    suppressing redundant sends so it's safe to call every frame.
    - FX_Pan3D: map the per-frame distance to a volume (same curve as
    sd_loop_play) and push it to the loop. One-shots never reach here
    (fire-and-forget leaves Sound[].num==0, so the engine's pan loop skips
    them). Pan/stereo still not wired -- distance attenuation is what governs
    audibility here.

    Live-confirmed: projector now audible on approach.

    Co-Authored-By: Claude Opus 4.8 <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 Monday, June 29, 2026 22:33:00
    https://gitlab.synchro.net/main/sbbs/-/commit/fc82f87d19f90338ab6cde8d
    Modified Files:
    src/doors/syncduke/syncduke_stubs.c src/doors/termgfx/audio.c audio.h audio_mgr.c audio_mgr.h
    Log Message:
    syncduke: stereo pan for positional audio (ambiences + one-shots)

    Builds on the distance-attenuation fix: now sounds also track DIRECTION. SyncTERM's A;Volume APC accepts separate VL/VR (live per-channel left/right, not just mono V), so a running loop can be re-panned every frame with no re-queue click -- our wrapper only emitted V=, so add a VL=/VR= builder.

    - audio.c/.h: termgfx_audio_volume_lr() -- live per-side channel volume.
    - audio_mgr.c/.h: termgfx_audio_loop_volume() takes a pan, computes VL/VR
    (side toward the source full, opposite ramps -- the original audiolib law),
    and suppresses unchanged L/R so per-frame calls stay cheap.
    - syncduke_stubs.c: sd_angle_to_pan() replicates MV_CalcPanTable's triangle
    exactly (front=center, hard right at angle 8, behind=center, hard left at
    24). FX_Pan3D pushes volume+pan to loops; one-shots (FX_PlayVOC3D/WAV3D)
    are panned at queue time via the existing pan slot.

    Pan is discrete per-frame (no T= ramp yet). Live-confirmed by ear: the
    cinema projector pans as you cross it, weapons/enemies have direction.

    Co-Authored-By: Claude Opus 4.8 <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, June 30, 2026 22:32:00
    https://gitlab.synchro.net/main/sbbs/-/commit/668f41fb2a540e168c059fc8
    Modified Files:
    src/doors/syncduke/syncduke_stubs.c
    Log Message:
    syncduke: prefix the music cache name with the track filename

    The door-side / client cache key was the bare content hash (d_<hash>); make
    it "<trackname>_<hash>" (e.g. STALKER_3f2a8b1c) so the cached OGGs are legible when inspecting a client's SyncTERM cache. The hash still does the work -- stable cross-session and collision-free across GRPs; the name is just for readability. Strip the source extension and neutralise any dots first so termgfx's extension-stripping doesn't eat the hash. SyncDOOM stays hash-only (its music module only gets the lump bytes, not the lump name).

    Co-Authored-By: Claude Opus 4.8 <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, June 30, 2026 22:32:00
    https://gitlab.synchro.net/main/sbbs/-/commit/42acf099383d7ebe27db5348
    Modified Files:
    src/doors/syncduke/syncduke_stubs.c
    Log Message:
    syncduke: treat 0% music volume as OFF

    SyncDuke's Setup Sound has no separate MUSIC:OFF toggle, so 0% on the music slider is the off switch. At 0%, PlayMusic() now skips the render/encode/upload entirely (no silent transfer) and MUSIC_SetVolume() stops the loop; the most- recent track is remembered so raising the slider back off 0 replays it.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 02:20:00
    https://gitlab.synchro.net/main/sbbs/-/commit/056e7754cb9645b0ca2abfce
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_io.c syncduke_plat.c syncduke_stubs.c
    Log Message:
    syncduke: render/xfer + frame-stall telemetry

    - The music render line now splits render=<transcode ms> xfer=<encode+cache+
    upload ms>, so a load stall reads as transcode vs transfer at a glance.
    - Frame-stall watchdog: log when a present + pace cycle exceeds 500ms, with the
    in-flight count and effective pipeline depth (via new syncduke_pace_inflight/
    _curdepth accessors) -- high inflight during a stall points at the link, not a
    local hitch.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/8ce3410c26469a686ac937cf
    Added Files:
    src/doors/syncduke/syncduke_node.c
    Modified Files:
    src/doors/syncduke/CMakeLists.txt syncduke.h syncduke_plat.c
    Log Message:
    syncduke: node module scaffold + status broadcast

    Adds syncduke_node.c: door-native Synchronet node integration (init/teardown
    + a "playing SyncDuke" NODE_EXT who's-online status), wired into syncduke_plat.c's _platform_init (init) and _nextpage (per-frame tick, right after _handle_events()). No-ops entirely off a BBS (sbbs_node_available()==0). First of a 5-task arc; later tasks add the Ctrl-U who's-online banner and incoming inter-node message display on top of this scaffold.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/1a3f9d5049b422d6aa1f18d9
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_io.c syncduke_node.c
    Log Message:
    syncduke: cross-tier who's-online/message banner overlay

    Adds the banner overlay mechanism (Task 2 of the node-features plan): syncduke_node.c gains banner state (g_ov rows + expiry + change signature)
    and syncduke_node_overlay_sig()/syncduke_node_draw(), a top-strip painted
    via syncduke_out_put over sixel/JXL/text tiers alike and auto-clearing on expiry. present() (syncduke_io.c) folds the banner signature into its
    frame de-dupe so a banner-only change is never skipped, forces a repaint (syncduke_have_last=0 + rt_invalidate()) when the banner changes, and
    draws the banner after each frame emit.

    Exposes syncduke_clock_ms() (syncduke_io.c) as a non-static accessor for
    the same monotonic ms clock present()/pacing already use, so the banner's expiry timer shares that exact clock domain rather than a second, merely similar copy (syncduke_in_now_ms() in syncduke_input.c is static and a
    separate instance).

    The banner is currently unused (Tasks 3/4 wire Ctrl-U and incoming
    messages as producers); a temporary self-test call was build-verified
    then removed before this commit.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/ff95478b425582369ca68278
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_input.c syncduke_node.c src/doors/syncduke/tests/test_evdev.c test_keymap.c test_kitty.c
    Log Message:
    syncduke: Ctrl-U who's-online listing

    Ctrl-U (0x15) requests a who's-online banner: handle_key() routes the fresh press to syncduke_node_userlist_request() (and swallows its kitty auto-repeat) instead of falling through to syncduke_map_key(), which sets a flag consumed
    by the next syncduke_node_tick(). The tick builds the banner from the live
    node list (sbbs_list_nodes/sbbs_my_node/sbbs_username/sbbs_node_ext/ sbbs_action_str) into the Task 2 overlay buffer via banner_set().

    test_kitty.c gets a TDD unit test asserting Ctrl-U never reaches the raw scancode queue and does route to syncduke_node_userlist_request(); the other two syncduke_input.c-linking tests (test_evdev.c, test_keymap.c) get the same stub so they keep linking now that handle_key() references the new symbol.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/93195bf74451dfadf0de6752
    Modified Files:
    src/doors/syncduke/syncduke_node.c
    Log Message:
    syncduke: show incoming inter-node messages in-game

    Implement syncduke_node_recv() to poll ~1 Hz for Synchronet inter-node messages,
    strip Ctrl-A/BEL codes, fold whitespace, word-wrap verbatim into the banner overlay, and ring the bell for audible alert. Integrates with the existing banner infrastructure (Tasks 1-3) via banner_set() and the display tier infrastructure. Uses syncduke_clock_ms() for consistent millisecond clock.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/d1470e9168d5138953a8d5a6
    Modified Files:
    src/doors/syncduke/syncduke_node.c
    Log Message:
    syncduke: fix banner-draw stack over-read on wide terminals

    syncduke_node_draw padded the text to `cols` (up to 128) with %-*.*s; on a terminal wider than ~106 columns the formatted line plus escape bytes exceeded the 128-byte stack buffer, so snprintf truncated but returned the full intended length and syncduke_out_put then memcpy'd that many bytes -- reading past the buffer and shipping stack garbage (and cutting the SGR reset, leaving the terminal white-on-red). Switch to SyncDOOM's draw_page_overlay pattern: erase- to-EOL (\x1b[K) fills the strip to the true terminal edge with a bounded byte count, independent of `cols`. Fixes the over-read and the wide-terminal under- fill in one change.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/3c1cb3b26821ccb825f763d0
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_game.c syncduke_node.c
    Log Message:
    syncduke: include current level (E#L#) in the node status

    The who's-online NODE_EXT status was a constant "playing SyncDuke". Append the current episode/level as (E#L#) while a real game is in progress -- excluded in the menu/title and attract-mode demos (MODE_DEMO / ud.recstat==2). Read from the
    engine (ud.volume_number/level_number, 0-based; shown +1 as the game does) via a
    new syncduke_game_status() in syncduke_game.c, keeping syncduke_node.c engine- independent. Matches SyncDOOM's "(E#M#)"/"(MAPxx)" status.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/fde0c52494a2034d5f241c6f
    Modified Files:
    src/doors/syncduke/syncduke_game.c xtrn/syncduke/install-xtrn.ini lobby.js
    Log Message:
    syncduke: use "SyncDuke (Nukem 3D)" as the display name

    Consistent user-facing game name (the internal identity stays SyncDuke: binary, sources, cache prefixes, env vars, SCFG codes are unchanged). The node who's- online status now reads "playing SyncDuke (Nukem 3D)" (+ " E#L#" in a level); the install-xtrn.ini program names become "SyncDuke (Nukem 3D)" (lobby) and "SyncDuke (Nukem 3D) single-player" (direct launcher); the lobby's co-op announce
    names the game the same way.

    Co-Authored-By: Claude Opus 4.8 <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 01, 2026 23:24:00
    https://gitlab.synchro.net/main/sbbs/-/commit/be4d8b0f87f13dbc973bf5f5
    Added Files:
    src/doors/syncduke/syncduke_events.c
    Modified Files:
    src/doors/syncduke/CMakeLists.txt syncduke.h syncduke_config.c syncduke_io.c syncduke_plat.c
    Log Message:
    syncduke: events.jsonl activity-log writer (start/level/death)

    Add a per-frame detector (syncduke_events.c, run from _nextpage) that appends compact JSON lines to a -eventlog path: a "start" event when a real game begins (node, user, terminal size, tier, build, mode, skill), a "level" event for each level entered, and a "death" event on the player's death. Each line is one atomic
    append (no lock), matching src/doors/syncdoom's event model, so a lobby can tail
    the log for a recent-activity feed. Logging is off unless -eventlog is passed. syncduke_config.c parses -eventlog and exposes the home/eventlog paths; syncduke_io.c exposes the active render-tier name for the start event.

    Co-Authored-By: Claude Opus 4.8 <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 02, 2026 21:40:00
    https://gitlab.synchro.net/main/sbbs/-/commit/14ddd3ddc7731e34d50950f3
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_events.c syncduke_game.c
    Log Message:
    syncduke: door emits dukematch frag events + reports DM mode

    Co-Authored-By: Claude Opus 4.8 <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 02, 2026 21:40:00
    https://gitlab.synchro.net/main/sbbs/-/commit/54a1b7013c738e004774b237
    Modified Files:
    src/doors/syncduke/syncduke_net.c
    Log Message:
    syncduke: door exits cleanly when a multiplayer peer never connects (no solo fallback)

    A door launched with a net role (master/join) whose handshake times out now _exit(0)s cleanly (freeing the node) instead of dropping the host into a single-player game. A no-net-role (P=play solo) launch is unaffected.

    Co-Authored-By: Claude Opus 4.8 <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 03, 2026 01:03:00
    https://gitlab.synchro.net/main/sbbs/-/commit/2194d13dbb4518bb005dff5b
    Modified Files:
    src/doors/syncduke/syncduke_events.c xtrn/syncdoom/lobby.js xtrn/syncduke/syncduke_lib.js
    Log Message:
    SyncDuke: log level completion + time (match SyncDOOM); harmonize lobby message

    The level event now fires on the level-change edge but logs the level just CLEARED (last_vol/last_lev) with its own elapsed time, instead of the level entered with the previous level's time. The lobby renders it as
    "X cleared E1L1 in M:SS" (was "reached E1L1"), matching SyncDOOM's sd_event_level. The on-entry emit is dropped.

    Also point SyncDOOM's empty-lobby message at the same wording SyncDuke uses: "No multiplayer games are waiting."

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KYmBzSZGyRondAXevh9RJP

    ---
    þ 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 04, 2026 00:42:00
    https://gitlab.synchro.net/main/sbbs/-/commit/e3cfaa9eeed88c864982fa85
    Modified Files:
    src/doors/syncduke/syncduke_events.c
    Log Message:
    syncduke: log level completions reliably (detect the MODE_EOL edge)

    Clearing a level wasn't logged to events.jsonl, so it never showed in the
    lobby feed. The old detector watched for the level NUMBER changing while in_game, but finishing a level sets gm=MODE_EOL (the bonus screen), which
    makes ev_real_game() false -> in_game drops before the next level loads, and the whole clear->bonus->next transition read as a fresh "start". Detect the clear at the MODE_EOL edge instead -- the reliable "level finished" signal (same idea as SyncDOOM's GS_LEVEL->intermission edge) -- and log it with the real elapsed time (the few that used to slip through logged secs=0, timed at level entry).

    Co-Authored-By: Claude Opus 4.8 <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 04, 2026 14:32:00
    https://gitlab.synchro.net/main/sbbs/-/commit/3c678a3299e53cb51aceecf2
    Modified Files:
    src/doors/syncduke/syncduke_events.c
    Log Message:
    syncduke: log the engine's real level time, not the door's wall clock

    The level-completion event timed levels with ev_secs() -- a door-side
    wall clock started at the door's level entry -- so loading a savegame
    mid-level over-reported the time (the door timer kept running from a
    level entry the reload didn't reset). Report the engine's own per-level
    play time instead: ps[myconnectindex].player_par / 26, the "Your Time"
    value from the bonus screen. It counts game tics and lives in the
    player struct, which saveplayer/loadplayer block-serialize, so it is
    written into savegames and resumes on load. Read it at the MODE_EOL
    edge, before the next level's resetplayerstats() zeroes it.

    SyncDOOM already reported leveltime/35 (its analogous engine counter),
    so this brings the two doors into agreement.

    Co-Authored-By: Claude Opus 4.8 <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 Sunday, July 05, 2026 03:53:00
    https://gitlab.synchro.net/main/sbbs/-/commit/485414c789c5c98119c7c297
    Modified Files:
    src/doors/syncduke/syncduke_events.c
    Log Message:
    syncduke: fix misreported "cleared" events in the activity feed

    The lobby feed showed "cleared E1L8 in 0:00" for a user map that was
    never cleared. Three defects in syncduke_events_tick():

    - The level-number-change fallback edge logged a "level" (cleared)
    event when the player ABANDONED a level from the in-game menu (new
    game / load / warp) -- with a garbage elapsed time, since ev_secs()'s
    totalclock base is reset by newgame(). Real clears always pass
    through the MODE_EOL edge, so the change edge now only re-captures
    the level identity and logs nothing.

    - The MODE_EOL edge named the WRONG map: every EOL setter (exit switch,
    nuke button -- sector.c, player.c) advances or wraps ud.level_number
    in the same tic it sets MODE_EOL, so reading it at the edge named the
    NEXT level ("cleared E1L2" after finishing E1L1; a cleared user map
    wrapped to "E1L1"). The level's display name is now captured at level
    entry and the EOL edge logs the captured name.

    - "start" re-fired on every death/respawn and level transition (in_game
    bounces because gm loses MODE_GAME until the next enterlevel),
    flooding events.jsonl; it now fires once per door session.

    Also name user maps by their file's basename with the ".map" extension
    stripped ("Roch") in level/death/frag events -- their level slot is
    always 7, so "E1L8" was meaningless -- and JSON-escape the map string
    now that it can come from a filename. No lobby.js change needed: the
    feed treats "map" as a free string.

    Co-Authored-By: Claude Fable 5 <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 Sunday, July 05, 2026 16:37:00
    https://gitlab.synchro.net/main/sbbs/-/commit/a1541556bf396aae61909620
    Modified Files:
    src/doors/syncduke/syncduke_config.c
    Log Message:
    syncduke: node-tag logs as _n<node>, not .<node>

    The per-node debug log was named syncduke.<node>.log (e.g. syncduke.11.log). Synchronet uses <name>.<digit>.log for ROLLED-OVER log files, so that reads as a rollover of syncduke.log and could confuse a sysop. Tag with "_n<node>" before the extension instead: syncduke.log -> syncduke_n11.log. Matches SyncDOOM's new logger.

    Co-Authored-By: Claude Fable 5 <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 Sunday, July 05, 2026 16:57:00
    https://gitlab.synchro.net/main/sbbs/-/commit/444e8c5694bf791649ee4961
    Modified Files:
    src/doors/syncduke/syncduke_input.c syncduke_node.c
    Log Message:
    syncduke: fix -Wmisleading-indentation warnings (unstack crammed lines)

    The same crammed-one-liner pattern GCC's -Wall flags in SyncDOOM also lived
    in the SyncDuke shims -- unstack them, one statement per line at the correct depth (tabs, per uncrustify.cfg), no behavior change:

    syncduke_input.c evdev_edge case 29/97 (Ctrl), 56/100 (Alt), 103/108 (arrows);
    the F1-F7 cases 59-65; and csi param parse "if (have) ... "
    syncduke_node.c the Ctrl-A / whitespace-fold strip, and the column clamp
    ("if (w > 80) w = 80; if (w < 20) w = 20;")

    Braced single-statement bodies (e.g. the look_hold cases) don't trip the warning and were left as-is.

    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/3db074e5cd440b68ab5fefc9
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_input.c syncduke_io.c
    Log Message:
    syncduke: move onto termgfx keymode

    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 enable-time settle
    window, the kitty CSI-u parameter decoder, the 88-entry evdev->ASCII
    table and the modifier classifier. All of it was duplicated, byte for
    byte, in SyncDOOM, SyncConquer and SyncRetro.

    The key mode's state now lives in one termgfx_keymode_t owned by syncduke_input.c (which parses the CTDA and CSI?u replies) and reached
    from syncduke_io.c's terminal-restore path via syncduke_keymode(). That
    restore is a single termgfx_keymode_restore() call on both the Winsock
    and POSIX branches, emitting only what was actually enabled, instead of
    two hand-guarded literal writes on each.

    syncduke_kitty_active() and syncduke_evdev_active() are kept as the
    door's public predicates -- the input layer branches on them in a dozen
    places to choose native key-up over the byte path -- but they now read
    the shared state.

    No change to the bytes on the wire, the settle window's 500ms, Duke's
    key -> Build-scancode map, or the modifier keycodes recognized.

    ---
    þ 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/52baec2f10cb82b503962bd5
    Modified Files:
    src/doors/syncduke/syncduke_input.c src/doors/syncduke/tests/test_evdev.c test_keymap.c test_kitty.c
    Log Message:
    syncduke: repair the three input tests, which had rotted unbuildable

    test_keymap.c, test_kitty.c and test_evdev.c could not be linked with the command lines in their own header comments: syncduke_input.c grew calls
    into syncduke_node.c's page-compose hooks, and test_keymap additionally
    needed the termgfx audio and RTT symbols the pump now touches. They also
    need an -s1 argument -- a bare launch hits syncduke_door.c's pre-main constructor, which prints usage and exits.

    This predates the termgfx keymode work (verified by building them at 474e8cf076); keymode.c is simply added to the link lines as the newest dependency, and the three tests then pass: 43 assertions in test_kitty,
    55 in test_evdev.

    One stale expectation fixed rather than papered over: test_keymap
    asserted plain R maps to CapsLock (AutoRun). AutoRun deliberately moved
    to Ctrl-R so that R reaches Duke's Steroids hotkey -- see handle_key()'s
    0x12 case -- so R now maps to its own scancode. The assertion is updated
    to Steroids, and the file-header comment in syncduke_input.c, which still documented the old binding, is corrected. Ctrl-R itself is handled in handle_key(), a layer above the map_ascii() these tests drive, so there
    is nothing to assert for it here.

    ---
    þ 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/91d049a9cff95b2dd4a1acbe
    Modified Files:
    src/doors/syncduke/syncduke_input.c
    Log Message:
    syncduke: numpad Home/End/PgUp/PgDn are Aim/Look under kitty, not weapons

    With NumLock off, Windows Terminal sends the numpad's kitty FUNCTION
    codepoints -- KP_HOME 57423, KP_END 57424, KP_PAGE_UP 57421, KP_PAGE_DOWN 57422. The CSI-u handler ran them through kitty_kp_normalize(), which folds them to the DIGIT-key codepoints KP_7/1/9/3; those fall through to the ASCII fold and become '7'/'1'/'9'/'3' -- which in Duke are weapon-select keys. So in gameplay the numpad End key selected weapon 1 and holstered whatever you were holding, and Home/PgUp/PgDn silently switched weapons too.

    Every other input path already maps these four correctly: the legacy CSI ~/H/F path and the SyncTERM evdev path both drive look_hold(sc_kpad_7/1/9/3) -- Aim up/down, Look up/down. Only the kitty path folded them to digits. It went unseen because the terminals it was tested on (foot, Contour) were captured with NumLock ON, whose DIGIT codepoints SHOULD fold to weapons.

    Intercept the four function codepoints in gameplay before the normalize, with the same look_hold mapping the other two paths use. NumLock-ON digit
    codepoints
    (57406/57400/57408/57402) are untouched and still select weapons, which is
    what
    NumLock ON is for. Menus were already correct (the non-gameplay block maps KP_HOME/KP_END to first/last item). Found by tracing the raw CSI-u that
    Windows
    Terminal actually sends.

    ---
    þ 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/9d6fa39bad4799645773cd31
    Modified Files:
    src/doors/syncduke/syncduke_stubs.c
    Log Message:
    syncduke: map the MusicVolume slider linearly in dB

    The faithful percent->dB step left the MusicVolume slider mapping the game 0..255 to a 0..20 percent that SyncTERM then log-converted, so the slider
    was lumpy near the bottom and the trim was the opaque number 20. Map the
    slider LINEARLY IN DB instead -- how a real fader behaves -- between a
    shared floor and a readable per-door trim: SD_MUSIC_TRIM_DB -14 (the old SD_MUSIC_MAXV 20 in honest units) at the top, SD_MUSIC_FLOOR_DB -40 just
    above the 0 = OFF cutoff.

    Confirmed by ear. Same behaviour at the top of the slider; smoother, and a touch quieter, through the middle.

    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 02:18:00
    https://gitlab.synchro.net/main/sbbs/-/commit/7b224665c1128790d1568987
    Modified Files:
    src/doors/syncduke/syncduke.h syncduke_config.c syncduke_door.c syncduke_input.c syncduke_io.c syncduke_plat.c xtrn/syncduke/syncduke.example.ini
    Log Message:
    syncduke: end the game when the player has stopped playing

    The fifth and last phase-2 door to adopt the shared idle-user clock. Same reasoning as the siblings: the BBS's own "Maximum Inactivity" cannot see a DSR-paced door, since that counter is reset by any socket read and 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, countdown on the bottom row, same wording as the
    siblings, 10-minute default. Configured by -i<seconds> or the door's own
    [idle] section; the argument wins when given, so a launcher can pass -i0 to excuse an exempt user. Unset is NOT off, which the shipped template now
    says.

    THE CLOCK IS ARMED LAZILY, ON FIRST MAIN-TIME USE, and that is not a
    stylistic choice. Arming at the end of syncduke_door_resolve() looks right
    -- the resolve is lazy, and it is where -i is parsed -- but it is wrong: syncduke_door_splash() calls syncduke_door_socket(), which resolves, and
    splash runs inside the PRE-MAIN CONSTRUCTOR. syncduke.ini is read by syncduke_config_init(), a constructor in a different translation unit, so
    the two race with undefined order. The door's won, the ini had not been
    read, and every install would have silently used the compile-time default
    with [idle] ignored entirely. Arming on first use puts it after every constructor, where both sources are known.

    The activity hook goes in three places and deliberately not in the obvious
    one. press() (legacy bytes), hold_press() and hold_release() (native kitty/evdev) are hooked; their common rawq_push() is not, because syncduke_input_expire() drives it with synthesized key-ups -- which would
    forge activity for a player who has already walked away. hold_release()
    wakes the clock but never CONSUMES: swallowing a release would leave that
    key stuck down in the engine. Mouse events count too, since mouse-steer lets someone play for a long stretch with no keystroke.

    Input that answers the countdown is consumed rather than delivered, so
    "press any key" cannot also fire a weapon or open a menu.

    -i is matched on an ALL-DIGIT suffix, both when parsed and when stripped -- deliberately tighter than this door's own -s/-t rule, so no Duke word option beginning with -i could ever be swallowed by it.

    This door had no unified exit check to hang the timeout on, so one now rides
    in _nextpage() beside the session time limit: the single place already
    reached on every presented frame. It restores the terminal before leaving, rather than dropping the player on a wedged screen.

    The countdown does not get its own writer. It becomes the Ctrl-S strip's content while showing, so it lands on the row that strip already owns. When
    it clears, the row is wiped and a full repaint forced -- the sixel tier never repaints that reserved row on its own, so a bare wipe would leave it blank.

    Verified offline (-i fires to the millisecond, -i0 never does, the countdown ticks down on the wire with correct plurals, the ini alone works with no -i, and an explicit -i overrides it) and on a live node.

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

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