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.
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.
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>
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.
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).
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.
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>
%-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.
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>
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.
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>
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>
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>
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>
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.
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>
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.
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>
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.
#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.
- #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.
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.
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.
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>
- 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>
- 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>
- 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>
- 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>
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>