Commit graph

45 commits

Author SHA1 Message Date
Stephen Dennis
8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`.  Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.

The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.

New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.

The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper).  const_cast budget: zero
added.

Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW).  delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup.  After any signature flip, grep the
old spelling.

Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00
Stephen Dennis
ae78230a9f chore(int64): modules drop atoi; comsys MAX_LOG and timeout full width (#1402)
mail_mod and comsys_mod used C atoi for folders, charges, dbrefs, and
history config — the same silent 32-bit truncation as int sinks after
mux_atoi64. Route those through mux_atoi64 (mathutil.h) and widen
locals where they matter.

Also: DESC idle timeout clamps after int64 parse; engine/module
@cset/log MAX_LOG uses int64_t end-to-end with mux_i64toa_t.
2026-07-28 19:43:03 -06:00
Stephen Dennis
ddff5b45a5 chore(int64): softcode mux_atoi64 sinks use int64_t (#1402)
Convert remaining engine `int x = mux_atoi64(...)` (and explicit
static_cast<int> forms) to int64_t so parse width is not discarded on
any platform. Includes functions/funceval/funmath, mail folder numbers,
comsys charge parse, JIT register indices, and HIR lower mid/left/right.

dbref parse keeps full-width parse then rejects values outside int
range. Channel charge still stores int after an in-range check.
2026-07-28 19:40:41 -06:00
Stephen Dennis
626490aa53 feat(nls/comsys): channel join/leave and connect/disconnect become translatable sentences (#1723)
The scope Brazil set on #1723: join/leave are the server speaking about an
event, so they translate; the speech wrapper (" says, ") is scaffolding
around a player's own words and deliberately stays T().

The engine built all four announcements as pose fragments through
BuildChannelMessage -- T(":has joined this channel.") appended after a
composed comtitle+name prefix -- so no translatable unit existed, and the
same catalogue translated channel traffic under the module but not under
the built-in engine every game actually runs.

## Shape

BuildChannelSpeaker    the comtitle/name/SPOOF logic extracted from
                       BuildChannelMessage; the pose path is byte-identical.
BuildChannelAnnounce   the sentence path: an M_()-resolved format with one
                       %s, receiving the comtitle-decorated speaker, header
                       prepended.  Four sites converted: join, leave (both
                       leavechannel and delcom), connect, disconnect.

Join/leave reuse the msgids the module has always used, so the two
implementations converge on the same catalogue entries (#1614).  Connect/
disconnect already existed in the .pot from engine_com.cpp's login
announcements; only the es/ko fills were new.

Measured, built-in engine, LANG=C:

    en  [prueba] Wizard has joined this channel.
    es  [prueba] Wizard se ha unido a este canal.
    ko  [prueba] Wizard님이 이 채널에 참여했습니다.

English output is byte-identical to before -- header + ' ' + speaker +
sentence composes the same text either way -- and the conformance baseline
passed WITHOUT re-blessing, which is the proof of that.

## SPEECHMOD: kept, with a documented change

The old pose path ran the SPEECHMOD hook over the bare verb phrase.  The
verb phrase no longer exists as a unit, so %0 now carries the whole
announcement ("Jess has joined this channel."), and an evaluated attribute
runs once per audience variant rather than once in total.  Kept rather than
dropped because it is a live softcode hook; changed rather than emulated
because the old input cannot be reconstructed from a translated sentence.

## The format guard fired, correctly

check_formats.py rejected the new call site -- safe_tprintf_str() with a
non-literal format -- which is exactly the ratchet working on new code.
The parameter is named pChannelAnnounceFmt and allowlisted beside
pRoomAnnounceFmt / pMonitorAnnounceFmt, the prior catalogue-resolved
announcement formats, with the justification recorded in the allowlist:
check_nls.py verifies every msgstr keeps its msgid's conversion types,
which is the property the guard exists to protect.

    es.po  282 -> 284/977  (29.1%)
    ko.po  313 -> 315/977  (32.2%)

make test: Smoke 1561 x3, conformance PASSED (no re-bless), handoff 13,
format 31749, tests/nls 7, ko 4, plural 43.  TESTEXIT=0.

Refs #1419, #1614, #1622, #1702, #1723.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:48:53 -06:00
Stephen Dennis
460cd2a37c fix(comsys): @cboot victim notify names the executor
The dual-path module already used the executor moniker for
"%s boots you off channel %s."; engine mistakenly used Moniker(thing)
so the victim was told they booted themselves.
2026-07-28 18:09:26 +00:00
Stephen Dennis
8e904de0cc nls: mark GAME/connect/Suspect/boot system notifies with M_() (#1419)
Residual Phase 3 player/staff prose that still used T() on live paths:
connect/reconnect/disconnect room and monitor lines, [Suspect] flags,
GAME: restart/shutdown/signal/log-full, dual-path channel boot, and
@email hostname resolution. pot + xx complete + ko msgmerge append;
tools/merge_nls_markings.py for Windows agents without xgettext
(canonical pot remains make -C mux/po pot on Linux).
2026-07-28 12:07:45 -06:00
Stephen Dennis
d4d14066e2 feat(comsys): @cwho via mux_table display columns (#1667 Phase 4 A4) 2026-07-28 09:22:57 -06:00
Stephen Dennis
8814deb4da nls: mark comsys channel, remaining mail sentences, and UDF notify prose (#1688)
Phase 3 coverage away from the mux_table lane:

  comsys   create/destroy/join/leave/alias/title/recall/@cset/@cboot
  mail     remaining MAIL: sentences (alias, send, full, fwd/re body)
  functions Function defined/deleted/not found, UDF total, coins

Left alone: column listings, HISTORY_n attr names, decompile, HTML.
pot grows (~798 -> ~872); xx rebuilt complete; ko msgmerge only.
2026-07-28 15:20:10 +00:00
Stephen Dennis
3d1bd97a65 feat(comsys): comlist via mux_table schema (#1667 Phase 4 A3) 2026-07-28 15:05:01 +00:00
Stephen Dennis
fadc7648e3 feat(comsys): @clist/full via mux_table schema (#1667 Phase 4 A1) 2026-07-28 08:31:41 -06:00
Stephen Dennis
40017efc41 feat(comsys): @clist headers via mux_table schema (#1667 Phase 4 A2) 2026-07-28 08:22:41 -06:00
Stephen Dennis
890d7f302d fix(comsys): the module's command surface, compared against the engine (#1640)
Four divergences from the issue, plus three more the probe found once the
fixtures stopped being plain ASCII.

From #1640:

  * @clist/full ignored the switch and printed the default listing, losing
    Header, Access, Users and Msgs -- the four columns the switch exists for.
  * @cwho printed a bare name where the engine prints unparse_object(), so
    staff lost the dbref and flags that identify WHICH object is on a channel.
  * comtitles reached no message at all.  The module stored them and
    round-tripped them through SyncChannelUser, then never read them back when
    composing one: not speech, not poses, not join/leave, and not spoof
    channels, where the comtitle is supposed to REPLACE the speaker's name.
  * delcom emitted the channel broadcast instead of the leaver's own
    confirmation -- which the leaver cannot even see, since clearing
    bConnected first is what suppresses it, so delcom looked like it had done
    nothing and any trigger matching "^You have left channel" stopped firing.

Comtitles are one fix, not four: BuildSpeakerPrefix mirrors the engine's
BuildChannelMessage, including that the LISTENER decides whether a comtitle
is shown, so SendChannelMessage now takes both variants and picks per
recipient.

Found while verifying, none of which an ASCII fixture can see:

  * The ENGINE conflated byte offset with column offset in @clist/full
    (comsys.cpp:2877), resetting iPos.m_column to the byte offset after
    writing JXR.  Those are equal only while every preceding field is plain
    ASCII; a colored channel header makes the byte count larger, PadField
    believes it is already past column 56, and the Users column shifts left.
  * The module stored channel headers with ANSI uncollapsed -- a raw strncpy
    where do_cheader runs StripTabsAndTruncate -- so `[ansi(r,RED)][ansi(b,BLU)]`
    persisted an extra reset between the codes.  A divergence in SQLite, not
    on screen, so it survived a handoff.  The same strncpy truncated at
    MAX_HEADER_LEN *bytes*, which can split a codepoint or a color code.
  * @clist/headers was missing the engine's pad to column 79.

Column layout in the module now goes through StripTabsAndTruncate/PadField
rather than printf field widths.  A printf precision counts codepoints and
knows nothing about PUA color; StripTabsAndTruncate carries a byte limit and
a column limit as a pair and reserves budget for the closing color sequence.
My first version of this fix hand-rolled the column stops with %-14.13s and
verified byte-identical output against the engine -- for ASCII only, which is
exactly the blind spot that produced two of the three findings above.  #1649
covers the general problem; nothing here waits on it.

@cwho deliberately reproduces the engine's composition including
strip_color(): @cwho discards color so its width arithmetic is honest while
@clist two functions away preserves it.  Both are coping strategies for the
primitive #1649 proposes, and this file's job is parity, not unilaterally
improving one side into a fresh divergence.

mux_IObjectInfo gains UnparseObject: the visibility rule is Examinable() plus
the CHOWN_OK/JUMP_OK/LINK_OK/DESTROY_OK/ABODE exceptions, which a module
cannot compute from GetFlags/DecodeFlags.  Caller-supplied buffer, matching
AtrGet, so nothing crosses the DLL boundary needing to be freed on the far
side.  CID_ObjectInfo is UseSameProcess only, so there is no proxy/stub to
update.

New harness, tests/comsys_cmdparity: one command stream, two fresh databases,
output diffed.  The two existing comsys harnesses compare state handoff and
delivery hooks; neither compares plain command output, and both scored green
against all seven of these.  Fixtures are deliberately colored and CJK.
Verified it goes red: reinstating the engine's PadField conflation fails case
3 while cases 1 and 2 still pass, which is the ASCII blind spot reproduced on
demand.

Windows Server 2022, MSVC 14.51, Release x64: three parity streams identical;
comsys_handoff 13/13; comsys_mogrify 5/5; smoke 1555 succeeded / 5 failed /
0 crashes, unchanged, the five being the pre-existing no-OpenSSL gap (#1641).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 22:36:25 -06:00
Stephen Dennis
687ca87309 fix(comsys): the module honours all five MOGRIFY hooks and CHATFORMAT (#1572)
The module implemented BLOCK and NOBUFFER.  MESSAGE, OVERRIDE and FORMAT
were silently ignored, and per-player CHATFORMAT was not present at all,
so a channel configured with any of them behaved differently depending on
which implementation happened to be live.  Silence rather than an error,
which is why 31 comsys cases in the corpus passed identically against
both.

Two of the issue's claims were stale and are corrected here: BLOCK already
evaluated through the evaluator interface rather than comparing raw
attribute text, and NOBUFFER had landed.  The gap was three hooks and
CHATFORMAT, not four hooks and a semantics bug.

Order and precedence follow comsys.cpp:1705-1822 exactly.  "Mostly the
same" is what produced the divergence in the first place, so MESSAGE
replaces the text, FORMAT sees the result of MESSAGE, OVERRIDE both
suppresses CHATFORMAT and lets FORMAT win outright, and history still
records the UN-mogrified message as the engine does.

## An engine bug fell out of it

s_chatformat_atr was a static, lazy-initialised ONCE PER PROCESS.
CHATFORMAT is a vattr, created the first time any player sets one -- so on
a game where nobody had one before the first channel message, the number
cached as 0 and CHATFORMAT was ignored for the entire life of the process,
including for every player who set one afterwards.  Restarting fixed it,
which is the shape that never gets reported as a bug.

It survived because nothing compared the implementations.  Now it caches
only a hit.

## tests/comsys_mogrify, and why it is not in comsys_handoff

Five cases, each driving both implementations and asserting they agree.
4 of 5 fail against the unfixed code, and case 3 fails on the ENGINE side,
which is what pins the cache fix.

It is a separate harness because comsys_handoff cannot test delivery.
That driver's shape is "establish state under one implementation, read it
under the other", and bConnected is runtime state set when a player joins
during that process -- it is not persisted.  A run inheriting membership
from an earlier run has user records with bConnected false and delivers to
nobody.  Each side must join and speak within one process.

That cost real time and briefly looked like "the module never delivers at
all".  It does; verified over a real socket against netmux before drawing
any conclusion.  A note in comsys_handoff records why the cases are not
there, so nobody adds them back.

Case 5 is weak by construction and kept deliberately: it asserts
CHATFORMAT is ABSENT under OVERRIDE, which is trivially true when
CHATFORMAT never works at all, so it passes in both columns.  It earns its
place only once the others pass, where it distinguishes OVERRIDE from
FORMAT alone.

make test green: 1561/1561 on all three smoke routes, handoff 13/13,
mogrify 5/5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:53:36 -06:00
Stephen Dennis
b2561ba99d fix(comsys): read LOG_TIMESTAMPS by value, so the module can turn it off (#1585)
The engine wrote "1" to enable and atr_clr()ed to disable, then read the
attribute back with atr_get_info -- a presence test that never looks at the
value.  Self-consistent for engine-written data, and wrong the moment the
comsys module writes it.

The module cannot clear.  mux_IAttributeAccess exposes SetAttribute and no
remove (modules.h:552), so its only way to say "off" is to write ""
(comsys_mod.cpp:2742).  Under a presence test that reads as ON, so a
channel whose timestamps were turned off under the module came back
timestamped under the engine.

Read the value instead.  The engine never writes empty -- it writes "1" or
clears -- so no engine-written data changes meaning, and non-empty is
already how the module reads it (comsys_mod.cpp:1126).  Both
implementations now agree: "1" is on, empty or absent is off.  No module
API change, which is what makes this the small end of the fix; adding a
clear to mux_IAttributeAccess is a larger question and #1572 needs it
decided anyway.

Measured with two muxscript passes over ONE database -- the first with
`module comsys_mod` configured, the second without, so the module writes
the attribute and the built-in engine reads it.  Linux runs the module
implementation under the harness (#1581), which is what makes this
reachable here at all:

  pass 1 (module):    @cset/timestamp_logs tschan=1 -> LOG_TIMESTAMPS "1"
                      @cset/timestamp_logs tschan=0 -> LOG_TIMESTAMPS ""
  pass 2 (built-in):  crecall shows the line

  before: [Mon Jul 27 11:02:17 2026] <TS> Wizard says, "hello there"
  after:                             <TS> Wizard says, "hello there"

Negative control: with the value test forced back to presence, the
timestamp returns.  Worth recording that the control also caught a bad
assertion in my probe -- it matched "[<digit>" while the timestamp begins
"[Mon", so the check passed vacuously until the control disagreed with it.

make test green, both smoke routes 1560/1560.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:06:15 -06:00
Stephen Dennis
e0204e7720
Merge pull request #1559 from brazilofmux/fix/1555-cset-log-message
fix(comsys): report why @cset/log failed instead of blaming the value (#1555)
2026-07-27 07:25:58 -06:00
Stephen Dennis
cf3ec66051 fix(comsys): report why @cset/log failed instead of blaming the value (#1555)
do_chanlog and do_chanlog_timestamps each fail for several unrelated
reasons and returned a bare bool, so do_cset rendered one message for all
of them -- the one blaming the argument.  On a channel with no channel
object:

    @cset/log cgtest=20
    "@cset: Maximum history must be a number less than or equal to 200."

for a value that plainly satisfies that condition.  The history buffer
lives on the channel object, so with no object there is nowhere to record
it; that is a missing prerequisite, not a bad argument.

The downstream symptom does not point back here either.  Until the buffer
is set, cbuffer() is 0 and crecall() returns empty on a channel with
obvious traffic -- so it reads as a broken crecall, and nothing connects
that to @cset/object.  I lost a probe cycle to exactly this while writing
comsys coverage, concluding @cset/log was flaky before noticing the working
case had an object attached and the failing one did not.

Both helpers now return a ChanLogResult naming the reason, and do_cset
picks the message:

    CHANLOG_OK            unchanged success text
    CHANLOG_BAD_VALUE     the existing value message (still correct here)
    CHANLOG_NO_OBJECT     "... has no channel object.  Use @cset/object ..."
    CHANLOG_NOT_LOGGING   "... has no history buffer.  Use @cset/log ..."
    CHANLOG_INTERNAL      a plain failure, for mkattr failing

do_chanlog_timestamps is fixed alongside rather than left as the odd one
out: it is the same defect in the twin function, and its old message ("Is
logging enabled for %s?") was right for only one of its four failure paths.
A bad value there now says so instead.

Message style follows the rest of the do_cset switch: tprintf(T(...)),
since every one of these takes a format argument.  M_() is used in this
file only for literals passed straight to raw_notify, and tprintf(M_(...))
appears nowhere in the tree, so no NLS decision is implied.

Verified on Windows Server 2022, MSVC 14.51, Release x64.  Clean build, 0
warnings.  Every path exercised live:

    no object      -> "Channel cgtest has no channel object.  Use ..."
    value 9999     -> the value message (unchanged, still correct)
    value abc      -> the value message (unchanged, still correct)
    ts, no object  -> "... has no channel object.  Use @cset/object ..."
    ts, no buffer  -> "... has no history buffer.  Use @cset/log ..."
    ts, value 7    -> "Timestamp logging for channel cgtest must be 0 or 1."
    success paths  -> byte-identical to before, and cbuffer reports 20

Smoke 1524 succeeded / 17 failed, 318/318 dispatched -- but that is a
regression check only, not evidence for the fix: nothing in testcases/
invokes @cset/log or @cset/timestamp_logs today, so the suite cannot
distinguish this change.  The live transcript above is the evidence.
PR #1554 adds a fixture that does use @cset/log, which will cover the
success path once it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 07:21:59 -06:00
Stephen Dennis
b9f18ee32e nls: replace no-placeholder tprintf(T()) notify constants with M_ (#1419)
A few sites wrapped constant prose in tprintf() with no format arguments
(comsys max-aliases message, Lua-module-loaded line, three mail admin
notices). Drop the no-op tprintf and mark with M_() so they enter the
catalog like other constant notify prose. Leave real format strings as
T(). Regenerate pot/xx without fuzzy entries.
2026-07-27 12:59:45 +00:00
Stephen Dennis
a4333ae720 nls: mark comsys notify prose with M_ (#1419)
Next notify slice: channel speak/join/list/cset/cboot feedback in
comsys.cpp (~44 constant raw_notify sites). Leave tprintf formats as T().
Keep blank-line raw_notify as T("") (#1443). Half-mark pass promotes
funceval's notify_quiet "Set." twin. Regenerate pot/xx without fuzzy
entries.
2026-07-27 12:11:17 +00:00
Stephen Dennis
b5ee3b6f81 refactor: use UTF-8 text in user-facing string literals (#1513)
Replace ~900 typographic \xE2\x80\x.. escapes (curly quotes, en dashes)
and a few \xE2\x80\230 octal workarounds with real UTF-8 in message
strings under mux/modules and mux/src. Leave stringutil and convert
charset mapping tables as explicit byte sequences.
2026-07-26 21:52:48 -06:00
Stephen Dennis
e7eb6ec76d nls: route literal #-1 softcode tokens through S_ (#1475)
Mechanical hygiene under the opt-in M_() design: replace T("#-1…") and
T("#-2…") with S_() so softcode ABI tokens are obvious in source and
cannot enter a player catalog. ~400 call sites across engine, exp3,
mail, and driver. Assembled/library-spliced diagnostics (plan §4.2)
are unchanged where they are not a single literal.
2026-07-27 01:00:55 +00:00
Stephen Dennis
2f106f200f fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373)
Completes the sweep the issue called for.  mux_atol returns long, which
is 32-bit on LLP64, so every caller silently truncated on Windows.  Two
of those were real defects (the truthiness family and cf_size, fixed in
the preceding commits); the rest were latent, waiting for a value large
enough to matter.

Rather than audit 290 sites for whether each can reach 2^31 today, use
the 64-bit parser everywhere and remove the class.  A dbref cannot
overflow now, but nothing stops a later caller passing that same site a
timestamp or a byte count.

Pure 1:1 substitution: 285 lines changed, and every removed line
contained mux_atol while every added line contains mux_atoi64.  No
control flow, no types, no behaviour beyond the wider parse.

This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so
the generated code there is unchanged.  It only widens the parse on
Windows.  Narrowing destinations are unaffected either way: `int x =
mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both
models.

