Commit graph

40 commits

Author SHA1 Message Date
Stephen Dennis
5465b2fbeb fix(com,mail,sql): bound CResultsSet fields, channel names, malias caps (#1878 #1874 #1875)
CResultsSet constructor and NextField require length headers and payloads
to fit the result blob.  CreateChannel rejects names longer than
MAX_CHANNEL_LEN before duplicate checks.  Mail aliases cap at 50 per
owner and 100 members, including on load.
2026-07-31 09:12:46 -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
8d7999c70b feat(modules): GAME_CONFIG -- game-policy config reaches modules (#1654)
The parking condition on #1654 was "a second consumer"; the tally reached
three, all documented divergences:

    searchcost       mail    non-wizard self @mail/stats was free
    eval_comtitle    comsys  comtitles always evaluated under the module
    money_name_*     mail    the charge-refusal text could not be composed

## Shape

A new engine-registered class rather than an Initialize signature change:

    mux_IGameConfig::GetGameConfig(GAME_CONFIG *)    CID_GameConfig

No existing IID moves, so there is no ABI break in either direction: an old
module never asks, and a new module against an old engine gets
CLASSNOTAVAILABLE, soft-fails to nullptr, and keeps its prior behaviour.

Two design rules baked in rather than documented and hoped for:

  * Queried per CALL, never cached at Initialize.  A boot-time snapshot is
    #1613's bug -- @admin reports Set. while the module keeps stale values.
    @admin search_cost=50 now takes effect on the next @mail/stats.

  * Versioned by cbSize: caller zeroes the struct and sets cbSize; the
    engine fills what fits.  The struct can grow without a new interface,
    and zero must stay a safe default for every future field.

## Both consumers, verified as a mortal

The gap was wizard-invisible -- payfor() exempts wizards and every harness
runs as God -- so verification used muxscript -p with a @pcreate'd mortal:

    search_cost 5, rich       engine 150->145   module 150->145   IDENTICAL
    search_cost 99999, poor   both: "Finding mail stats costs 99999
                              Pennies.", money untouched

    eval_comtitle default     [ec] 3 Wizard says ...            both sides
    eval_comtitle 0           [ec] [strlen(abc)] Wizard says    both sides

The eval_comtitle rows close the divergence documented at
channel_speaker_name since #1640/#1647.

Conformance and smoke are unchanged by construction (wizard runs), and were
run anyway.

make test: Smoke 1561 x3, conformance PASSED, handoff 13.  TESTEXIT=0.

Refs #1613, #1614, #1631, #1640, #1647.  Closes #1654.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:35 -06:00
Stephen Dennis
3cd1f2e36d nls: mark dual-path comsys module player prose with M_() (#1419)
Engine comsys already marked join/leave/@cset/@clist/comlist sentences
and table labels; the loadable module (2.14 default) still spoke those
msgids via T(), so LANGUAGE=xx never hit the live path. Mark the module
notify/RawNotify surface to match, including table header labels and
Permission denied. +15 pot msgids; xx complete; ko partial append.
2026-07-28 12:12:21 -06: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
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
db2aa4e09e feat(libmux): mux_table layout helpers for multi-column notifies (#1667)
Phase 3 of the NLS-safe table design: put pad/truncate/emit helpers on
co_copy_field in libmux so modules and engine share one layout path.

- mux/include/mux_table.h + mux/lib/mux_table.c (module-safe, freestanding C)
- comsys_mod / mail_mod: drop local append_* copies; call mux_table_*
- tests/table: ljust, trunc, CJK width, emit_fields header/row parity
- docs: mark Phase 3 done; Phase 4 still owns blob-header conversion

Verified: MSVC Release libmux + modules; tests/build-msvc.sh table PASSED.
2026-07-28 14:10:05 +00:00
Stephen Dennis
5abb3684e7 fix(comsys): finish #1640 rebase onto co_copy_field / mux_format
After replaying command-surface parity onto master (with #1655/#1656):
use co_copy_field for column sites, mux_sprintf with T() formats, bump
IID_IObjectInfo for UnparseObject, and drop comsys from BAN_LEGACY.
2026-07-27 22:40:00 -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
08d666ac76 chore(comsys): lay out channel columns with co_copy_field (#1653)
%-N.Ns via snprintf measures bytes and can cut mid-UTF-8; mux_sprintf
measures codepoints, which still is not display width.  Convert the five
comlist/@clist/@cwho column sites to co_copy_field (Unicode 16 clusters,
visual width, color close) and ratchet BAN_LEGACY 78 → 73.
2026-07-27 22:06:07 -06:00
Stephen Dennis
73b570b313 fix(comsys): six raw snprintf sites, and record the mail-size divergence
The ratchet added alongside #1631 caught #1640 landing 7 new raw snprintf
call sites in comsys_mod.cpp while that guard was being written -- the drift
it exists to stop, observed inside a single merge.  Six format a player name
with a plain "%s" into a fixed buffer, which is exactly the case that emits
half a UTF-8 sequence, and are converted to mux_sprintf.

The remaining two format channel-list columns with "%-13.13s"-style
precision.  mux_vsnprintf counts CODEPOINTS there where snprintf counts
bytes, so converting them silently re-widens every CJK column.  That belongs
to #1649, which is about visual-width columns, so BAN_LEGACY records 78
rather than 77 with the reason written down.

Blessing the conformance baseline collapses the mail-stats divergence from
five lines to one character: with both implementations finally reporting the
same statistic, the engine's byte total is one lower than the module's for a
byte-identical body.  Both store 26 characters and both accumulate
MessageFetchSize + 1, but the engine's accessor returns 25 -- inconsistent
with its own mailreview, which returns 26.  Filed as #1652; not fixed here
because correcting it changes long-standing engine output.

Backlog for the remaining 242 legacy sites is #1653.  The searchcost gap in
the module's stats is #1654.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 22:01:19 -06:00
Stephen Dennis
7d5ea6458b fix(comsys): three of four module command-surface divergences (#1640)
Closes the command-output gaps #1640 listed for items 1, 3 and 4, plus
padding and speech comtitle that the same comparison found.

1. @clist/full — honour CLIST_FULL in the module (do_chanlist hands the
   key through before checking the switch). Columns match do_listchannels.
3. comtitle — channel_speaker_name() for join/leave and ordinary speech,
   matching BuildChannelMessage (SPOOF replaces; otherwise title + name).
4. delcom — disconnect before the leave broadcast (bConnected gates
   delivery), personal "You have left channel" confirmation, suppress
   Hidden; mechanism-matched so busy-channel broadcast still reaches others.
Also pad @clist description to 45/column 79.

Item 2 (@cwho dbref+flags) needs Examinable/UnparseObject on a module
interface and an IID bump — deferred.

Verification is tests/comsys_conformance, not a separate harness: the
baseline re-blessed 39 → 29 divergent lines as the three fixes landed.
2026-07-28 02:20:23 +00:00
Stephen Dennis
0b6e617f79 fix(comsys/mail): check the storage writes both modules were discarding (#1630)
Fifteen storage write results were discarded across the two modules.  Each
is a write that can be refused -- foreign keys, constraint violations, a
closed database, a full disk -- and each failed silently, letting in-memory
state and stored state diverge with nothing to notice.  That is the shape of
#1564, #1585, #1620 and #1587.

#1629 fixed one of them (DeleteMailBody, refused by a foreign key) and cost
a full investigation to find, because the only symptom was mail being
unreadable in-session while a restart read it fine.

  mail_mod.cpp    7  UpdateMailReadFlags, DeleteMailHeader,
                     DeleteAllMailHeaders, SyncMailBody, PutMeta,
                     ClearMailAliases, SyncMailAlias
  comsys_mod.cpp  8  SyncChannelUser, SyncChannel, SyncPlayerChannel,
                     DeletePlayerChannel, DeleteChannelUser,
                     DeleteChannel x2, DeleteAllPlayerChannels

Also InsertMailHeader, which is not in #1630's list: its result was already
consumed for the rowid, so a failure set sqlite_id = -1 and said nothing.
The message then lives in memory and not in the database -- it survives the
session and vanishes on restart.  Same class, so it is logged too.

Repeating #1629's fourteen-line block fifteen times would be worse than the
bug, so each module gets a log_storage_failure(mr, fmt, ...) helper that is
silent on success.  A call site wraps its call directly and names the
operation, the key identifying the row, and the result code -- #1587 needed
to know *which* body could not be deleted.  The helper carries
__attribute__((format(printf, 3, 4))), so GCC type-checked all fifteen
format strings against their arguments.

No behaviour change beyond the logging.

## Verified, not assumed

The logging only fires on failure, so a green run proves nothing by itself.
Forced a genuine refusal using channel_users.channel_name REFERENCES
channels(name):

  control    channel_users = ichan|1
  injected   channel_users empty -- the write was refused
             CComsysStorage::SyncChannelUser returns MUX_E_FAIL
             log_storage_failure fires; start_log reports fStarted = 1

Injection reverted; no residue.

## One caveat, filed as #1633

#1630 says checking "converts silent divergence into a log grep".  Under
muxscript that grep can never match.  CLogFile starts with bEnabled = false,
WriteBuffer early-returns on it, and the only StartLogging() caller in the
tree is mux/src/driver.cpp -- netmux.  start_log still reports fStarted = 1,
so a caller has every reason to believe it logged.

So these lines land in a real server and are invisible to the harness that
tests comsys/mail.  The already-merged #1620 and #1629 lines have the same
property.  Filed separately rather than fixed here.

(The startup line "Comsys: using module implementation." is a bare
fprintf(stderr, ...) at engine_com.cpp:2944, not evidence that logging
works -- I nearly took it as such.)

tests/comsys_handoff: 13 passed, 0 failed.

Refs #1587, #1614, #1620, #1629, #1633.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:28:19 -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
4edea3c4d8 fix(comsys): let the module write engine-owned channel attributes (#1585, #1620)
Both bugs are one refusal.  The engine writes HISTORY_%d, MAX_LOG and
LOG_TIMESTAMPS on the channel object as GOD with AF_CONST, and
bCanSetAttr denies AF_CONST in every branch -- God included, which is
unlike every other flag it checks.  So the module's permission-checked
SetAttribute is refused, and whichever implementation did not write a
value first can never change it.

  #1585  a channel whose timestamps the engine enabled could not be
         turned off from the module, and before #1624 the module
         reported success anyway.

  #1620  the same refusal reaching HISTORY_%d once the ring wraps onto
         an engine-written slot.  num_messages is a column and keeps
         counting, so the counter and the history diverge -- #1564's
         symptom arriving by a second route.

mux_IComsysStorage gains SetChannelAttr: the module asks the ENGINE to
perform the write.  The engine owns the attribute layer and the
permission check, and here it is writing its own data rather than a
player's, so an attribute add is its operation to make.

Two alternatives were weighed and rejected:

  - Dropping AF_CONST from the five comsys writes is the smallest
    change, but then anyone controlling the channel object can forge
    recall history with a plain @set.  Channel logs stop being
    tamper-evident, which is what AF_CONST was buying.

  - Giving God an escape from AF_CONST in bCanSetAttr is one line and
    restores an invariant that holds for every other flag there, but it
    is a global permission change to fix one subsystem's problem.

Scope is deliberately narrow.  The channel is passed by NAME, so the
engine resolves the object itself and this cannot become "write any
attribute anywhere as GOD".  Player-owned attributes are untouched --
mail's Mailcurf/Mailfolders keep going through the permission-checked
mux_IAttributeAccess, which is the right path for anything a player
owns.

tests/comsys_handoff now passes with no TODO markers:

  ok 4 - module can clear a flag the engine set (#1585)
  ok 6 - module overwrites an engine-written history slot (#1620)
  === comsys handoff: 8 passed, 0 failed, 0 known-failing ===

The harness demanded this edit rather than allowing it: with the fix in
and the markers still present it reported "2 unexpectedly passing" and
failed the run, which is what those markers are for.

One assertion changed direction rather than being deleted.  #1624 added
a check that a REFUSED write was reported honestly instead of as
success; there is no refusal left to report, so it now checks that the
clear reports success -- because it now is one.  Asserting on the old
failure text would have failed for the right reason in the wrong
direction.

make test green: 1561/1561 on all three smoke routes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:35:33 +00:00
Stephen Dennis
08e09e0034 fix(comsys,mail): check the nine discarded SetAttribute results (#1620)
mux_IAttributeAccess::SetAttribute is permission-checked, and bCanSetAttr
denies AF_CONST in every branch including God's.  The engine writes these
same attributes as GOD with AF_CONST, so on a game that has run the
built-in implementation these writes are refused -- and all nine call
sites across the two modules discarded the result.

Measured consequences, both on macOS with the module asserted live:

  - @cset/timestamp_logs told the player "timestamp logging set." after
    the write was refused and the value was unchanged.  That is #1585,
    and reporting success is why it read as a mysterious divergence
    rather than a refusal.

  - a channel history write refused on ring wrap-around vanished with no
    trace, while num_messages (a relational column) kept counting.  That
    is #1564's counter-vs-history divergence returning by another route.

This does not make the refused writes land -- that needs the storage
decision on #1589.  It converts silent data loss into something a log
grep finds, and stops the modules reporting the opposite of what
happened.

comsys: the two @cset paths now report failure instead of success, the
MAX_LOG shrink path reports when old entries could not be cleared, and
the history write logs the attribute, object and result code.

mail: the four Mailcurf/Mailfolders writes go through one checked
helper, since a refused folder write silently reverts the player's
folder list and looks like the command never ran.

Verified under netmux, where the module log is visible:

  COM/HIST : Comsys module: channel history write refused
             (HISTORY_1 on #12, result -11); message not recorded.

-11 is MUX_E_PERMISSION.  Exactly one refusal per ring pass, since only
the engine-written slot is denied -- so this does not flood.

Controls: the module setting and clearing its OWN attribute still
succeeds and still reports success, so the checks do not fire on the
ordinary path.

Known limitation: the module log is not visible under muxscript (the
same gap as #1596), so the harness cannot yet see these.  The @cset
messages are visible on both.

make test green: 1561/1561 on both smoke routes, with the modules live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:08:05 -06:00
Stephen Dennis
978f90e63a fix(comsys): match engine EvalWithArgs identity for NOBUFFER (#1572)
call_mogrifier evaluates as (chan_obj, chan_obj, speaker), not as GOD.
blocked_by_mogrify already did; nobuffer_by_mogrify now does too.

Also skip NOBUFFER for join/leave traffic — the engine only runs MOGRIFY`*
when !bJoinLeaveMsg, so join/leave history stayed logged.  Tighten the
xlate approximation so "#N" is true and "#-" is false.
2026-07-27 18:18:43 +00:00
Stephen Dennis
4cbf2555b2 fix(comsys): honour MOGRIFY`NOBUFFER in the module (#1572)
The engine skips the recall buffer when the channel object's
MOGRIFY`NOBUFFER hook evaluates true (comsys.cpp:1727).  The module wrote
history unconditionally, so a channel that suppressed a line under the
engine recorded it under the module.

#1572's premise about why the other hooks are hard is wrong, and that is
the more useful half of this change.  It says the module "reaches the
engine only through mux_IAttributeAccess, mux_IObjectInfo and mux_INotify,
none of which expose an evaluator", and concludes that MESSAGE/FORMAT/
OVERRIDE need a new module API before they can be implemented.

mux_IEvaluator exists (modules.h:344), the comsys module already holds it
as m_pIEvaluator and acquires it at startup, and its EvalWithArgs is fully
implemented engine-side (engine_com.cpp:1807) -- its interface comment
says it was added for MOGRIFY`BLOCK (#1194).  So the remaining hooks are
ordinary work, not a design decision.  blocked_by_mogrify holds the
evaluator, guards on it being non-null, and then reads the raw attribute
text without evaluating -- which is exactly the "evaluated softcode versus
literal text" divergence #1572 documents for BLOCK.

This hook is therefore evaluated, not read raw: three arguments (channel
name, message, sender as "#<dbref>") through EvalWithArgs, matching
call_mogrifier, with xlate() parity on the result.  A hook added now should
match the engine rather than inherit BLOCK's shortcut.

Measured with the module live -- Linux runs it under the harness (#1581):

  &MOGRIFY`NOBUFFER nbobj=1
  before the hook is set:  recorded
  after:                   suppressed

  recall: "<NB> ... has joined" + "line-before-hook"     (no line-after-hook)

Negative control: with the gate disabled, line-after-hook reaches history.

make test green, both smoke routes 1560/1560.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:39:35 -06:00
Stephen Dennis
802582f0d5 fix(comsys): honour LOG_TIMESTAMPS in the module history writer (#1570)
RecordChannelHistory wrote the bare message always, so
@cset/timestamp_logs reported success while crecall stayed untimestamped
under module comsys_mod.

Consult LOG_TIMESTAMPS on the channel object (non-empty = on, matching
how the module clears it to "") and wrap with
CLinearTimeAbsolute::ReturnDateString(0) in the same "[%s] %s" shape as
the engine. libmux already exports that type; the earlier "no time source"
rationale was incorrect.

Verified with module loaded: plain history, then after timestamp_logs on
a new line is prefixed with a local ReturnDateString stamp.
2026-07-27 15:48:18 +00:00
Stephen Dennis
f06edb5e33 fix(comsys): the module never wrote channel history, so crecall was empty (#1564)
Red master. Three cases in comsysemit_fn.mux fail wherever the comsys
module is active, and they are right to: crecall genuinely returns nothing
on a channel with obvious traffic.

CComsysMod::SendChannelMessage incremented num_messages, wrote the channel
row, and notified listeners. It never wrote HISTORY_n. The module's own
do_crecall reads HISTORY_n, so recall was reading attributes that nothing
in the module wrote. The engine writes them at comsys.cpp:1851; only the
engine ever did.

That is why the symptom looked contradictory: cmsgs reported messages and
cbuffer reported a buffer, because those come from the channel row and the
MAX_LOG attribute, while the history itself was never recorded. Nothing
logged an error, so it was invisible to anything that did not read the
history back.

RecordChannelHistory mirrors the engine's logging block:

  - index is num_messages % logmax computed AFTER the increment, which is
    exactly what do_crecall's read expects
  - join/leave messages are logged too; the engine guards its block only on
    the channel object being valid, not on bJoinLeaveMsg, and its recall
    output does include "has joined this channel"
  - GOD (dbref 1) is the executor, matching the engine's
    atr_add(..., GOD, ...) and this module's existing GOD-executor reads of
    channel-object attributes. The speaker is an ordinary member who will
    not usually control the channel object, so passing the executor through
    would fail bCanSetAttr for most messages.

LOG_TIMESTAMPS is NOT honoured here. The engine wraps history lines in a
timestamp when that attribute is set; the module has no equivalent time
source in reach, so @cset/timestamp_logs still does nothing under the
module. That is a pre-existing second divergence, left as-is rather than
half-implemented, and worth its own issue.

This also fixes the Windows build defect that hid all of it from me. The
four module entry points in comsys_mod.cpp and mail_mod.cpp were declared

    extern "C" MUX_RESULT DCL_API mux_Register(void)

with no DCL_EXPORT, unlike exp3 and sqlproxy which have it. ELF exports by
default, so Unix was unaffected; on Windows the DLLs exported nothing at
all, mux_AddModule could never load them, and the fallback to the built-in
engine implementation is silent -- the else branch in
discover_comsys_mail_modules logs nothing. So comsys.dll and mail.dll were
built and shipped but unloadable, and every comsys test I wrote was
validated against the engine path without my knowing a second
implementation existed.

Verified on Windows Server 2022, MSVC 14.51, Release x64:

  - dumpbin now shows mux_Register/mux_Unregister/mux_CanUnloadNow/
    mux_GetClassObject exported from comsys.dll and mail.dll; before, both
    exported nothing
  - with "module comsys" in the config the module loads and initializes
  - BEFORE the history fix, the Linux failure reproduces exactly here:
    cbuffer=20, cmsgs=3, crecall empty, lattr(chanobj) = Created Modified
    MAX_LOG with no HISTORY_n
  - AFTER: crecall returns the history, cemit text reaches it, and
    lattr(chanobj) shows HISTORY_1..HISTORY_4
  - smoke on the engine path unchanged: 1532 succeeded, 17 failed, and
    comsysemit TC002/TC003/TC006 pass

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 08:02:30 -06: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
9649651ae5 fix(comsys): module gag/charge/BLOCK and join/who/dbck parity
#1194 transmit path:
- reject Gagged non-wizards
- collect channel charge via new IObjectInfo PayFor/GiveTo (engine payfor)
- evaluate MOGRIFY`BLOCK via IEvaluator EvalWithArgs before send

#1195 membership/visibility:
- MAX_USERS_PER_CHANNEL on join; bConnected from UNDEAD semantics
- suppress join/leave announce when Hidden
- comwho filters Hidden (WizardWho/SeeHidden) and lists objects
- load skips invalid who; dbck prunes invalid only (keep object listeners)

COM: bump IID_IObjectInfo and IID_IEvaluator for the new vtable slots.
Rebuild exp3/mail/comsys modules against the new IIDs.
2026-07-25 13:43:00 -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
938ff1baa3 Merge master into fix/1191-dual-store (take master's #1192/#1203 text, keep bump_revision) 2026-07-25 09:23:04 -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
670a62690b fix(comsys): complete module PlayerNuke teardown (#1193)
Mirror engine ReleaseAllResources for comsys: disconnect, clear aliases
and membership, destroy owned channels (and dangling aliases to them),
drop the player's comsys row, and DeleteAllPlayerChannels in storage.
2026-07-25 08:08:44 -06:00
Stephen Dennis
4f99e6b63f fix: resolve Pass 3 Medium audit defects #1081–#1085
WebSocket (#1081–#1083):
- Require client mask bit (RFC 6455 §5.1); unit test added
- Handshake reject flushes + closes; SHA-1 failure is fatal 500
- ws_queue_frame enforces output_limit drop-oldest on framed size

comsys_mod (#1084):
- IPermissions::CouldDoit; join/transmit/receive OR channel locks

conf (#1085):
- cf_bool/cf_option/cf_string live-sync g_dc; site_chars live int

Smoke 1319/1319; websocket_test 14/14.
2026-07-24 13:18:24 -06:00
Stephen Dennis
110cd3894a Make mail/comsys m_cRef atomic
Global component/server-lock counters in both modules were already
converted to std::atomic, but per-instance m_cRef fields remained plain
uint32_t with non-atomic ++/-- and a Release() decrement-then-check
that could race to double-delete under concurrent access.

Promote CMailMod, CMailModFactory, CComsysMod, and CComsysModFactory
m_cRef to std::atomic<uint32_t>. AddRef uses fetch_add (relaxed);
Release uses fetch_sub (acq_rel) with the previous-value check so the
delete decision is based on the atomic read-modify-write result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:03:38 -06:00
Stephen Dennis
9271471d41 Fix mail/comsys false alarms, PlayerNuke guard, debian packaging
- mail_mod.cpp: close malloc/free mismatch and FinalConstruct cleanup
  as false alarms (strdup→free is correct; destructor null-checks all
  interface pointers individually).
- comsys PlayerNuke(): add player < 0 guard returning MUX_E_INVALIDARG.
- debian/postinst: new — restricts config files to mode 640 on install.
- convert/configure.ac: error if yacc/lex not found instead of silent
  fallthrough; fix AC_CHECK_LIB to test yywrap instead of main.
- Update ISSUES.md trackers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:00:52 -06:00
Stephen Dennis
b6f44f6e15 Fix console 80x24 reconnect, atomic module counters, parser warning
- Console hydra_connection.cpp: reconnect path now uses cached
  termWidth_/termHeight_ instead of hardcoded 80x24.
- mail_mod.cpp, comsys_mod.cpp: g_cComponents and g_cServerLocks
  changed from uint32_t to std::atomic<uint32_t> for thread safety.
- parser/mux_parse.h: token_name() changed from static to inline,
  eliminating -Wunused-function warning under -Wall -Wextra.
- Update ISSUES.md trackers; close debian stubslave as intentional.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:20:20 -06:00
Stephen Dennis
32d15df3c4 Fix Tier 2 issues: comsys dbck, worldbuilder paths, console parsing
- comsys dbck(): implement channel consistency checks — prune users
  for destroyed players, reassign orphaned channel ownership to GOD,
  remove comsys entries for destroyed players.
- worldbuilder test_worldbuilder.py: os.chdir to script directory at
  startup so fixture paths resolve from any working directory.
- console spawn.cpp: report regex compilation errors to stderr instead
  of silently swallowing them.
- console world.cpp: validate >> extraction on world/hydra lines;
  skip malformed entries with diagnostic.
- dowin32.sh: distinguish diff exit code 1 (normal) from >= 2 (error).
- debian/README.Debian: update version references from 2.12 to 2.14.
- Close false alarms: executor.py log file already closed in finally.
- Update ISSUES.md trackers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:45:39 -06:00
Stephen Dennis
1814aabf1e Harden restart-file loading, signal globals, and COM error paths
- load_restart_db(): clamp all memcpy into fixed-size fields (addr,
  doing, username, output_prefix, output_suffix) to destination size;
  validate num_main_game_ports against MAX_LISTEN_PORTS; replace
  mux_assert(0) with graceful close-and-return; check fgets() return.
- Signal globals: g_shutdown_flag and g_panicking changed from bool to
  volatile sig_atomic_t; slave.cpp counters likewise.
- COM modules: mail, comsys, and exp3 FinalConstruct() now check every
  mux_CreateInstance() return value and bail early on failure.
- engine_com.cpp: log storage interface creation failures for comsys
  and mail instead of silently continuing.
- dowin32.sh: quote dirname arguments to prevent word splitting.
- Update ISSUES.md trackers to reflect fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:23:30 -06:00
Stephen Dennis
a52f0b160c Comsys module: full STL conversion — eliminate manual memory management
Replace all hand-managed C-era data structures with STL containers:

- comuser: strdup/free title → std::string, on_next linked list → bConnected flag
- channel: realloc'd sorted comuser** array → std::map<dbref, comuser>,
  on_users intrusive linked list eliminated (iterate map filtering bConnected)
- comsys_t: packed ALIAS_SIZE-stride array + parallel strdup'd channel array →
  std::vector<com_alias> with std::string members, hash chain pointer removed
- m_channels: raw channel* → std::unique_ptr<channel>
- m_comsys_table[500] hash table → std::unordered_map<dbref, comsys_t>
- Removed NUM_COMSYS, ALIAS_SIZE, m_num_channels, create_new_comsys, add_comsys
- sort_com_aliases → std::sort, select_user → map::find, get_comsys returns ref

Net result: −641 lines deleted, +264 added. All 551 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:03:24 -06:00
Stephen Dennis
ca6758c416 Fix Windows build for COM storage interface changes
- engine_com.cpp: Use engine-side log_text() instead of driver-side
  g_pILog->log_text() for comsys/mail init logging
- comsys_mod.cpp: Replace stale CloseDatabase() with m_pIStorage
  release in shutdown() (database connection now owned by engine)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:50:00 -06:00
Stephen Dennis
14edd376ca Route comsys_mod/mail_mod SQLite access through COM storage interfaces
Replace direct -lsqlite3 linking in comsys_mod.so and mail_mod.so with
COM interfaces (mux_IComsysStorage, mux_IMailStorage) provided by
engine.so. This eliminates the SQLite version mismatch risk from mixing
system libsqlite3 with engine.so's vendored static copy on the same
database file.

- Add mux_IComsysStorage (11 methods) and mux_IMailStorage (14 methods)
  to modules.h with C function pointer callbacks for ABI safety
- Implement CComsysStorage/CMailStorage in engine_com.cpp wrapping
  CSQLiteDB methods
- Remove all sqlite3 API calls from comsys_mod.cpp and mail_mod.cpp
- Remove -lsqlite3 from modules/Makefile.am build lines
- All 493 smoke tests pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:39:27 -06:00
Stephen Dennis
68acbd2eea Rework Windows build for component-based directory layout
Adapt all vcxproj files and solution to the new directory structure
(src/ driver, lib/ shared library, modules/engine/ game logic,
modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ loadable modules).

Key changes:
- libmux.dll exports utility symbols via LIBMUX_API macro
  (__declspec(dllexport) when BUILDING_LIBMUX, dllimport otherwise)
- LIBMUX_API added to all shared headers: stringutil.h, timeutil.h,
  mathutil.h, utf8tables.h, svdhash.h, svdrand.h, sha1.h, alloc.h,
  dbutil.h, core.h
- Per-file PreprocessorDefinitions in libmux.vcxproj inherit from
  project-level via %(PreprocessorDefinitions)
- Driver factory declarations (CDriverControlFactory,
  CConnectionManagerFactory) guarded with BUILDING_DRIVER
- PCG-XSH-RR-64/32 (pcg32) for Windows (no __int128 needed);
  Unix PCG-XSL-RR-128/64 unchanged
- MSVC portability fixes: _strnicmp, _BitScanForward64, (std::min)(),
  HAVE_WORKING_FORK guards, WINDOWS_FILES/UNIX_FILES ModuleAdd paths
- Remove stubslave.cpp and slave.cpp from netmux.vcxproj (separate
  processes)
- Fix sqlproxy/sqlslave vcxproj relative paths for new layout
- Add ws2_32.lib to engine.vcxproj for socket functions
- Add strcasecmp/strtok_r/strndup compat shims for comsys/mail

Builds successfully: libmux.dll, engine.dll, netmux.exe, exp3.dll,
sqlproxy.dll, sqlslave.dll. Comsys/mail blocked on sqlite3 linking
architecture (need COM-mediated or independent sqlite3 linkage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:38:57 -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/modules/comsys_mod.cpp (Browse further)