Left alone: mux_atol itself in mathutil, its declaration, and three
comments that name it.  Callers that genuinely want 32-bit semantics can
still ask for them; none appear to.

Verified on Windows: full solution builds clean with no new warnings,
smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched --
identical to before the sweep, with the same 16 build-configuration
failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST).
Spot checks after the change: the boolean family returns 1 for multiples
of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited,
and arithmetic, string and list functions are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:12:35 -06:00
Stephen Dennis
8227de7f8d fix(comsys): chanfind by name; purge aliases on channel destroy
- #1204: fun_chanfind matches channel name or header (case-insensitive).
  Default colored [name] headers made name-only lookups fail while help
  examples implied reverse header lookup; both are now supported.
- #1199: do_destroychannel, do_channelnuke, and module DestroyChannel
  sweep other players' aliases that pointed at the destroyed channel
  so in-memory comsys state matches SQLite CASCADE cleanup.
- Update help.txt and add smoke TC015b for name-based chanfind.
2026-07-25 11:13:18 -06:00
Stephen Dennis
37917971f6 fix(comsys/mail): guard gen-sync re-entrancy; bump control-interface IIDs
Review findings on #1191:

1. Re-entrancy.  sqlite_load_comsys() calls select_channel() for every
   channel_users row, and select_channel() now calls
   ensure_comsys_softcode_sync() -- which assigns s_seen_rev only AFTER
   the load returns, so the nested call saw the same stale revision and
   started the load again.  Instrumenting the hook showed it really does
   re-enter (depth 2 on a @ccreate + addcom + chanusers probe with the
   modules active); it terminated incidentally rather than by design, so
   a future change to the loader could turn it into unbounded recursion.
   Guard both hooks on mudstate.bSQLiteLoading -- depth is now 1 with the
   same results.

2. IID bumps.  GetRevision (comsys) and GetRevision/SoftcodeSend (mail)
   grow the control-interface vtables, so a stale comsys_mod.so /
   mail_mod.so must fail mux_CreateInstance discovery instead of being
   called through slots it does not implement.  Bump IID_IComsysControl
   and IID_IMailControl (the #817 pattern, matching the IID_IMailStorage
   bump that went with #1203).  Verified the modules still load and the
   gen-sync still works after the bump.

Verified: @list modules reports comsys_mod/mail_mod loaded (the PR's
mechanism works -- all four required exports are now present); module
-owned channels are visible to softcode chanusers(); full make test
green, smoke 1407/1407, 0 crashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 09:43:29 -06:00
Stephen Dennis
fb6362eb61 fix(comsys/mail): activate modules and sync softcode with module store (#1191)
Root cause: comsys_mod/mail_mod lacked mux_CanUnloadNow, so ModuleLoad
never set bLoaded and CreateInstance always returned CLASSNOTAVAILABLE —
commands and softcode both used the engine path by accident.

With modules actually loaded:
- Bump module revision on every write-through; softcode reloads engine
  maps from SQLite when the revision advances (select_channel, mail_fetch,
  MailList, etc.).
- CComsysStorage::SyncChannel sets has_comsys so softcode reload works.
- Channel Sync uses ON CONFLICT DO UPDATE so updates do not CASCADE-wipe
  channel_users/player_channels.
- mailsend() goes through IMailControl::SoftcodeSend; bodies and
  mail_db_top are write-through for softcode mail_* fields.
2026-07-25 08:33:42 -06:00
Stephen Dennis
40b057f588 fix(comsys): correct inverted @cdestroy channel lookup (#1189)
The engine path treated a found channel as missing and continued with an
end-iterator when the name was absent. Use == end() for the miss check.
2026-07-25 07:58:18 -06:00
Stephen Dennis
132c197e94 fix: comsys V0123 channel-name UTF-8 boundary backoff read wrong buffer (#841)
load_comsystem_V0123 converts the channel-name line to UTF-8 (pBufferUnicode,
updating nChannel), and when it exceeds MAX_CHANNEL_LEN backs off to a whole-
character boundary — but the backoff loop read temp (the pre-conversion line)
while the bytes copied come from pBufferUnicode. The header case just below does
it right (reads pBufferUnicode), so this was a copy-paste inconsistency.

Not memory-unsafe (nChannel <= 50 and temp is an LBUF, so in bounds; memcpy
count never exceeds pBufferUnicode), but loading a pre-2007 V0123 comsys file
with a channel name > 50 bytes after conversion could truncate the name mid-
character, leaving invalid UTF-8 in ch->name. Read pBufferUnicode[nChannel-1]
(matching the header path). Legacy load path only; smoke 1255/1255.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:01:04 -06:00
Stephen Dennis
9dfa2c404d engine: comsys/mail created in-game now survive a warm boot
Found verifying #783: the SQLite loaders are gated on metadata keys --
sqlite_load_comsys() on has_comsys, sqlite_load_mail() on mail_db_top
-- but ONLY the DbConvert -C/-m import path ever set them.  Channel
and mail mutations write through to the tables row-by-row, so a
channel created with @ccreate (or mail sent in-game) on a game that
never went through a -C/-m import landed in the tables and was then
ignored forever: the next warm boot found no gate, fell back to the
legacy comsys.db/mail.db flatfiles, and the runtime came up without
them while the orphaned rows lingered.

The write-throughs now maintain the gates: sqlite_wt_channel() sets
has_comsys, and sqlite_wt_mail_body() keeps mail_db_top current (it
sizes the loader's body array).  One cheap metadata upsert alongside
mutations that are already rare.

Verified: a channel @ccreate'd and mail sent in one muxscript session
now warm-load in a fresh process -- db_unload -C/-m exports both,
where before it exported nothing ("has no comsys/mail") despite the
rows sitting in the tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:49 -06:00
Stephen Dennis
1a770c2902 engine: add LBufPtr owning handle; convert a verified subset to RAII (#717)
Incremental progress on the alloc_lbuf/free_lbuf -> LBuf RAII migration, adding
the structural enabler the issue calls for and converting a small, individually
verified set of the harder (ownership-escaping) sites.

alloc.h:
  - LBuf gains release() (relinquish ownership to a caller) and reset() (free
    now, become empty). release() lets a producer that returns an lbuf use RAII
    for its early/error paths and hand the buffer off at the success return.
  - New LBufPtr: an owning, nullable, movable handle. Unlike LBuf (which always
    allocates and suits scoped scratch), LBufPtr can be empty, reseated, and
    released — for conditional ownership, storing an lbuf in a struct/queue
    entry, or producing a buffer to return. LBufPtr_Src / LBufPtr_Adopt mirror
    the LBuf macros.

Converted sites (each verified to neither double-free nor leak on any path):
  - flags.cpp: unparse_object_numonly, unparse_object — single-exit producers,
    now LBuf + release().
  - comsys.cpp: call_mogrifier — LBufPtr + release() (its early nullptr returns
    precede the allocation, so they are unaffected).
  - functions.cpp: switch_handler / switchall_handler — the ping-pong mbuff/tbuff
    scratch buffers now use LBuf RAII, removing six manual free_lbuf() calls
    including the error-prone early-return path in switch_handler. mbuff is only
    transiently aliased into mudstate.switch_token (restored before scope exit),
    so RAII scope-exit freeing preserves its lifetime.

The remaining ~145 manual sites (fargs[] stores, did_it() charge/runout swaps,
cross-function/struct lifetimes) still need per-site structural work; #717 stays
open. Build clean, all 1053 smoke tests pass (the corpus exercises switch()/
case()/unparse_object heavily).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:52:07 -06:00
Stephen Dennis
900e3e3175 Convert ~75 atr_get/atr_pget sites to LBuf_Adopt across 17 files
Use the new LBuf_Adopt() macro to take RAII ownership of caller-owned
pool buffers returned by atr_get, atr_pget, and atr_get_LEN.  This
eliminates ~82 explicit free_lbuf calls and automatically covers
early-return paths that previously required careful manual placement
of the free.

Heaviest conversions: player.cpp (13), comsys.cpp (14), command.cpp (9).
Complex interleaved patterns (did_it charge/runout swaps, PureName
reassignment, process_cmdent loops) left manual for now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:34:51 -06:00
Stephen Dennis
9b4c2946c2 Migrate ~55 alloc_lbuf/free_lbuf sites in large engine files to LBuf RAII
Fourth wave: ast (11), funceval (10), funceval2 (7), comsys (3),
speech (10), set (6), engine (7), command (10).  Covers the NOEVAL
handlers (cand/cor/if/switch/iter), function evaluators (ifelse,
letq, objeval, sortby, munge, while, sandbox), notify_check message
buffers, and command dispatch paths.  Buffers stored in fargs[]
arrays, returned through output pointers, or used in ping-pong swap
patterns are left manual.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 18:14:49 -06:00
Stephen Dennis
974964719c Convert all static scratch buffers to thread_local
43 static scratch-buffer arrays across 21 files (5 in mux/src, 38 in
mux/modules/) changed from `static` to `thread_local`. Under the
current single-threaded evaluator this is a zero-behavior-change swap
— `thread_local` storage has the same lifetime and zero-allocation
properties as `static` — but each thread gets its own copy, which
makes these functions safe for a future multi-threaded evaluator
without any locking.

Read-only constant tables (`aRadix64`, `aRadixPenn36`,
`aRadixPenn64`, `Empty`) left as `static` because they are immutable
shared data.

Affected areas:
  net.cpp        — queue_string co_buf, trimmed_site, dump_users NameField
  signals.cpp    — signal_desc
  stubslave.cpp  — Stub_PipePump
  attrcache.cpp  — sqlite_attr_buf
  boolexp.cpp    — parsestore
  command.cpp    — preserve_cmd, SpaceCompressCommand, LowerCaseCommand
  comsys.cpp     — NewTitle, Buffer, temp
  db.cpp         — tbuff, Buffer (x2)
  flags.cpp      — buff
  funceval.cpp   — textbuff
  functions.cpp  — TimeBuffer64, TimeBuffer80, Buffer
  help.cpp       — Line, Buffer
  mail.cpp       — aFolders, Buffer, res, szFittedMailAliasDesc
  match.cpp      — buffer
  player.cpp     — szSalt, buf (x2), buff
  plusemail.cpp   — buf
  predicates.cpp — Buf (x2), pName
  session.cpp    — szFittedDoing
  set.cpp        — pRestrictedKeyText
  unparse.cpp    — buf, boolexp_buf
  mail_mod.cpp   — result, res, buf

All 21 files verified with g++ -std=c++17 -fsyntax-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:21:14 -06:00
Stephen Dennis
620a7328fa Add offset/limit pagination to cwho(), channels(), chanusers(), maillist()
Fixes silent LBUF truncation on large channel/mailbox lists (GitHub #704).
All four list-returning functions now accept optional offset/limit args
for softcode pagination. Backward compatible — existing calls unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:10:37 -06:00
Stephen Dennis
a1206de44f Fix four engine ISSUES.md findings, remove resolved items
- MakeCanonicalUserFunctionName: reject too-long names instead of
  silently truncating (caller already handles pbValid=false)
- HTML escaping: escape exit names in xch_cmd attribute values
- Command deletion: free CEF_ALLOC commands before replacing in htab
- Comsys truncation: reject too-long channel names with error message
- Predicates cleanup: remove stale hash entries when no backup command

Removed resolved issues (#3 truncation, #6 HTML, #7 command deletion,
#9 comsys, #10 predicates) from ISSUES.md. Renumbered remaining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:46:49 -06:00
Stephen Dennis
fa18bdae47 Replace stack-allocated LBUF arrays with pool-backed LBuf RAII wrapper
Add LBuf class to alloc.h: an RAII wrapper around alloc_lbuf/free_lbuf
that moves LBUF_SIZE buffers from the stack to the heap pool. Convert
all 108 non-static UTF8 xxx[LBUF_SIZE] stack arrays across 25 source
files. Static BSS buffers (24) are unchanged.

This eliminates LBUF_SIZE from recursive stack frames, making it safe
to increase LBUF_SIZE without risking stack overflow in the evaluation
pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:52:30 -06:00
Stephen Dennis
2a814dade8 Add chanfind() reverse lookup and bulk field output for chanusers()
Addresses staff feedback about channel name/header correlation and the
cost of looping over chanusers() to get per-user data.

chanfind(header):
  Given a channel header string (e.g. "<PublicServices>"), returns the
  canonical channel name (e.g. "PubServ"). Case-insensitive match with
  subscriber-aware visibility. Solves the problem of players seeing a
  display header in chat and not knowing the internal channel name.

chanusers(channel[, sep[, field]]):
  Optional third argument returns per-user field values instead of
  dbrefs. Supported fields: title, status, flags, gagjoin, comtitles,
  alias (Wizard-only), name. This avoids the N-call loop pattern:
    chanusers(PubServ, |, title)  =>  Cherry|Trees|Muffin

5 new smoke tests (636/641 total, 5 pre-existing failures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:36:01 -06:00
Stephen Dennis
9d0d716e22 Fix @clist/full display and add access-check fields to chaninfo()
Addresses staff feedback about @clist/full being effectively broken when
games disable the cp/co permission flags in favor of lock-based access
control.

@clist/full changes:
- Add Header column so players can correlate display names with channels
- Show owner by name (Moniker) instead of raw integer dbref
- Replace raw flag bits with effective access for the querying player
  (JXR based on test_*_access which respects both flags and locks)
- Drop dead Balance/Charge/Obj columns
- Fix subscriber visibility (same bug fixed for channels() in prior commit)

Also fix subscriber visibility in normal @clist.

New chaninfo() fields:
- canjoin: 1/0 based on test_join_access(executor)
- cantransmit: 1/0 based on test_transmit_access(executor)
- canreceive: 1/0 based on test_receive_access(executor)

These let softcode build rich channel listings that reflect actual
access control (flags + locks), not just the raw permission bits.

3 new smoke tests (631/636 total, 5 pre-existing failures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:24:52 -06:00
Stephen Dennis
c1a40d0a25 Implement chaninfo(), chanusers(), chanuser() comsys accessors
Phase 1 of the softcode accessor redesign (docs/design-softcode-accessors.md).

Three new generic field-based channel query functions that expose all
schema-stored comsys data with subscriber-aware visibility:

- chaninfo(channel, field): 11 fields (name, header, owner, object,
  type, flags, charge, users, msgs, desc, buffer). The object field
  preserves the existing Wizard-only restriction from chanobj().

- chanusers(channel[, sep]): delimited list of subscriber dbrefs.

- chanuser(channel, player, field): 6 per-user fields (alias, title,
  status, flags, gagjoin, comtitles) with per-field permissions matching
  existing comalias/comtitle/cflags semantics.

Also fixes channels() to include non-public channels where the executor
is a subscriber — previously subscribers were locked out of their own
channel metadata.

22 new smoke tests (628/633 total, 5 pre-existing failures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:08:00 -06:00
Stephen Dennis
53bcc37a1c Switch Windows build from /MT to /MD and add mux_fclose
The modular architecture (netmux.exe, libmux.dll, engine.dll, etc.)
with static CRT (/MT) gave each module its own CRT heap. FILE* handles
from mux_fopen (in libmux) crashed when used by stdio functions in
other modules — fclose in write_pidfile, fgets in cf_include, etc.

Switch all 11 vcxproj files to /MD (shared CRT DLL) so all modules
share one CRT instance. Ship msvcp140.dll, vcruntime140.dll, and
vcruntime140_1.dll in the binary distribution.

Also add mux_fclose to libmux as good hygiene (pairs with mux_fopen),
and replace all cross-module fclose calls. This change is safe on Unix
where everything links into one process.

Add Startmux.bat as a replacement for Startmux.wsf since Windows
Script Host is no longer associated by default on modern Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 06:07:24 -06:00
Stephen Dennis
fe3cda5a79 Add PennMUSH and RhostMUSH feature adoption
PennMUSH: #lambda/#apply anonymous inline attributes, @include with
/nobreak /localize /clearregs, channel mogrifiers (MOGRIFY`BLOCK,
MESSAGE, FORMAT, OVERRIDE, NOBUFFER), cmogrifier().

RhostMUSH: between(), delextract(), garble(), caplist(), moon(),
soundex(), soundlike(), while(), crc32obj(), subnetmatch(),
wrapcolumns(), sandbox() with AST fallback for JIT compatibility.

21 new features/functions, 15 new smoke test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:08:21 -06:00
Stephen Dennis
1f1b4da008 Add @chatformat — per-player channel message formatting
Players can set a CHATFORMAT attribute on themselves to customize how
channel messages appear.  When a channel message is delivered, if the
recipient has CHATFORMAT, it is evaluated with:

  %0 = channel name
  %1 = default formatted message
  %2 = sender dbref

The output replaces the default message for that player only.

Example: &CHATFORMAT me=\[[ansi(hc,%0)]\] %1

Engine-side implementation (comsys.cpp SendChannelMessage).
Lazy-initialized attribute lookup — zero overhead when no players
use CHATFORMAT.  Help text added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:36:22 -06:00
Stephen Dennis
bba4e51f39 Channel API: 8 new PennMUSH-compatible channel query functions
cbuffer(channel) — buffer size (MAX_LOG depth)
cdesc(channel) — channel description from chan_obj DESC
cflags(channel[, player]) — channel flags (PLS) or per-user flags (OGQ)
cmsgs(channel) — total message count
cowner(channel) — channel owner dbref
crecall(channel[, lines[, sep]]) — recall message history
cstatus(channel, player) — On/Off/Gag status
cusers(channel) — subscriber count

All include visibility checks (public/control/Comm_All).
Help text entries added for all 8 functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:14:26 -06:00
Stephen Dennis
aa03b51a38 Engine-side comsys: full STL conversion — mirror module patterns
Apply the same STL conversion to the engine-side comsys (comsys.h,
comsys.cpp, funceval.cpp) that was done to the comsys module:

- comsys_table[500] hash with ->next chains → unordered_map<dbref, comsys_t>
- comuser** sorted array + num_users/max_users → map<dbref, comuser>
- on_users intrusive linked list → bool bConnected flag
- ALIAS_SIZE packed stride arrays → vector<com_alias> with string members
- UTF8 *title (StringClone/MEMFREE) → std::string
- MEMALLOC(sizeof(channel)) → new channel() (STL members need constructors)
- Eliminated: create_new_comsys, add_comsys, destroy_comsys, sort_users,
  NUM_COMSYS, ALIAS_SIZE, static num_channels
- funceval.cpp fun_cwho(): iterate users map instead of array/linked list

Net: −927 deleted, +443 added across 3 files. All 551 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:42:08 -06:00
Stephen Dennis
b8fbb939ce Fix: sqlite_sync_comsys fails on orphaned channel aliases
Real-world comsys.db files can contain player aliases pointing to
channels that no longer exist (e.g., a deleted "Mudnet" channel).
The FK constraint on player_channels.channel_name → channels.name
rejects the insert, aborting the entire comsys import.

Skip orphaned aliases via select_channel() check before inserting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:08:10 -06:00
Stephen Dennis
48777504e4 Eliminate mux_string from notify pipeline and trim remaining small uses
Rewrite notify_check() from mux_string to raw PUA-encoded UTF-8 lbufs.
The engine now passes PUA strings through; the network layer converts
PUA to ANSI/XTERM/HTML per client.  html_escape() replaces encode_Html(),
co_strip_color() replaces export_TextPlain() for @listen matching.

Remove mux_string overloads: raw_notify, raw_notify_html,
send_text_to_player (session.cpp, conn_bridge.cpp, net.cpp).
Convert handle_ears, look_for_exits (predicates.cpp), notify_comsys
(comsys.cpp), and small uses in mail.cpp/match.cpp.
Add extern "C" guards to color_ops.h for C++ inclusion.

593/593 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:31:08 -06:00
Stephen Dennis
4ff1398de1 Restructure mux/ directory: component-based layout with proper build root
Move from flat mux/src/ layout to clean component hierarchy:
- mux/ is now the autoconf/automake build root (configure.ac lives here)
- mux/include/ — shared headers used by multiple components
- mux/lib/ — libmux.so (core utilities, no game state)
- mux/src/ — netmux driver only (thin networking shell)
- mux/modules/engine/ — engine.so (game logic)
- mux/modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ — external modules
- mux/ganl/ — GANL networking library
- mux/sqlite/ — SQLite amalgamation (builds libsqlite3.a)
- mux/announce/ — announce tool (was mux/src/tools/)

Build changes:
- SUBDIRS ordering: ganl sqlite lib src modules announce
- libmux.so gets -Wl,-soname,libmux.so; netmux links via -L -lmux
- engine.so links libsqlite3.a and libmux.so with -Wl,--no-undefined
- RPATH uses $ORIGIN for portable .so resolution
- Install hooks use absolute paths for game/bin symlinks

Bug fixes:
- engine.so mux_Register() now passes nullptr to mux_RegisterClassObjects
  (matches all other modules; libmux already has the factory via dlsym)
- DbConvert() now calls pcache_init() before db_write, fixing a latent
  crash (free(): invalid pointer) when exporting from SQLite databases

411/411 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:38:37 -06:00
Renamed from mux/src/comsys.cpp (Browse further)