tinymux/mux/modules/comsys/comsys_mod.cpp

4555 lines
132 KiB
C++
Raw Permalink Normal View History

/*! \file comsys_mod.cpp
* \brief Comsys Module Channel system as a loadable module
*
* This module implements the MUX channel system as a dynamically loaded
* module. It hooks into server events for player connect/disconnect
* and provides mux_IComsysControl for command dispatch.
*
* Core dependencies are accessed exclusively through COM interfaces:
* mux_INotify player notification
* mux_IObjectInfo object property queries
* mux_IAttributeAccess attribute read/write
* mux_IEvaluator softcode evaluation
* mux_IPermissions permission checks
* mux_ILog logging
*
* Channel data is loaded from and persisted to the game's SQLite database
* via the module's own sqlite3 connection.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "libmux.h"
#include "modules.h"
#include "timeutil.h"
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 20:31:58 -06:00
// Column layout: co_copy_field (#1649 / #1653 / #1656). cheader storage
// still uses StripTabsAndTruncate via core.h so ANSI collapse matches the
// engine's do_cheader. Plain assembly uses mux_sprintf (mux_format.h).
//
#include "core.h"
#include "mux_format.h"
#include "mathutil.h"
#include "color_ops.h"
#include "mux_table.h"
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 20:31:58 -06:00
#include "comsys_mod.h"
#include <atomic>
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
#include <cstdarg>
#include <cstring>
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
// Windows compatibility for POSIX functions.
//
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif
// Column layout: libmux mux_table_* on co_copy_field (#1667 Phase 3).
//
#define append_ljust_field mux_table_append_ljust
#define append_bytes mux_table_append_bytes
// Module bookkeeping.
//
static std::atomic<uint32_t> g_cComponents{0};
static std::atomic<uint32_t> g_cServerLocks{0};
// Module entry points.
//
static MUX_CLASS_INFO comsys_classes[] =
{
{ CID_ComsysMod }
};
#define NUM_CLASSES (sizeof(comsys_classes)/sizeof(comsys_classes[0]))
// Required by libmux ModuleLoad — without this, bLoaded stays false and
// CreateInstance never finds the class (#1191 root cause).
//
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
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_CanUnloadNow(void)
{
if ( 0 == g_cComponents
&& 0 == g_cServerLocks)
{
return MUX_S_OK;
}
return MUX_S_FALSE;
}
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
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Register(void)
{
MUX_RESULT mr = MUX_E_UNEXPECTED;
if ( 0 == g_cComponents
&& 0 == g_cServerLocks)
{
mr = mux_RegisterClassObjects(NUM_CLASSES, comsys_classes, nullptr);
}
return mr;
}
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
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Unregister(void)
{
return mux_RevokeClassObjects(NUM_CLASSES, comsys_classes);
}
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
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_GetClassObject(MUX_CID cid, MUX_IID iid, void **ppv)
{
MUX_RESULT mr = MUX_E_CLASSNOTAVAILABLE;
if (CID_ComsysMod == cid)
{
CComsysModFactory *pFactory = nullptr;
try
{
pFactory = new CComsysModFactory;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pFactory)
{
return MUX_E_OUTOFMEMORY;
}
mr = pFactory->QueryInterface(iid, ppv);
pFactory->Release();
}
return mr;
}
// ---------------------------------------------------------------------------
// CComsysMod — main module class.
// ---------------------------------------------------------------------------
CComsysMod::CComsysMod(void) : m_cRef(1),
m_pILog(nullptr),
m_pIServerEventsControl(nullptr),
m_pINotify(nullptr),
m_pIObjectInfo(nullptr),
m_pIAttributeAccess(nullptr),
m_pIEvaluator(nullptr),
m_pIPermissions(nullptr),
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
m_pIGameConfig(nullptr),
m_pIStorage(nullptr),
m_revision(0)
{
g_cComponents++;
}
MUX_RESULT CComsysMod::FinalConstruct(void)
{
MUX_RESULT mr;
// Acquire logging interface.
//
mr = mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
IID_ILog,
reinterpret_cast<void **>(&m_pILog));
if (MUX_FAILED(mr))
{
return mr;
}
// Register for server events.
//
mux_IServerEventsSink *pIServerEventsSink = nullptr;
mr = QueryInterface(IID_IServerEventsSink,
reinterpret_cast<void **>(&pIServerEventsSink));
if (MUX_SUCCEEDED(mr))
{
mr = mux_CreateInstance(CID_ServerEventsSource, nullptr,
UseSameProcess, IID_IServerEventsControl,
reinterpret_cast<void **>(&m_pIServerEventsControl));
if (MUX_SUCCEEDED(mr))
{
m_pIServerEventsControl->Advise(pIServerEventsSink);
}
pIServerEventsSink->Release();
}
// Acquire core interfaces.
//
mr = mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
IID_INotify,
reinterpret_cast<void **>(&m_pINotify));
if (MUX_FAILED(mr))
{
return mr;
}
mr = mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess,
IID_IObjectInfo,
reinterpret_cast<void **>(&m_pIObjectInfo));
if (MUX_FAILED(mr))
{
return mr;
}
mr = mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess,
IID_IAttributeAccess,
reinterpret_cast<void **>(&m_pIAttributeAccess));
if (MUX_FAILED(mr))
{
return mr;
}
mr = mux_CreateInstance(CID_Evaluator, nullptr, UseSameProcess,
IID_IEvaluator,
reinterpret_cast<void **>(&m_pIEvaluator));
if (MUX_FAILED(mr))
{
return mr;
}
mr = mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess,
IID_IPermissions,
reinterpret_cast<void **>(&m_pIPermissions));
if (MUX_FAILED(mr))
{
return mr;
}
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
// Optional (#1654): an engine older than CID_GameConfig simply lacks the
// class, and every consumer defaults to the engine's compiled-in
// behaviour. Hard-failing here would make the module unloadable against
// a binary it otherwise works with.
//
mr = mux_CreateInstance(CID_GameConfig, nullptr, UseSameProcess,
IID_IGameConfig,
reinterpret_cast<void **>(&m_pIGameConfig));
if (MUX_FAILED(mr))
{
m_pIGameConfig = nullptr;
}
// Log that we are alive.
//
if (nullptr != m_pILog)
{
bool fStarted;
MUX_RESULT mr2 = m_pILog->start_log(&fStarted, LOG_ALWAYS,
T("INI"), T("INFO"));
if (MUX_SUCCEEDED(mr2) && fStarted)
{
m_pILog->log_text(T("Comsys module loaded."));
m_pILog->end_log();
}
}
return mr;
}
CComsysMod::~CComsysMod()
{
// Release the storage interface.
//
if (nullptr != m_pIStorage)
{
m_pIStorage->Release();
m_pIStorage = nullptr;
}
// STL containers clean up automatically.
//
m_channels.clear();
m_comsys.clear();
if (nullptr != m_pILog)
{
bool fStarted;
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
T("INI"), T("INFO"));
if (MUX_SUCCEEDED(mr) && fStarted)
{
m_pILog->log_text(T("Comsys module unloading."));
m_pILog->end_log();
}
m_pILog->Release();
m_pILog = nullptr;
}
if (nullptr != m_pIServerEventsControl)
{
m_pIServerEventsControl->Release();
m_pIServerEventsControl = nullptr;
}
if (nullptr != m_pINotify)
{
m_pINotify->Release();
m_pINotify = nullptr;
}
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->Release();
m_pIObjectInfo = nullptr;
}
if (nullptr != m_pIAttributeAccess)
{
m_pIAttributeAccess->Release();
m_pIAttributeAccess = nullptr;
}
if (nullptr != m_pIEvaluator)
{
m_pIEvaluator->Release();
m_pIEvaluator = nullptr;
}
if (nullptr != m_pIPermissions)
{
m_pIPermissions->Release();
m_pIPermissions = nullptr;
}
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
if (nullptr != m_pIGameConfig)
{
m_pIGameConfig->Release();
}
g_cComponents--;
}
MUX_RESULT CComsysMod::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IComsysControl *>(this);
}
else if (IID_IComsysControl == iid)
{
*ppv = static_cast<mux_IComsysControl *>(this);
}
else if (IID_IServerEventsSink == iid)
{
*ppv = static_cast<mux_IServerEventsSink *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CComsysMod::AddRef(void)
{
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
}
uint32_t CComsysMod::Release(void)
{
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
if (1 == prev)
{
delete this;
return 0;
}
return prev - 1;
}
// ---------------------------------------------------------------------------
// Channel data loading via engine storage interface.
// ---------------------------------------------------------------------------
bool CComsysMod::LoadChannels(void)
{
if (nullptr == m_pIStorage)
{
return false;
}
MUX_RESULT mr = m_pIStorage->LoadAllChannels(
[](void *ctx, const UTF8 *name, const UTF8 *header,
int type, int temp1, int temp2, int charge, int charge_who,
int amount_col, int num_messages, int chan_obj)
{
CComsysMod *self = static_cast<CComsysMod *>(ctx);
auto ch = std::make_unique<channel>();
if (nullptr != name)
{
strncpy(reinterpret_cast<char *>(ch->name),
reinterpret_cast<const char *>(name), MAX_CHANNEL_LEN);
ch->name[MAX_CHANNEL_LEN] = '\0';
}
if (nullptr != header)
{
strncpy(reinterpret_cast<char *>(ch->header),
reinterpret_cast<const char *>(header), MAX_HEADER_LEN);
ch->header[MAX_HEADER_LEN] = '\0';
}
ch->type = type;
ch->temp1 = temp1;
ch->temp2 = temp2;
ch->charge = charge;
ch->charge_who = charge_who;
ch->amount_col = amount_col;
ch->num_messages = num_messages;
ch->chan_obj = chan_obj;
std::vector<UTF8> key(ch->name,
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
self->m_channels[key] = std::move(ch);
}, this);
return MUX_SUCCEEDED(mr);
}
bool CComsysMod::LoadChannelUsers(void)
{
if (nullptr == m_pIStorage)
{
return false;
}
MUX_RESULT mr = m_pIStorage->LoadAllChannelUsers(
[](void *ctx, const UTF8 *channel_name, int who,
bool is_on, bool comtitle_status, bool gag_join_leave,
const UTF8 *title)
{
CComsysMod *self = static_cast<CComsysMod *>(ctx);
if (nullptr == channel_name) return;
struct channel *ch = self->select_channel(channel_name);
if (nullptr == ch) return;
// #1195: skip invalid dbrefs; object listeners are allowed.
//
if (nullptr != self->m_pIObjectInfo)
{
bool bValid = false;
self->m_pIObjectInfo->IsValid(who, &bValid);
if (!bValid)
{
return;
}
}
comuser cu;
cu.who = who;
cu.bUserIsOn = is_on;
cu.ComTitleStatus = comtitle_status;
cu.bGagJoinLeave = gag_join_leave;
cu.bConnected = false;
if (nullptr != title && title[0] != '\0')
{
cu.title = reinterpret_cast<const char *>(title);
}
ch->users[who] = std::move(cu);
}, this);
return MUX_SUCCEEDED(mr);
}
bool CComsysMod::LoadPlayerChannels(void)
{
if (nullptr == m_pIStorage)
{
return false;
}
MUX_RESULT mr = m_pIStorage->LoadAllPlayerChannels(
[](void *ctx, int who, const UTF8 *alias,
const UTF8 *channel_name)
{
CComsysMod *self = static_cast<CComsysMod *>(ctx);
if (nullptr == alias || nullptr == channel_name) return;
comsys_t &c = self->m_comsys[who];
c.who = who;
com_alias ca;
ca.alias = reinterpret_cast<const char *>(alias);
ca.channel = reinterpret_cast<const char *>(channel_name);
c.aliases.push_back(std::move(ca));
}, this);
return MUX_SUCCEEDED(mr);
}
// ---------------------------------------------------------------------------
// Internal data structure helpers.
// ---------------------------------------------------------------------------
struct channel *CComsysMod::select_channel(const UTF8 *name)
{
std::vector<UTF8> key(name,
name + strlen(reinterpret_cast<const char *>(name)) + 1);
auto it = m_channels.find(key);
if (it != m_channels.end())
{
return it->second.get();
}
// Case-insensitive fallback.
//
for (auto &kv : m_channels)
{
if (0 == strcasecmp(reinterpret_cast<const char *>(name),
reinterpret_cast<const char *>(kv.second->name)))
{
return kv.second.get();
}
}
return nullptr;
}
struct comuser *CComsysMod::select_user(struct channel *ch, dbref player)
{
if (nullptr == ch)
{
return nullptr;
}
auto it = ch->users.find(player);
if (it != ch->users.end())
{
return &it->second;
}
return nullptr;
}
comsys_t &CComsysMod::get_comsys(dbref who)
{
comsys_t &c = m_comsys[who];
c.who = who;
return c;
}
const UTF8 *CComsysMod::get_channel_from_alias(dbref player, const UTF8 *alias)
{
static const UTF8 empty[] = { '\0' };
auto it = m_comsys.find(player);
if (it == m_comsys.end())
{
return empty;
}
const comsys_t &c = it->second;
for (const auto &ca : c.aliases)
{
if (ca.alias == reinterpret_cast<const char *>(alias))
{
return reinterpret_cast<const UTF8 *>(ca.channel.c_str());
}
}
return empty;
}
// ---------------------------------------------------------------------------
// Connect/disconnect helpers.
// ---------------------------------------------------------------------------
void CComsysMod::do_comconnectraw_notify(dbref player, const UTF8 *chan)
{
struct channel *ch = select_channel(chan);
if (nullptr == ch)
{
return;
}
struct comuser *cu = select_user(ch, player);
if (nullptr == cu)
{
return;
}
// Only send if channel is LOUD, user is on, and player is not hidden.
//
if ( (ch->type & CHANNEL_LOUD)
&& cu->bUserIsOn)
{
UTF8 msg[MOD_LBUF_SIZE];
const UTF8 *pName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetName(player, &pName);
}
if (nullptr == pName)
{
pName = T("???");
}
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s %s has connected."),
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(pName));
if (nullptr != m_pINotify)
{
for (auto &kv : ch->users)
{
comuser &u = kv.second;
if (u.bConnected && u.bUserIsOn && !u.bGagJoinLeave)
{
m_pINotify->RawNotify(u.who, msg);
}
}
}
}
}
void CComsysMod::do_comdisconnectraw_notify(dbref player, const UTF8 *chan)
{
struct channel *ch = select_channel(chan);
if (nullptr == ch)
{
return;
}
struct comuser *cu = select_user(ch, player);
if (nullptr == cu)
{
return;
}
if ( (ch->type & CHANNEL_LOUD)
&& cu->bUserIsOn)
{
UTF8 msg[MOD_LBUF_SIZE];
const UTF8 *pName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetName(player, &pName);
}
if (nullptr == pName)
{
pName = T("???");
}
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s %s has disconnected."),
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(pName));
if (nullptr != m_pINotify)
{
for (auto &kv : ch->users)
{
comuser &u = kv.second;
if (u.bConnected && u.bUserIsOn && !u.bGagJoinLeave)
{
m_pINotify->RawNotify(u.who, msg);
}
}
}
}
}
void CComsysMod::do_comconnectchannel(dbref player, const UTF8 *channel,
const std::string &alias)
{
struct channel *ch = select_channel(channel);
if (nullptr != ch)
{
struct comuser *user = select_user(ch, player);
if (nullptr != user)
{
user->bConnected = true;
}
else if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Bad Comsys Alias: %s for Channel: %s"),
alias.c_str(),
reinterpret_cast<const char *>(channel));
m_pINotify->RawNotify(player, msg);
}
}
else if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Bad Comsys Alias: %s for Channel: %s"),
alias.c_str(),
reinterpret_cast<const char *>(channel));
m_pINotify->RawNotify(player, msg);
}
}
void CComsysMod::do_comdisconnectchannel(dbref player, const UTF8 *channel)
{
struct channel *ch = select_channel(channel);
if (nullptr == ch)
{
return;
}
struct comuser *user = select_user(ch, player);
if (nullptr != user)
{
user->bConnected = false;
}
}
// ---------------------------------------------------------------------------
// Write-through helpers — delegate to engine storage interface.
// ---------------------------------------------------------------------------
void CComsysMod::bump_revision(void)
{
// Wrap-safe for softcode gen-sync; softcode treats any change as dirty.
//
m_revision++;
if (m_revision < 0)
{
m_revision = 1;
}
}
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
// Report a refused storage write (#1630). Silent on success, so it can wrap
// a call directly: log_storage_failure(m_pIStorage->Foo(k), "Foo(k=%d)", k);
//
// The message carries the operation, the key that identifies the row, and the
// result code. #1620's history-write log is the same idea spelled out at one
// call site; this is that shape made cheap enough to use at all of them.
//
void CComsysMod::log_storage_failure(MUX_RESULT mr, const char *fmt, ...)
{
if ( MUX_SUCCEEDED(mr)
|| nullptr == m_pILog)
{
return;
}
char detail[192];
va_list ap;
va_start(ap, fmt);
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 20:31:58 -06:00
mux_vsnprintf(reinterpret_cast<UTF8 *>(detail), sizeof(detail), reinterpret_cast<const UTF8 *>(fmt), ap);
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
va_end(ap);
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("DB"));
if (fStarted)
{
UTF8 buf[256];
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 20:31:58 -06:00
mux_sprintf(buf, sizeof(buf),
T("Comsys module: storage write refused: %s; result %d."),
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
detail, mr);
m_pILog->log_text(buf);
m_pILog->end_log();
}
}
void CComsysMod::sqlite_wt_channel_user(const UTF8 *channel_name,
const comuser &user)
{
if (nullptr == m_pIStorage) return;
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
log_storage_failure(m_pIStorage->SyncChannelUser(channel_name, user.who,
user.bUserIsOn, user.ComTitleStatus, user.bGagJoinLeave,
reinterpret_cast<const UTF8 *>(user.title.c_str())),
"SyncChannelUser(channel=%s, who=#%d)",
reinterpret_cast<const char *>(channel_name),
static_cast<int>(user.who));
bump_revision();
}
void CComsysMod::sqlite_wt_channel(struct channel *ch)
{
if (nullptr == m_pIStorage) return;
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
log_storage_failure(m_pIStorage->SyncChannel(ch->name, ch->header,
ch->type, ch->temp1, ch->temp2, ch->charge, ch->charge_who,
ch->amount_col, ch->num_messages, ch->chan_obj),
"SyncChannel(channel=%s)",
reinterpret_cast<const char *>(ch->name));
bump_revision();
}
void CComsysMod::sqlite_wt_player_channel(dbref who, const UTF8 *alias,
const UTF8 *channel_name)
{
if (nullptr == m_pIStorage) return;
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
log_storage_failure(
m_pIStorage->SyncPlayerChannel(who, alias, channel_name),
"SyncPlayerChannel(who=#%d, alias=%s, channel=%s)",
static_cast<int>(who), reinterpret_cast<const char *>(alias),
reinterpret_cast<const char *>(channel_name));
bump_revision();
}
void CComsysMod::sqlite_wt_delete_player_channel(dbref who, const UTF8 *alias)
{
if (nullptr == m_pIStorage) return;
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
log_storage_failure(m_pIStorage->DeletePlayerChannel(who, alias),
"DeletePlayerChannel(who=#%d, alias=%s)",
static_cast<int>(who), reinterpret_cast<const char *>(alias));
bump_revision();
}
void CComsysMod::sqlite_wt_delete_channel_user(const UTF8 *channel_name,
dbref who)
{
if (nullptr == m_pIStorage) return;
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
log_storage_failure(m_pIStorage->DeleteChannelUser(channel_name, who),
"DeleteChannelUser(channel=%s, who=#%d)",
reinterpret_cast<const char *>(channel_name), static_cast<int>(who));
bump_revision();
}
// ---------------------------------------------------------------------------
// Channel access checks.
// ---------------------------------------------------------------------------
// Attribute numbers for channel-object locks (attrs.h A_LOCK/A_LUSE/A_LENTER).
// Duplicated here so the module does not include engine attrs.h.
//
static constexpr int kA_LOCK = 42;
static constexpr int kA_LENTER = 59;
static constexpr int kA_LUSE = 62;
// #1084: type-bit OR could_doit on channel object — engine comsys parity.
//
static bool channel_lock_ok(mux_IPermissions *pPerm, dbref player,
dbref chan_obj, int atr)
{
if (nullptr == pPerm || NOTHING == chan_obj)
{
return false;
}
bool ok = false;
pPerm->CouldDoit(player, chan_obj, atr, &ok);
return ok;
}
bool CComsysMod::test_transmit_access(dbref player, struct channel *ch)
{
if (nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(player, &bCommAll);
if (bCommAll)
{
return true;
}
}
int access;
if (nullptr != m_pIObjectInfo)
{
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(player, &bPlayer);
access = bPlayer ? CHANNEL_PLAYER_TRANSMIT : CHANNEL_OBJECT_TRANSMIT;
}
else
{
access = CHANNEL_PLAYER_TRANSMIT;
}
return ((ch->type & access) != 0)
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LUSE);
}
bool CComsysMod::test_receive_access(dbref player, struct channel *ch)
{
if (nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(player, &bCommAll);
if (bCommAll)
{
return true;
}
}
int access;
if (nullptr != m_pIObjectInfo)
{
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(player, &bPlayer);
access = bPlayer ? CHANNEL_PLAYER_RECEIVE : CHANNEL_OBJECT_RECEIVE;
}
else
{
access = CHANNEL_PLAYER_RECEIVE;
}
return ((ch->type & access) != 0)
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LENTER);
}
bool CComsysMod::test_join_access(dbref player, struct channel *ch)
{
if (nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(player, &bCommAll);
if (bCommAll)
{
return true;
}
}
int access;
if (nullptr != m_pIObjectInfo)
{
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(player, &bPlayer);
access = bPlayer ? CHANNEL_PLAYER_JOIN : CHANNEL_OBJECT_JOIN;
}
else
{
access = CHANNEL_PLAYER_JOIN;
}
return ((ch->type & access) != 0)
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LOCK);
}
// ---------------------------------------------------------------------------
// Policy helpers (#1194 / #1195).
// ---------------------------------------------------------------------------
bool CComsysMod::flag_set(dbref obj, int word, unsigned int bit)
{
if (nullptr == m_pIObjectInfo)
{
return false;
}
unsigned int flags = 0;
if (MUX_FAILED(m_pIObjectInfo->GetFlags(obj, word, &flags)))
{
return false;
}
return (flags & bit) != 0;
}
bool CComsysMod::is_wizard(dbref obj)
{
if (nullptr != m_pIPermissions)
{
bool b = false;
m_pIPermissions->IsWizard(obj, &b);
return b;
}
if (nullptr != m_pIObjectInfo)
{
bool b = false;
m_pIObjectInfo->IsWizard(obj, &b);
return b;
}
return false;
}
bool CComsysMod::is_gagged(dbref obj)
{
return flag_set(obj, MOD_FLAG_WORD2, MOD_FLAG_GAGGED);
}
bool CComsysMod::is_hidden(dbref obj)
{
// Engine Hidden(x) is (Flags(x) & DARK) — FLAG_WORD1.
//
return flag_set(obj, MOD_FLAG_WORD1, MOD_FLAG_DARK);
}
bool CComsysMod::is_guest(dbref obj)
{
if (nullptr == m_pIObjectInfo)
{
return false;
}
unsigned int powers = 0;
if (MUX_FAILED(m_pIObjectInfo->GetPowers(obj, &powers)))
{
return false;
}
return (powers & MOD_POW_GUEST) != 0;
}
bool CComsysMod::undead_connected(dbref obj)
{
// Engine UNDEAD(x): Good_obj && (not player || Connected).
//
if (nullptr == m_pIObjectInfo)
{
return false;
}
bool bValid = false;
m_pIObjectInfo->IsValid(obj, &bValid);
if (!bValid)
{
return false;
}
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(obj, &bPlayer);
if (!bPlayer)
{
return true;
}
bool bConnected = false;
m_pIObjectInfo->IsConnected(obj, &bConnected);
return bConnected;
}
bool CComsysMod::pay_channel_charge(dbref player, struct channel *ch)
{
if (nullptr == ch || 0 >= ch->charge)
{
return true;
}
if (nullptr == m_pIObjectInfo)
{
return true;
}
const int cost = is_guest(player) ? 0 : ch->charge;
bool bPaid = false;
if (MUX_FAILED(m_pIObjectInfo->PayFor(player, cost, &bPaid)) || !bPaid)
{
return false;
}
ch->amount_col += ch->charge;
sqlite_wt_channel(ch);
m_pIObjectInfo->GiveTo(ch->charge_who, ch->charge);
return true;
}
bool CComsysMod::blocked_by_mogrify(dbref player, struct channel *ch,
const UTF8 *arg2)
{
// Engine call_mogrifier(MOGRIFY`BLOCK): non-empty result suppresses send.
//
if ( nullptr == ch
|| ch->chan_obj < 0
|| nullptr == m_pIAttributeAccess
|| nullptr == m_pIEvaluator
|| nullptr == arg2)
{
return false;
}
bool bValid = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
}
if (!bValid)
{
return false;
}
UTF8 atext[MOD_LBUF_SIZE];
atext[0] = '\0';
size_t nLen = 0;
// GOD executor so AF_DARK channel-object attrs are readable.
//
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
T("MOGRIFY`BLOCK"), atext, sizeof(atext) - 1, &nLen);
if (MUX_FAILED(mr) || 0 == nLen || '\0' == atext[0])
{
return false;
}
UTF8 chattype[2];
chattype[0] = (':' == arg2[0] || ';' == arg2[0]) ? arg2[0] : '"';
chattype[1] = '\0';
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"), player);
const UTF8 *pName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetName(player, &pName);
}
if (nullptr == pName)
{
pName = T("???");
}
const UTF8 *block_args[5] = {
chattype, ch->name, arg2, pName, sdrBuf
};
UTF8 result[MOD_LBUF_SIZE];
result[0] = '\0';
size_t nResult = 0;
mr = m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj, player,
atext, block_args, 5, result, sizeof(result), &nResult);
if (MUX_FAILED(mr) || 0 == nResult || '\0' == result[0])
{
return false;
}
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player, result);
}
return true;
}
// ---------------------------------------------------------------------------
// Unsubscribe a player from a channel completely.
// ---------------------------------------------------------------------------
void CComsysMod::do_delcomchannel(dbref player, const UTF8 *channel,
bool bQuiet)
{
struct channel *ch = select_channel(channel);
if (nullptr == ch)
{
return;
}
struct comuser *user = select_user(ch, player);
if (nullptr == user)
{
return;
}
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 20:31:58 -06:00
// Order matters, and it is the engine's (comsys.cpp:2591). Clearing
// bConnected FIRST is why the leaver does not see their own departure
// broadcast -- SendChannelMessage skips disconnected users -- and is what
// makes the two implementations agree on the visible result.
//
do_comdisconnectchannel(player, channel);
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 20:31:58 -06:00
if (!bQuiet)
{
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 20:31:58 -06:00
if ( user->bUserIsOn
&& !is_hidden(player))
{
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 20:31:58 -06:00
UTF8 pfxNormal[MOD_LBUF_SIZE];
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
pfxNoComtitle, sizeof(pfxNoComtitle));
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
mux_sprintf(msg, sizeof(msg),
M_("%s has left this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNormal));
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNoComtitle));
SendChannelMessage(player, ch, msg,
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
}
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 20:31:58 -06:00
// The engine gives the leaver a personal confirmation (#1640). The
// module emitted only the channel broadcast, which the leaver cannot
// even see -- so `delcom` looked like it had done nothing, and any
// softcode or client trigger matching "^You have left channel"
// stopped firing under the module.
//
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 20:31:58 -06:00
if (nullptr != m_pINotify)
{
UTF8 msg[256];
mux_sprintf(msg, sizeof(msg),
M_("You have left channel %s."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(channel));
m_pINotify->RawNotify(player, msg);
}
}
ch->users.erase(player);
sqlite_wt_delete_channel_user(channel, player);
}
// ---------------------------------------------------------------------------
// Sort aliases for display/lookup.
// ---------------------------------------------------------------------------
void CComsysMod::sort_com_aliases(comsys_t &c)
{
std::sort(c.aliases.begin(), c.aliases.end(),
[](const com_alias &a, const com_alias &b)
{
return a.alias < b.alias;
});
}
// ---------------------------------------------------------------------------
// Channel message broadcasting.
// ---------------------------------------------------------------------------
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 20:31:58 -06:00
// ---------------------------------------------------------------------------
// Speaker prefix, with the comtitle the engine has always shown (#1640).
//
// The module never applied comtitles to anything: not speech, not poses, not
// join/leave, and not spoof channels -- where the comtitle is supposed to
// REPLACE the name entirely. Measured against the engine on identical
// databases, `comtitle d=Tester` then `d hello`:
//
// engine HDR Tester Wizard says, "hello"
// module HDR Wizard says, "hello"
//
// The module stored the title correctly and round-tripped it through
// SyncChannelUser; it simply never read it back out when composing a message.
//
// Two variants because the LISTENER decides, not the speaker: a player with
// comtitles off sees the plain name even when the speaker has one set. On a
// spoof channel the engine builds no second variant, so everyone sees the
// comtitle -- pNoComtitle stays empty and SendChannelMessage falls back.
//
// Follows the engine's BuildChannelMessage (comsys.cpp:1349) branch for
// branch, including that ComTitleStatus is ignored when the channel is spoof.
// ---------------------------------------------------------------------------
void CComsysMod::BuildSpeakerPrefix(struct channel *ch,
const struct comuser *user, UTF8 *pNormal, size_t nNormal,
UTF8 *pNoComtitle, size_t nNoComtitle)
{
pNormal[0] = '\0';
pNoComtitle[0] = '\0';
const bool bSpoof = ((ch->type & CHANNEL_SPOOF) != 0);
const UTF8 *pMoniker = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetMoniker(user->who, &pMoniker);
}
if (nullptr == pMoniker)
{
pMoniker = T("???");
}
const bool bHasComTitle = !user->title.empty();
if ( !bHasComTitle
|| (!user->ComTitleStatus && !bSpoof))
{
mux_sprintf(pNormal, nNormal, T("%s %s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(pMoniker));
if (!bSpoof)
{
mux_sprintf(pNoComtitle, nNoComtitle, T("%s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pNormal));
}
return;
}
// The engine evaluates the comtitle as softcode unless `eval_comtitle 0`
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
// (mudconf.eval_comtitle, default on). Since #1654 the flag reaches the
// module through mux_IGameConfig, queried per call rather than cached --
// a boot-time snapshot is #1613's bug. Against an engine without the
// interface, fall back to the default (evaluate), which is the old
// behaviour.
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 20:31:58 -06:00
//
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
bool bEvalComtitle = true;
if (nullptr != m_pIGameConfig)
{
GAME_CONFIG gc;
memset(&gc, 0, sizeof(gc));
gc.cbSize = sizeof(gc);
if (MUX_SUCCEEDED(m_pIGameConfig->GetGameConfig(&gc)))
{
bEvalComtitle = gc.eval_comtitle;
}
}
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 20:31:58 -06:00
UTF8 title[MOD_LBUF_SIZE];
mux_sprintf(title, sizeof(title), T("%s"),
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 20:31:58 -06:00
user->title.c_str());
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
if ( bEvalComtitle
&& nullptr != m_pIEvaluator)
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 20:31:58 -06:00
{
UTF8 evaled[MOD_LBUF_SIZE];
size_t nOut = 0;
if (MUX_SUCCEEDED(m_pIEvaluator->EvalWithArgs(user->who, user->who,
user->who, title, nullptr, 0, evaled, sizeof(evaled) - 1,
&nOut)))
{
evaled[nOut] = '\0';
mux_sprintf(title, sizeof(title), T("%s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(evaled));
}
}
if (bSpoof)
{
// The comtitle stands in for the speaker.
//
mux_sprintf(pNormal, nNormal, T("%s %s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(title));
return;
}
mux_sprintf(pNormal, nNormal, T("%s %s %s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(title),
reinterpret_cast<const char *>(pMoniker));
mux_sprintf(pNoComtitle, nNoComtitle, T("%s %s"),
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 20:31:58 -06:00
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(pMoniker));
}
void CComsysMod::SendChannelMessage(dbref executor, struct channel *ch,
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 20:31:58 -06:00
const UTF8 *msg, const UTF8 *msgNoComtitle, bool bJoinLeaveMsg)
{
ch->num_messages++;
sqlite_wt_channel(ch);
if (nullptr == m_pINotify)
{
return;
}
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
// MOGRIFY`MESSAGE / `OVERRIDE / `FORMAT, and per-player CHATFORMAT
// (#1572). Only BLOCK and NOBUFFER were implemented here; the other
// three hooks and CHATFORMAT were silently ignored, so a channel
// configured with them behaved differently depending on which
// implementation happened to be live.
//
// Order and precedence follow the engine exactly (comsys.cpp:1705-1822),
// because "mostly the same" is what produced the divergence in the first
// place.
//
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"),
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
static_cast<int>(executor));
UTF8 mogMsg[MOD_LBUF_SIZE];
UTF8 mogFmt[MOD_LBUF_SIZE];
mogMsg[0] = '\0';
mogFmt[0] = '\0';
bool bMogOverride = false;
// The engine evaluates these only for non-join/leave traffic
// (comsys.cpp:1701).
//
if (!bJoinLeaveMsg)
{
const UTF8 *mog_args[3] = { ch->name, msg, sdrBuf };
mogrify_eval(ch, executor, T("MESSAGE"), mog_args, 3,
mogMsg, sizeof(mogMsg));
UTF8 ovr[MOD_LBUF_SIZE];
if (mogrify_eval(ch, executor, T("OVERRIDE"), mog_args, 3,
ovr, sizeof(ovr)))
{
bMogOverride = mogrify_truthy(ovr);
}
// FORMAT sees the message AFTER MESSAGE may have replaced it.
//
const UTF8 *fmt_msg = ('\0' != mogMsg[0]) ? mogMsg : msg;
const UTF8 *fmt_args[3] = { ch->name, fmt_msg, sdrBuf };
mogrify_eval(ch, executor, T("FORMAT"), fmt_args, 3,
mogFmt, sizeof(mogFmt));
}
const UTF8 *effMsg = ('\0' != mogMsg[0]) ? mogMsg : msg;
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 20:31:58 -06:00
// The no-comtitle variant follows the same mogrify substitution the
// normal one does (engine comsys.cpp:1766-1767): a MOGRIFY`MESSAGE
// result replaces both, because it replaced the whole line.
//
const UTF8 *effMsgNoComtitle = nullptr;
if ( nullptr != msgNoComtitle
&& '\0' != msgNoComtitle[0])
{
effMsgNoComtitle = ('\0' != mogMsg[0]) ? mogMsg : msgNoComtitle;
}
for (auto &kv : ch->users)
{
comuser &user = kv.second;
if (bJoinLeaveMsg && user.bGagJoinLeave)
{
continue;
}
if (user.bConnected && user.bUserIsOn
&& test_receive_access(user.who, ch))
{
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 20:31:58 -06:00
// Whether a comtitle is shown is the LISTENER's setting, not the
// speaker's. On a spoof channel there is no second variant.
//
const UTF8 *pMsg = ( user.ComTitleStatus
|| nullptr == effMsgNoComtitle)
? effMsg : effMsgNoComtitle;
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
// Channel-wide FORMAT wins outright when OVERRIDE is set.
//
if ('\0' != mogFmt[0] && bMogOverride)
{
m_pINotify->RawNotify(user.who, mogFmt);
continue;
}
// Per-player CHATFORMAT, unless OVERRIDE suppresses it.
//
if (!bMogOverride && nullptr != m_pIEvaluator)
{
UTF8 chatfmt[MOD_LBUF_SIZE];
size_t nFmt = 0;
if (MUX_SUCCEEDED(m_pIAttributeAccess->GetAttribute(
user.who, user.who, T("CHATFORMAT"),
chatfmt, sizeof(chatfmt) - 1, &nFmt))
&& 0 < nFmt)
{
chatfmt[nFmt] = '\0';
if ('\0' != chatfmt[0])
{
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 20:31:58 -06:00
const UTF8 *cfa[3] = { ch->name, pMsg, sdrBuf };
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
UTF8 fmtbuf[MOD_LBUF_SIZE];
size_t nOut = 0;
if (MUX_SUCCEEDED(m_pIEvaluator->EvalWithArgs(
user.who, user.who, executor, chatfmt,
cfa, 3, fmtbuf, sizeof(fmtbuf) - 1, &nOut)))
{
fmtbuf[nOut] = '\0';
m_pINotify->RawNotify(user.who, fmtbuf);
continue;
}
}
}
}
if ('\0' != mogFmt[0])
{
m_pINotify->RawNotify(user.who, mogFmt);
}
else
{
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 20:31:58 -06:00
m_pINotify->RawNotify(user.who, pMsg);
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
}
}
}
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
// Engine only evaluates MOGRIFY`* for non-join/leave traffic
// (comsys.cpp:1701). Pass that through so join/leave history is not
// suppressed by a sticky NOBUFFER=1 under the module alone.
//
RecordChannelHistory(executor, ch, msg, bJoinLeaveMsg);
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
}
// ---------------------------------------------------------------------------
// Append a message to the channel's recall buffer (#1564).
//
// The engine's SendChannelMessage writes HISTORY_n at comsys.cpp:1851. The
// module never did, so do_crecall above read HISTORY_n attributes that
// nothing wrote: recall came back empty on a channel with obvious traffic,
// with cmsgs and cbuffer both reporting healthy values because those come
// from elsewhere. Silent, and invisible to any test that did not read the
// history back.
//
// The index must be num_messages % logmax computed AFTER the increment in
// the caller, which is exactly what do_crecall's read expects.
//
// Join and leave messages are logged too. The engine guards its logging
// 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 both the engine -- which writes
// with atr_add(..., GOD, ...) -- and this module's existing use of a GOD
// executor for 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 (#1570): when the attribute is present and non-empty on the
// channel object, wrap the line as the engine does -- "[%s] %s" with
// CLinearTimeAbsolute::ReturnDateString(0). libmux exports that class, so
// the module can match the engine's format rather than inventing a second
// one with strftime. @cset/timestamp_logs sets or clears the attribute;
// without this consult it reported success while history stayed plain.
//
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
// MOGRIFY`NOBUFFER (#1572).
//
// The engine evaluates the hook with three arguments -- channel name, message,
// and the sender as "#<dbref>" -- and skips the recall buffer when the result
// is true by xlate() (comsys.cpp:1727). The module can do the same: it already
// holds mux_IEvaluator, whose EvalWithArgs exists for exactly this (the
// interface comment cites MOGRIFY`BLOCK, #1194).
//
// Evaluated via EvalWithArgs (same path as blocked_by_mogrify), with xlate-
// style truth on the result so "0" and empty stay off. A hook added now
// should match the engine rather than invent a second meaning.
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
//
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
// Evaluate MOGRIFY`<suffix> on the channel object (#1572).
//
// The engine's call_mogrifier (comsys.cpp:1307) reads the attribute and runs
// it with mux_exec as the channel object, with the speaker as enactor. This
// is that, through the module's evaluator interface. A GOD executor is used
// for the attribute READ so AF_DARK channel-object attributes are visible --
// the speaker is an ordinary member and usually cannot see them.
//
bool CComsysMod::mogrify_eval(struct channel *ch, dbref executor,
const UTF8 *suffix, const UTF8 *args[], int nargs,
UTF8 *out, size_t outsz)
{
if (nullptr != out && 0 < outsz)
{
out[0] = '\0';
}
if ( nullptr == ch
|| ch->chan_obj < 0
|| nullptr == out
|| nullptr == m_pIAttributeAccess
|| nullptr == m_pIEvaluator)
{
return false;
}
bool bValid = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
}
if (!bValid)
{
return false;
}
UTF8 aname[64];
mux_sprintf(aname, sizeof(aname), T("MOGRIFY`%s"),
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
reinterpret_cast<const char *>(suffix));
UTF8 expr[MOD_LBUF_SIZE];
size_t nExpr = 0;
if (MUX_FAILED(m_pIAttributeAccess->GetAttribute(1, ch->chan_obj, aname,
expr, sizeof(expr) - 1, &nExpr))
|| 0 == nExpr)
{
return false;
}
expr[nExpr] = '\0';
if ('\0' == expr[0])
{
return false;
}
size_t nResult = 0;
if (MUX_FAILED(m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj,
executor, expr, args, nargs, out, outsz - 1, &nResult)))
{
out[0] = '\0';
return false;
}
out[nResult] = '\0';
return true;
}
// Approximate xlate() (functions.cpp) for the common 0/1/empty results
// without pulling engine-private xlate into the module. Leading "#-" is
// false; bare "#N" is true; "0" is false; other non-empty is true.
//
bool CComsysMod::mogrify_truthy(const UTF8 *result)
{
const char *p = reinterpret_cast<const char *>(result);
while (' ' == *p) p++;
if ('\0' == *p) return false;
if ('#' == *p) return ('-' != p[1]);
if ('0' == p[0] && '\0' == p[1]) return false;
return true;
}
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
bool CComsysMod::nobuffer_by_mogrify(dbref executor, struct channel *ch,
const UTF8 *msg)
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
{
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
if ( nullptr == ch
|| ch->chan_obj < 0
|| nullptr == m_pIAttributeAccess
|| nullptr == m_pIEvaluator
|| nullptr == msg)
{
return false;
}
bool bValid = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
}
if (!bValid)
{
return false;
}
UTF8 expr[MOD_LBUF_SIZE];
size_t nExpr = 0;
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
T("MOGRIFY`NOBUFFER"), expr, sizeof(expr) - 1, &nExpr);
if (MUX_FAILED(mr) || 0 == nExpr)
{
return false;
}
expr[nExpr] = '\0';
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"),
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
static_cast<int>(executor));
const UTF8 *args[3] = { ch->name, msg, sdrBuf };
UTF8 result[MOD_LBUF_SIZE];
size_t nResult = 0;
// Match call_mogrifier / blocked_by_mogrify: evaluate as the channel
// object with the speaker as enactor (comsys.cpp:1342).
//
mr = m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj, executor,
expr, args, 3, result, sizeof(result) - 1, &nResult);
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
if (MUX_FAILED(mr) || 0 == nResult)
{
return false;
}
result[nResult] = '\0';
// Approximate xlate() (functions.cpp) for the common 0/1/empty results
// without pulling engine-private xlate into the module. Leading "#-" is
// false; bare "#N" is true; "0" is false; other non-empty is true.
//
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
const char *p = reinterpret_cast<const char *>(result);
while (' ' == *p) p++;
if ('\0' == *p) return false;
if ('#' == *p) return ('-' != p[1]);
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
if ('0' == p[0] && '\0' == p[1]) return false;
return true;
}
void CComsysMod::RecordChannelHistory(dbref executor, struct channel *ch,
const UTF8 *msg, bool bJoinLeaveMsg)
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
{
// MOGRIFY`NOBUFFER keeps a line out of the recall buffer (#1572). The
// engine honours it; the module did not, so a channel that suppressed
// history under the engine still recorded it under the module.
// Join/leave is not evaluated by the engine either (comsys.cpp:1701).
//
if ( !bJoinLeaveMsg
&& nobuffer_by_mogrify(executor, ch, msg))
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
{
return;
}
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
if ( nullptr == m_pIAttributeAccess
|| nullptr == m_pIObjectInfo
|| nullptr == ch
|| nullptr == msg)
{
return;
}
bool bValid = false;
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
if (!bValid)
{
return;
}
UTF8 valbuf[64];
size_t nLen = 0;
int64_t logmax = 0;
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
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
T("MAX_LOG"), valbuf, sizeof(valbuf) - 1, &nLen);
if (MUX_SUCCEEDED(mr) && 0 < nLen)
{
valbuf[nLen] = '\0';
logmax = mux_atoi64(valbuf);
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
}
if (logmax < 1)
{
// Channel does not log; do_crecall reports the same condition.
//
return;
}
UTF8 histattr[64];
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 20:31:58 -06:00
mux_sprintf(histattr, sizeof(histattr),
T("HISTORY_%d"), ((ch->num_messages % logmax) + logmax) % logmax);
const UTF8 *payload = msg;
UTF8 stamped[MOD_LBUF_SIZE];
UTF8 tsattr[64];
size_t nTs = 0;
mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
T("LOG_TIMESTAMPS"), tsattr, sizeof(tsattr) - 1, &nTs);
// Engine uses atr_get_info (presence); @cset clears by writing "".
// Treat non-empty as on, empty/missing as off.
//
if (MUX_SUCCEEDED(mr) && 0 < nTs)
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
// Same shape as comsys.cpp:1857-1865.
//
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 20:31:58 -06:00
mux_sprintf(stamped, sizeof(stamped),
T("[%s] %s"),
reinterpret_cast<const char *>(ltaNow.ReturnDateString(0)),
reinterpret_cast<const char *>(msg));
payload = stamped;
}
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
// Check the write (#1620). SetAttribute is permission-checked, and the
// engine writes these same attributes as GOD with AF_CONST -- which
// bCanSetAttr denies in every branch, God included. So on a game that
// ever ran the built-in comsys, a HISTORY_n slot the engine wrote is
// refused here forever once the ring wraps onto it. num_messages is a
// relational column and keeps counting, so the counter and the history
// silently diverge: exactly #1564's symptom, from a different cause.
//
// This does not make the write land. It makes a lost message say so
// instead of vanishing, which is the difference between #1564 taking an
// investigation and taking a log grep.
//
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 15:26:08 -06:00
// Through the engine (#1620). The permission-checked route is refused
// by the AF_CONST the engine puts on these, so a slot the engine wrote
// first was frozen against the module forever once the ring wrapped
// onto it -- and num_messages kept counting, which is #1564's symptom
// arriving by a second route.
//
MUX_RESULT mrHist = (nullptr != m_pIStorage)
? m_pIStorage->SetChannelAttr(ch->name, histattr, payload)
: MUX_E_FAIL;
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
if (MUX_FAILED(mrHist) && nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("HIST"));
if (fStarted)
{
UTF8 logbuf[256];
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 20:31:58 -06:00
mux_sprintf(logbuf, sizeof(logbuf),
T("Comsys module: channel history write refused (%s on #%d, result %d); message not recorded."),
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
reinterpret_cast<const char *>(histattr),
static_cast<int>(ch->chan_obj), mrHist);
m_pILog->log_text(logbuf);
m_pILog->end_log();
}
}
}
// ---------------------------------------------------------------------------
// Join/leave channel.
// ---------------------------------------------------------------------------
void CComsysMod::do_joinchannel(dbref player, struct channel *ch)
{
struct comuser *user = select_user(ch, player);
if (nullptr == user)
{
// #1195: enforce membership cap like the engine.
//
if (static_cast<int>(ch->users.size()) >= MAX_USERS_PER_CHANNEL)
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Too many people on channel %s already."),
reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(player, msg);
}
return;
}
comuser cu;
cu.who = player;
cu.bUserIsOn = true;
cu.ComTitleStatus = true;
cu.bGagJoinLeave = false;
// #1195: UNDEAD connection, not "always true".
//
cu.bConnected = undead_connected(player);
auto result = ch->users.emplace(player, std::move(cu));
user = &result.first->second;
sqlite_wt_channel_user(ch->name, *user);
}
else if (!user->bUserIsOn)
{
user->bUserIsOn = true;
sqlite_wt_channel_user(ch->name, *user);
}
else
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("You are already on channel %s."),
reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(player, msg);
}
return;
}
// Engine suppresses join announce for Hidden players.
//
if (is_hidden(player))
{
return;
}
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 20:31:58 -06:00
UTF8 pfxNormal[MOD_LBUF_SIZE];
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
pfxNoComtitle, sizeof(pfxNoComtitle));
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
mux_sprintf(msg, sizeof(msg),
M_("%s has joined this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNormal));
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle),
M_("%s has joined this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNoComtitle));
SendChannelMessage(player, ch, msg,
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
}
void CComsysMod::do_leavechannel(dbref player, struct channel *ch)
{
struct comuser *user = select_user(ch, player);
if (nullptr == user)
{
return;
}
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("You have left channel %s."),
reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(player, msg);
}
if (user->bUserIsOn)
{
// Engine suppresses leave announce for Hidden players.
//
if (!is_hidden(player))
{
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 20:31:58 -06:00
UTF8 pfxNormal[MOD_LBUF_SIZE];
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
pfxNoComtitle, sizeof(pfxNoComtitle));
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
mux_sprintf(msg, sizeof(msg),
M_("%s has left this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNormal));
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."),
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 20:31:58 -06:00
reinterpret_cast<const char *>(pfxNoComtitle));
SendChannelMessage(player, ch, msg,
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
}
user->bUserIsOn = false;
sqlite_wt_channel_user(ch->name, *user);
}
}
// ---------------------------------------------------------------------------
// Channel who list.
// ---------------------------------------------------------------------------
void CComsysMod::do_comwho(dbref player, struct channel *ch)
{
if (nullptr == m_pINotify || nullptr == m_pIObjectInfo)
{
return;
}
// #1195: mirror engine do_comwho — players vs objects; filter Hidden
// unless Wizard_Who / See_Hidden.
//
bool bSeeHidden = false;
bool bWizardWho = false;
m_pIObjectInfo->SeeHidden(player, &bSeeHidden);
m_pIObjectInfo->WizardWho(player, &bWizardWho);
const bool bMaySeeHidden = bSeeHidden || bWizardWho;
m_pINotify->RawNotify(player, M_("-- Players --"));
for (auto &kv : ch->users)
{
comuser &user = kv.second;
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
if (!bPlayer)
{
continue;
}
bool bConnected = false;
m_pIObjectInfo->IsConnected(user.who, &bConnected);
const bool bHidden = is_hidden(user.who);
if (bConnected && (!bHidden || bMaySeeHidden))
{
if (user.bUserIsOn)
{
const UTF8 *pName = nullptr;
m_pIObjectInfo->GetMoniker(user.who, &pName);
if (nullptr != pName)
{
m_pINotify->RawNotify(player, pName);
}
}
}
else if (!bHidden)
{
// Stale presence — disconnect from this channel's view.
//
do_comdisconnectchannel(user.who, ch->name);
}
}
m_pINotify->RawNotify(player, M_("-- Objects --"));
for (auto &kv : ch->users)
{
const comuser &user = kv.second;
bool bPlayer = false;
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
if (bPlayer)
{
continue;
}
bool bGoing = false;
m_pIObjectInfo->IsGoing(user.who, &bGoing);
if (bGoing)
{
continue;
}
if (user.bUserIsOn)
{
const UTF8 *pName = nullptr;
m_pIObjectInfo->GetMoniker(user.who, &pName);
if (nullptr != pName)
{
m_pINotify->RawNotify(player, pName);
}
}
}
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(player, msg);
}
// ---------------------------------------------------------------------------
// Recall channel message history from channel object attributes.
// ---------------------------------------------------------------------------
void CComsysMod::do_comlast(dbref player, struct channel *ch, int arg)
{
if (nullptr == m_pINotify || nullptr == m_pIAttributeAccess)
{
return;
}
// Validate the channel object.
//
bool bValid = false;
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
}
if (!bValid)
{
m_pINotify->RawNotify(player,
M_("Channel does not have an object."));
return;
}
const dbref obj = ch->chan_obj;
// Read MAX_LOG attribute to determine logging depth.
//
UTF8 valbuf[64];
size_t nLen = 0;
int64_t logmax = 0;
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, obj,
T("MAX_LOG"), valbuf, sizeof(valbuf) - 1, &nLen);
if (MUX_SUCCEEDED(mr) && 0 < nLen)
{
valbuf[nLen] = '\0';
logmax = mux_atoi64(valbuf);
}
if (logmax < 1)
{
m_pINotify->RawNotify(player, M_("Channel does not log."));
return;
}
if (arg < 1)
{
arg = 1;
}
if (arg > logmax)
{
arg = logmax;
}
int histnum = ch->num_messages - arg;
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s -- Begin Comsys Recall --"),
reinterpret_cast<const char *>(ch->header));
m_pINotify->RawNotify(player, msg);
for (int count = 0; count < arg; count++)
{
histnum++;
UTF8 attrname[64];
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 20:31:58 -06:00
mux_sprintf(attrname, sizeof(attrname),
T("HISTORY_%d"), ((histnum % logmax) + logmax) % logmax);
UTF8 message[MOD_LBUF_SIZE];
size_t msgLen = 0;
mr = m_pIAttributeAccess->GetAttribute(player, obj,
attrname, message, sizeof(message) - 1, &msgLen);
if (MUX_SUCCEEDED(mr) && 0 < msgLen)
{
message[msgLen] = '\0';
m_pINotify->RawNotify(player, message);
}
}
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s -- End Comsys Recall --"),
reinterpret_cast<const char *>(ch->header));
m_pINotify->RawNotify(player, msg);
}
// ---------------------------------------------------------------------------
// Process a channel message (alias dispatch target).
// ---------------------------------------------------------------------------
void CComsysMod::do_processcom(dbref player, const UTF8 *arg1, UTF8 *arg2)
{
if (nullptr == arg2 || '\0' == *arg2)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player, M_("No message."));
}
return;
}
// Truncate excessively long messages.
//
if (3500 < strlen(reinterpret_cast<const char *>(arg2)))
{
arg2[3500] = '\0';
}
struct channel *ch = select_channel(arg1);
if (nullptr == ch)
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Unknown channel %s."),
reinterpret_cast<const char *>(arg1));
m_pINotify->RawNotify(player, msg);
}
return;
}
struct comuser *user = select_user(ch, player);
if (nullptr == user)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player,
M_("You are not listed as on that channel. "
"Delete this alias and readd."));
}
return;
}
// Handle sub-commands.
//
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "on"))
{
do_joinchannel(player, ch);
return;
}
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "off"))
{
do_leavechannel(player, ch);
return;
}
if (!user->bUserIsOn)
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("You must be on %s to do that."),
reinterpret_cast<const char *>(arg1));
m_pINotify->RawNotify(player, msg);
}
return;
}
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "who"))
{
do_comwho(player, ch);
return;
}
// Handle "last [N]" — channel history recall.
//
if (0 == strncmp(reinterpret_cast<const char *>(arg2), "last", 4)
&& ('\0' == arg2[4]
|| (' ' == arg2[4]
&& '\0' != arg2[5])))
{
int64_t nRecall = 10; // DFLT_RECALL_REQUEST
if (' ' == arg2[4])
{
nRecall = mux_atoi64(arg2 + 5);
}
int nRecallI = (nRecall > INT_MAX) ? INT_MAX
: (nRecall < 0) ? 0 : static_cast<int>(nRecall);
do_comlast(player, ch, nRecallI);
return;
}
// #1194: Gagged (non-wizard) may not speak on channels.
//
if (is_gagged(player) && !is_wizard(player))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player,
M_("GAGGED players may not speak on channels."));
}
return;
}
// Check transmit access.
//
if (!test_transmit_access(player, ch))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player,
M_("That channel type cannot be transmitted on."));
}
return;
}
// #1194: channel charge (payfor + giveto to charge_who).
//
if (!pay_channel_charge(player, ch))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(player,
M_("You dont have enough coins."));
}
return;
}
// #1194: MOGRIFY`BLOCK on the channel object.
//
if (blocked_by_mogrify(player, ch, arg2))
{
return;
}
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 20:31:58 -06:00
// Build and send the message. The speaker prefix carries the comtitle
// (#1640); everything after it is unchanged.
//
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 20:31:58 -06:00
UTF8 pfxNormal[MOD_LBUF_SIZE];
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
pfxNoComtitle, sizeof(pfxNoComtitle));
const bool bHaveNoComtitle = ('\0' != pfxNoComtitle[0]);
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
msgNoComtitle[0] = '\0';
const char *pPose = reinterpret_cast<const char *>(arg2);
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 20:31:58 -06:00
// One format string per shape, applied to both prefixes, so the two
// variants cannot drift apart the way the engine's and the module's did.
//
const UTF8 *pFmt;
if (':' == pPose[0])
{
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 20:31:58 -06:00
// Pose: "<prefix> <action>", with the leading space already supplied
// when the pose starts with one.
//
pPose++;
if (' ' == *pPose)
{
pPose++;
pFmt = T("%s%s");
}
else
{
pFmt = T("%s %s");
}
}
else if (';' == pPose[0])
{
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 20:31:58 -06:00
// Semipose: "<prefix><text>"
//
pPose++;
pFmt = T("%s%s");
}
else
{
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 20:31:58 -06:00
// Say: "<prefix> says, “<text>”"
//
pFmt = M_("%s says, “%s”");
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 20:31:58 -06:00
}
mux_sprintf(msg, sizeof(msg), pFmt,
reinterpret_cast<const char *>(pfxNormal), pPose);
if (bHaveNoComtitle)
{
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), pFmt,
reinterpret_cast<const char *>(pfxNoComtitle), pPose);
}
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 20:31:58 -06:00
SendChannelMessage(player, ch, msg,
bHaveNoComtitle ? msgNoComtitle : nullptr, false);
}
// ---------------------------------------------------------------------------
// mux_IComsysControl implementation.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::Initialize(mux_IComsysStorage *pStorage)
{
if (nullptr == pStorage)
{
return MUX_E_INVALIDARG;
}
m_pIStorage = pStorage;
m_pIStorage->AddRef();
// Load all channel data from SQLite.
//
if (!LoadChannels())
{
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
if (fStarted)
{
m_pILog->log_text(T("Comsys module: LoadChannels failed."));
m_pILog->end_log();
}
}
}
if (!LoadChannelUsers())
{
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
if (fStarted)
{
m_pILog->log_text(T("Comsys module: LoadChannelUsers failed."));
m_pILog->end_log();
}
}
}
if (!LoadPlayerChannels())
{
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
if (fStarted)
{
m_pILog->log_text(T("Comsys module: LoadPlayerChannels failed."));
m_pILog->end_log();
}
}
}
if (nullptr != m_pILog)
{
bool fStarted;
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
if (fStarted)
{
m_pILog->log_text(T("Comsys module: loaded "));
m_pILog->log_number(static_cast<int>(m_channels.size()));
m_pILog->log_text(T(" channels from SQLite."));
m_pILog->end_log();
}
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::PlayerConnect(dbref player)
{
auto it = m_comsys.find(player);
if (it == m_comsys.end())
{
return MUX_S_OK;
}
comsys_t &c = it->second;
// Track which channels we have already connected to (avoid duplicates).
//
std::vector<std::string> seen;
for (const auto &ca : c.aliases)
{
bool bFound = false;
for (const auto &s : seen)
{
if (s == ca.channel)
{
bFound = true;
break;
}
}
if (!bFound)
{
seen.push_back(ca.channel);
do_comconnectchannel(player,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
ca.alias);
do_comconnectraw_notify(player,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
}
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::PlayerDisconnect(dbref player)
{
auto it = m_comsys.find(player);
if (it == m_comsys.end())
{
return MUX_S_OK;
}
comsys_t &c = it->second;
std::vector<std::string> seen;
for (const auto &ca : c.aliases)
{
bool bFound = false;
for (const auto &s : seen)
{
if (s == ca.channel)
{
bFound = true;
break;
}
}
if (!bFound)
{
seen.push_back(ca.channel);
do_comdisconnectchannel(player,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
do_comdisconnectraw_notify(player,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
}
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::PlayerNuke(dbref player)
{
if (player < 0)
{
return MUX_E_INVALIDARG;
}
// Mirror engine ReleaseAllResources comsys side (#1193):
// disconnect presence, clear aliases/membership, destroy owned
// channels, drop per-player comsys row, and purge SQLite player_channels.
//
// 1. Quiet disconnect of channel presence.
//
PlayerDisconnect(player);
// 2. Remove all aliases and channel membership for this player.
//
auto itCom = m_comsys.find(player);
if (itCom != m_comsys.end())
{
comsys_t &c = itCom->second;
std::vector<com_alias> aliases = c.aliases;
for (const auto &ca : aliases)
{
do_delcomchannel(player,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()), true);
sqlite_wt_delete_player_channel(player,
reinterpret_cast<const UTF8 *>(ca.alias.c_str()));
}
c.aliases.clear();
m_comsys.erase(itCom);
}
// 3. Strip residual membership (joined without a surviving alias).
//
for (auto &kv : m_channels)
{
struct channel *ch = kv.second.get();
if (nullptr == ch)
{
continue;
}
if (0 != ch->users.erase(player))
{
sqlite_wt_delete_channel_user(ch->name, player);
}
}
// 4. Destroy channels owned by this player. SQLite ON DELETE CASCADE
// clears channel_users / player_channels for those channel names; also
// drop other players' in-memory aliases that pointed at them.
//
std::vector<std::string> destroyedNames;
for (auto it = m_channels.begin(); it != m_channels.end(); )
{
if (player == it->second->charge_who)
{
destroyedNames.emplace_back(
reinterpret_cast<const char *>(it->second->name));
if (nullptr != m_pIStorage)
{
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
log_storage_failure(
m_pIStorage->DeleteChannel(it->second->name),
"DeleteChannel(channel=%s) [owner #%d destroyed]",
reinterpret_cast<const char *>(it->second->name),
static_cast<int>(player));
bump_revision();
}
it = m_channels.erase(it);
}
else
{
++it;
}
}
if (!destroyedNames.empty())
{
for (auto &kv : m_comsys)
{
comsys_t &c = kv.second;
for (auto ait = c.aliases.begin(); ait != c.aliases.end(); )
{
bool bHit = false;
for (const auto &nm : destroyedNames)
{
if (ait->channel == nm)
{
bHit = true;
break;
}
}
if (bHit)
{
sqlite_wt_delete_player_channel(c.who,
reinterpret_cast<const UTF8 *>(ait->alias.c_str()));
ait = c.aliases.erase(ait);
}
else
{
++ait;
}
}
}
}
// 5. Belt-and-suspenders: remaining player_channels rows for this who.
//
if (nullptr != m_pIStorage)
{
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
log_storage_failure(m_pIStorage->DeleteAllPlayerChannels(player),
"DeleteAllPlayerChannels(who=#%d)", static_cast<int>(player));
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// allcom — send command to all channels.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::AllCom(dbref executor, const UTF8 *pAction)
{
if (nullptr == pAction)
{
return MUX_E_INVALIDARG;
}
const char *a = reinterpret_cast<const char *>(pAction);
if (0 != strcmp(a, "who") && 0 != strcmp(a, "on") && 0 != strcmp(a, "off"))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Only options available are: on, off and who."));
}
return MUX_E_INVALIDARG;
}
auto it = m_comsys.find(executor);
if (it == m_comsys.end())
{
return MUX_S_OK;
}
comsys_t &c = it->second;
for (const auto &ca : c.aliases)
{
do_processcom(executor,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
const_cast<UTF8 *>(pAction));
if (0 == strcmp(a, "who") && nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor, T(""));
}
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// comlist — list player's channel aliases.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::ComList(dbref executor, const UTF8 *pPattern)
{
if (nullptr == m_pINotify)
{
return MUX_E_FAIL;
}
// comlist schema (#1667 Phase 4 A3) — same as engine do_comlist.
//
static const size_t kComlistAliasCols = 15;
static const size_t kComlistChannelCols = 18;
{
UTF8 header[256];
size_t pos = 0;
pos = append_ljust_field(header, sizeof(header), pos,
M_("Alias"), kComlistAliasCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Channel"), kComlistChannelCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Status")));
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Title")));
m_pINotify->RawNotify(executor, header);
}
auto it = m_comsys.find(executor);
if (it == m_comsys.end())
{
m_pINotify->RawNotify(executor, M_("-- End of comlist --"));
return MUX_S_OK;
}
comsys_t &c = it->second;
bool bWild = (nullptr != pPattern && '\0' != *pPattern);
for (const auto &ca : c.aliases)
{
struct channel *ch = select_channel(
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
struct comuser *user = (nullptr != ch)
? select_user(ch, executor) : nullptr;
if (nullptr != user)
{
if (bWild && std::string::npos == ca.channel.find(
reinterpret_cast<const char *>(pPattern)))
{
continue;
}
UTF8 msg[256];
size_t pos = 0;
pos = append_ljust_field(msg, sizeof(msg), pos,
reinterpret_cast<const UTF8 *>(ca.alias.c_str()),
kComlistAliasCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
pos = append_ljust_field(msg, sizeof(msg), pos,
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
kComlistChannelCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
if (pos < sizeof(msg))
{
mux_sprintf(msg + pos, sizeof(msg) - pos,
T("%s %s%s %s"),
user->bUserIsOn ? T("on ") : T("off"),
user->ComTitleStatus ? T("con ") : T("coff"),
user->bGagJoinLeave ? T(" gag") : T(""),
reinterpret_cast<const UTF8 *>(user->title.c_str()));
}
m_pINotify->RawNotify(executor, msg);
}
else
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Bad Comsys Alias: %s for Channel: %s"),
ca.alias.c_str(),
ca.channel.c_str());
m_pINotify->RawNotify(executor, msg);
}
}
m_pINotify->RawNotify(executor, M_("-- End of comlist --"));
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// comtitle — set/get comtitle for a channel alias.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::ComTitle(dbref executor, const UTF8 *pAlias,
const UTF8 *pTitle, int key)
{
if (nullptr == pAlias || '\0' == *pAlias)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Need an alias to do comtitle."));
}
return MUX_E_INVALIDARG;
}
const UTF8 *chName = get_channel_from_alias(executor, pAlias);
if ('\0' == chName[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor, M_("Unknown alias."));
}
return MUX_E_NOTFOUND;
}
struct channel *ch = select_channel(chName);
if (nullptr == ch)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Illegal comsys alias, please delete."));
}
return MUX_E_NOTFOUND;
}
struct comuser *user = select_user(ch, executor);
if (nullptr == user)
{
return MUX_E_NOTFOUND;
}
UTF8 msg[256];
switch (key)
{
case COMTITLE_OFF:
if (0 == (ch->type & CHANNEL_SPOOF))
{
user->ComTitleStatus = false;
sqlite_wt_channel_user(ch->name, *user);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Comtitles are now off for channel %s"),
reinterpret_cast<const char *>(ch->name));
}
else
{
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("You can not turn off comtitles on that channel."));
}
break;
case COMTITLE_ON:
user->ComTitleStatus = true;
sqlite_wt_channel_user(ch->name, *user);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Comtitles are now on for channel %s"),
reinterpret_cast<const char *>(ch->name));
break;
case COMTITLE_GAG:
user->bGagJoinLeave = true;
sqlite_wt_channel_user(ch->name, *user);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Join/leave messages are now gagged for channel %s"),
reinterpret_cast<const char *>(ch->name));
break;
case COMTITLE_UNGAG:
user->bGagJoinLeave = false;
sqlite_wt_channel_user(ch->name, *user);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Join/leave messages are now ungagged for channel %s"),
reinterpret_cast<const char *>(ch->name));
break;
default:
// Set title.
//
if (nullptr != pTitle && '\0' != *pTitle)
{
user->title.assign(reinterpret_cast<const char *>(pTitle),
0, MAX_TITLE_LEN);
}
else
{
user->title.clear();
}
sqlite_wt_channel_user(ch->name, *user);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Title set to %s on channel %s."),
user->title.c_str(),
reinterpret_cast<const char *>(ch->name));
break;
}
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor, msg);
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// @clist — list channels.
// ---------------------------------------------------------------------------
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 20:31:58 -06:00
// ---------------------------------------------------------------------------
// @clist/full — same schema as engine do_listchannels (#1667 Phase 4 A1).
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 20:31:58 -06:00
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::ChanListFull(dbref executor, const UTF8 *pPattern)
{
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 20:31:58 -06:00
bool bCommAll = false;
if (nullptr != m_pIPermissions)
{
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 20:31:58 -06:00
m_pIPermissions->HasCommAll(executor, &bCommAll);
}
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 20:31:58 -06:00
if (!bCommAll)
{
m_pINotify->RawNotify(executor,
M_("Warning: Only public channels and your channels will be shown."));
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 20:31:58 -06:00
}
static const size_t kFullNameCols = 13;
static const size_t kFullHeaderCols = 15;
static const size_t kFullOwnerCols = 15;
static const size_t kFullAccessCols = 6;
{
UTF8 header[256];
size_t pos = 0;
pos = append_bytes(header, sizeof(header), pos, "*** ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Channel"), kFullNameCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Header"), kFullHeaderCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Owner"), kFullOwnerCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Access"), kFullAccessCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Users")));
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Msgs")));
m_pINotify->RawNotify(executor, header);
}
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 20:31:58 -06:00
const bool bWild = (nullptr != pPattern && '\0' != *pPattern);
for (auto &kv : m_channels)
{
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 20:31:58 -06:00
struct channel *ch = kv.second.get();
if (bWild && nullptr == strstr(
reinterpret_cast<const char *>(ch->name),
reinterpret_cast<const char *>(pPattern)))
{
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 20:31:58 -06:00
continue;
}
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 20:31:58 -06:00
if (!bCommAll
&& 0 == (ch->type & CHANNEL_PUBLIC))
{
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 20:31:58 -06:00
bool bControls = false;
if (nullptr != m_pIPermissions)
{
m_pIPermissions->HasControl(executor, ch->charge_who,
&bControls);
}
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 20:31:58 -06:00
if ( !bControls
&& nullptr == select_user(ch, executor))
{
continue;
}
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 20:31:58 -06:00
}
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 20:31:58 -06:00
const UTF8 *pOwnerName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetMoniker(ch->charge_who, &pOwnerName);
}
if (nullptr == pOwnerName)
{
pOwnerName = T("???");
}
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 20:31:58 -06:00
// The engine shows "-" for an unset header here, unlike the default
// listing which shows it empty.
//
const UTF8 *pHeader = ch->header;
if ('\0' == pHeader[0])
{
pHeader = T("-");
}
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 20:31:58 -06:00
UTF8 line[MOD_LBUF_SIZE];
size_t pos = 0;
line[pos++] = (ch->type & CHANNEL_PUBLIC) ? 'P' : '-';
line[pos++] = (ch->type & CHANNEL_LOUD) ? 'L' : '-';
line[pos++] = (ch->type & CHANNEL_SPOOF) ? 'S' : '-';
line[pos++] = ' ';
line[pos] = '\0';
pos = append_ljust_field(line, sizeof(line), pos, ch->name,
kFullNameCols);
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 20:31:58 -06:00
pos = append_bytes(line, sizeof(line), pos, " ");
pos = append_ljust_field(line, sizeof(line), pos, pHeader,
kFullHeaderCols);
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 20:31:58 -06:00
pos = append_bytes(line, sizeof(line), pos, " ");
pos = append_ljust_field(line, sizeof(line), pos, pOwnerName,
kFullOwnerCols);
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 20:31:58 -06:00
pos = append_bytes(line, sizeof(line), pos, " ");
UTF8 access[4];
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 20:31:58 -06:00
access[0] = test_join_access(executor, ch) ? 'J' : '-';
access[1] = test_transmit_access(executor, ch) ? 'X' : '-';
access[2] = test_receive_access(executor, ch) ? 'R' : '-';
access[3] = '\0';
// Access is 6 cols (JXR + pad); counts follow with no extra separator.
//
pos = append_ljust_field(line, sizeof(line), pos, access,
kFullAccessCols);
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 20:31:58 -06:00
if (pos < sizeof(line))
{
mux_sprintf(line + pos, sizeof(line) - pos,
T("%5d %4d"), static_cast<int>(ch->users.size()),
ch->num_messages);
}
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 20:31:58 -06:00
m_pINotify->RawNotify(executor, line);
}
m_pINotify->RawNotify(executor, M_("-- End of list of Channels --"));
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 20:31:58 -06:00
return MUX_S_OK;
}
MUX_RESULT CComsysMod::ChanList(dbref executor, const UTF8 *pPattern,
int key)
{
if (nullptr == m_pINotify)
{
return MUX_E_FAIL;
}
// @clist/full was silently answered with the DEFAULT listing (#1640):
// Header, Access, Users and Msgs -- the four columns that are the entire
// reason /full exists -- were dropped. /headers was implemented and both
// implementations agreed on it, which is why this looked like a
// formatting difference rather than an unhandled switch.
//
if (key & CLIST_FULL)
{
return ChanListFull(executor, pPattern);
}
// @clist / @clist/headers schema — same as engine do_chanlist
// (#1667 Phase 4 A2): "*** "/PLS prefix, name 13, owner 15, third 45,
// pad to 79. Labels are separate msgids via mux_table_append_ljust.
//
static const size_t kClistNameCols = 13;
static const size_t kClistOwnerCols = 15;
static const size_t kClistThirdCols = 45;
static const size_t kClistLineCols = 79;
{
UTF8 header[256];
size_t pos = 0;
pos = append_bytes(header, sizeof(header), pos, "*** ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Channel"), kClistNameCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
M_("Owner"), kClistOwnerCols);
pos = append_bytes(header, sizeof(header), pos, " ");
pos = append_ljust_field(header, sizeof(header), pos,
(key & CLIST_HEADERS) ? M_("Header") : M_("Description"),
kClistThirdCols);
while (pos < kClistLineCols && pos + 1 < sizeof(header))
{
header[pos++] = ' ';
}
if (pos < sizeof(header))
{
header[pos] = '\0';
}
m_pINotify->RawNotify(executor, header);
}
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 20:31:58 -06:00
bool bWild = (nullptr != pPattern && '\0' != *pPattern);
for (auto &kv : m_channels)
{
struct channel *ch = kv.second.get();
// Simple wildcard filter.
//
if (bWild && nullptr == strstr(
reinterpret_cast<const char *>(ch->name),
reinterpret_cast<const char *>(pPattern)))
{
continue;
}
// Visibility check: public or owner or Comm_All.
//
bool bVisible = ((ch->type & CHANNEL_PUBLIC) != 0);
if (!bVisible && nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
bVisible = bCommAll;
}
if (!bVisible)
{
bool bControls = false;
if (nullptr != m_pIPermissions)
{
m_pIPermissions->HasControl(executor, ch->charge_who,
&bControls);
}
bVisible = bControls;
}
if (!bVisible)
{
continue;
}
const UTF8 *pOwnerName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetMoniker(ch->charge_who, &pOwnerName);
}
if (nullptr == pOwnerName)
{
pOwnerName = T("???");
}
const UTF8 *pDesc = (key & CLIST_HEADERS)
? ch->header
: M_("No description.");
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 20:31:58 -06:00
UTF8 line[MOD_LBUF_SIZE];
size_t pos = 0;
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 20:31:58 -06:00
line[pos++] = (ch->type & CHANNEL_PUBLIC) ? 'P' : '-';
line[pos++] = (ch->type & CHANNEL_LOUD) ? 'L' : '-';
line[pos++] = (ch->type & CHANNEL_SPOOF) ? 'S' : '-';
line[pos++] = ' ';
line[pos] = '\0';
pos = append_ljust_field(line, sizeof(line), pos, ch->name,
kClistNameCols);
pos = append_bytes(line, sizeof(line), pos, " ");
pos = append_ljust_field(line, sizeof(line), pos, pOwnerName,
kClistOwnerCols);
pos = append_bytes(line, sizeof(line), pos, " ");
pos = append_ljust_field(line, sizeof(line), pos, pDesc,
kClistThirdCols);
while (pos < kClistLineCols && pos + 1 < sizeof(line))
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 20:31:58 -06:00
{
line[pos++] = ' ';
}
if (pos < sizeof(line))
{
line[pos] = '\0';
}
m_pINotify->RawNotify(executor, line);
}
m_pINotify->RawNotify(executor,
M_("-- End of list of Channels --"));
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// @cwho — who is on a channel.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::ChanWho(dbref executor, const UTF8 *pArg)
{
if (nullptr == m_pINotify)
{
return MUX_E_FAIL;
}
if (nullptr == pArg || '\0' == *pArg)
{
m_pINotify->RawNotify(executor, M_("You must specify a channel."));
return MUX_E_INVALIDARG;
}
// Parse "channel/all" format.
//
UTF8 chanName[MAX_CHANNEL_LEN + 1];
size_t i = 0;
while ('\0' != pArg[i] && '/' != pArg[i] && i < MAX_CHANNEL_LEN)
{
chanName[i] = pArg[i];
i++;
}
chanName[i] = '\0';
bool bAll = ('/' == pArg[i] && 'a' == pArg[i + 1]);
struct channel *ch = select_channel(chanName);
if (nullptr == ch)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Unknown channel %s."),
reinterpret_cast<const char *>(chanName));
m_pINotify->RawNotify(executor, msg);
return MUX_E_NOTFOUND;
}
// Permission check.
//
bool bAllowed = false;
if (nullptr != m_pIPermissions)
{
m_pIPermissions->HasCommAll(executor, &bAllowed);
if (!bAllowed)
{
m_pIPermissions->HasControl(executor, ch->charge_who, &bAllowed);
}
}
if (!bAllowed)
{
m_pINotify->RawNotify(executor, M_("Permission denied."));
return MUX_E_PERMISSION;
}
// @cwho schema (#1667 Phase 4 A4) — same as engine do_channelwho.
//
static const size_t kCwhoNameCols = 29;
static const size_t kCwhoStatusCols = 6;
static const size_t kCwhoPlayerCols = 6;
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(executor, msg);
{
size_t pos = 0;
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Name"),
kCwhoNameCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Status"),
kCwhoStatusCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Player"),
kCwhoPlayerCols);
m_pINotify->RawNotify(executor, msg);
}
for (auto &kv : ch->users)
{
comuser &user = kv.second;
// If not showing all, only show connected users.
//
if (!bAll)
{
bool bConnected = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsConnected(user.who, &bConnected);
}
if (!bConnected)
{
continue;
}
}
// UnparseObject for name+dbref+flags (#1640). strip_color so
// width arithmetic stays honest on colored monikers.
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 20:31:58 -06:00
//
UTF8 unparsed[MOD_LBUF_SIZE];
unparsed[0] = '\0';
if (nullptr != m_pIObjectInfo)
{
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 20:31:58 -06:00
m_pIObjectInfo->UnparseObject(executor, user.who, false,
unparsed, sizeof(unparsed));
}
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 20:31:58 -06:00
if ('\0' == unparsed[0])
{
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 20:31:58 -06:00
const UTF8 *pName = nullptr;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->GetName(user.who, &pName);
}
mux_sprintf(unparsed, sizeof(unparsed),
T("%s"), reinterpret_cast<const char *>(
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 20:31:58 -06:00
(nullptr != pName) ? pName : T("???")));
}
bool bPlayer = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
}
size_t pos = 0;
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 20:31:58 -06:00
pos = append_ljust_field(msg, sizeof(msg), pos,
strip_color(unparsed), kCwhoNameCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
pos = append_ljust_field(msg, sizeof(msg), pos,
user.bUserIsOn ? T("on ") : T("off"), kCwhoStatusCols);
pos = append_bytes(msg, sizeof(msg), pos, " ");
pos = append_ljust_field(msg, sizeof(msg), pos,
bPlayer ? T("yes") : T("no "), kCwhoPlayerCols);
m_pINotify->RawNotify(executor, msg);
}
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
m_pINotify->RawNotify(executor, msg);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// @cemit — emit to a channel.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::CEmit(dbref executor, const UTF8 *pChannel,
const UTF8 *pText, int key)
{
if (nullptr == pChannel || nullptr == pText)
{
return MUX_E_INVALIDARG;
}
struct channel *ch = select_channel(pChannel);
if (nullptr == ch)
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Channel %s does not exist."),
reinterpret_cast<const char *>(pChannel));
m_pINotify->RawNotify(executor, msg);
}
return MUX_E_NOTFOUND;
}
// Permission check.
//
bool bAllowed = false;
if (nullptr != m_pIPermissions)
{
m_pIPermissions->HasCommAll(executor, &bAllowed);
if (!bAllowed)
{
m_pIPermissions->HasControl(executor, ch->charge_who, &bAllowed);
}
}
if (!bAllowed)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor, M_("Permission denied."));
}
return MUX_E_PERMISSION;
}
UTF8 msg[MOD_LBUF_SIZE];
if (key == CEMIT_NOHEADER)
{
strncpy(reinterpret_cast<char *>(msg),
reinterpret_cast<const char *>(pText), sizeof(msg) - 1);
msg[sizeof(msg) - 1] = '\0';
}
else
{
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
T("%s %s"),
reinterpret_cast<const char *>(ch->header),
reinterpret_cast<const char *>(pText));
}
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 20:31:58 -06:00
// No speaker prefix here: @cemit is the channel talking, not a player,
// so there is no comtitle variant to choose between.
//
SendChannelMessage(executor, ch, msg, nullptr, false);
return MUX_S_OK;
}
MUX_RESULT CComsysMod::CSet(dbref executor, const UTF8 *pChannel,
const UTF8 *pValue, int key)
{
if (CSET_LIST == key)
{
// Redirect to ChanList with CLIST_FULL.
//
return ChanList(executor, nullptr, CLIST_FULL);
}
struct channel *ch = select_channel(pChannel);
if (nullptr == ch)
{
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("@cset: Channel %s does not exist."),
reinterpret_cast<const char *>(pChannel));
m_pINotify->Notify(executor, msg);
return MUX_S_OK;
}
// Permission check: must control channel owner or have Comm_All.
//
bool bControls = false;
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
if (!bControls && !bCommAll)
{
m_pINotify->Notify(executor, M_("Permission denied."));
return MUX_S_OK;
}
const UTF8 *msg = nullptr;
UTF8 msgbuf[MOD_LBUF_SIZE];
switch (key)
{
case CSET_PUBLIC:
ch->type |= CHANNEL_PUBLIC;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s placed on the public listings."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_PRIVATE:
ch->type &= ~CHANNEL_PUBLIC;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s taken off the public listings."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_LOUD:
ch->type |= CHANNEL_LOUD;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s now sends connect/disconnect msgs."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_QUIET:
ch->type &= ~CHANNEL_LOUD;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s connect/disconnect msgs muted."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_SPOOF:
ch->type |= CHANNEL_SPOOF;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s set spoofable."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_NOSPOOF:
ch->type &= ~CHANNEL_SPOOF;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s set unspoofable."),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
case CSET_OBJECT:
{
dbref thing = NOTHING;
if (nullptr != pValue && '\0' != pValue[0])
{
m_pIObjectInfo->MatchThing(executor, pValue, &thing);
}
bool bValid = false;
if (NOTHING != thing)
{
m_pIObjectInfo->IsValid(thing, &bValid);
}
if (NOTHING == thing)
{
ch->chan_obj = NOTHING;
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("Channel %s is now disassociated from any channel object."),
reinterpret_cast<const char *>(ch->name));
}
else if (bValid)
{
ch->chan_obj = thing;
const UTF8 *pName = nullptr;
m_pIObjectInfo->GetName(thing, &pName);
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("Channel %s is now using %s(#%d) as channel object."),
reinterpret_cast<const char *>(ch->name),
pName ? reinterpret_cast<const char *>(pName) : "???",
thing);
}
else
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("%d is not a valid channel object."), thing);
}
msg = msgbuf;
}
break;
case CSET_HEADER:
{
if (nullptr == pValue || '\0' == pValue[0])
{
// Reset to default bold [ChannelName] header.
// COLOR_INTENSE = \xEF\x94\x81, COLOR_RESET = \xEF\x94\x80
//
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 20:31:58 -06:00
mux_sprintf(ch->header, sizeof(ch->header),
T("\xEF\x94\x81[%s]\xEF\x94\x80"),
reinterpret_cast<const char *>(ch->name));
}
else
{
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 20:31:58 -06:00
// Optimize/terminate the ANSI, as do_cheader does. The raw
// strncpy that was here stored the header uncollapsed, so
// `@cset/header c=[ansi(r,RED)][ansi(b,BLU)]` persisted an
// extra reset between the two codes under the module and not
// under the engine -- a divergence in SQLite, not merely on
// screen, so it survived a handoff. It also truncated at
// MAX_HEADER_LEN *bytes*, which can split a codepoint or a
// color code in half (#1640, #1649).
//
UTF8 hdr[MAX_HEADER_LEN + 1];
const mux_field nLen = StripTabsAndTruncate(pValue, hdr,
MAX_HEADER_LEN, MAX_HEADER_LEN);
memcpy(ch->header, hdr, nLen.m_byte + 1);
}
msg = reinterpret_cast<const UTF8 *>("Set.");
}
break;
case CSET_LOG:
{
if (nullptr == pValue || '\0' == pValue[0])
{
msg = reinterpret_cast<const UTF8 *>(
"@cset: Maximum history must be a number less than "
"or equal to 200.");
break;
}
int64_t value = mux_atoi64(pValue);
if (value < 0 || value > 200)
{
msg = reinterpret_cast<const UTF8 *>(
"@cset: Maximum history must be a number less than "
"or equal to 200.");
break;
}
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
// Set when shrinking the ring leaves entries we could not clear
// (#1620); reported with the result rather than swallowed.
//
bool bStale = false;
bool bObjValid = false;
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bObjValid);
}
if (!bObjValid)
{
msg = reinterpret_cast<const UTF8 *>(
"@cset: Channel has no object. Use @cset/object first.");
break;
}
// Read old MAX_LOG value.
//
UTF8 oldbuf[64];
size_t nOldLen = 0;
int64_t oldnum = 0;
MUX_RESULT mr2 = m_pIAttributeAccess->GetAttribute(
executor, ch->chan_obj, T("MAX_LOG"),
oldbuf, sizeof(oldbuf) - 1, &nOldLen);
if (MUX_SUCCEEDED(mr2) && 0 < nOldLen)
{
oldbuf[nOldLen] = '\0';
oldnum = mux_atoi64(oldbuf);
}
// If reducing, clear old HISTORY_N attributes.
//
if (value < oldnum)
{
for (int64_t count = 0; count <= oldnum; count++)
{
UTF8 histattr[64];
mux_sprintf(histattr, sizeof(histattr), T("HISTORY_%lld"),
static_cast<long long>(count));
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 15:26:08 -06:00
if (nullptr == m_pIStorage
|| MUX_FAILED(m_pIStorage->SetChannelAttr(ch->name,
histattr, T(""))))
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
{
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 15:26:08 -06:00
// Engine-written slots used to refuse here (#1620),
// leaving stale history above the new maximum. Going
// through the engine removes that cause; anything
// still failing is a real storage failure and is
// still reported rather than swallowed.
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
//
bStale = true;
}
}
}
// Set MAX_LOG.
//
UTF8 vbuf[32];
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 20:31:58 -06:00
mux_sprintf(vbuf, sizeof(vbuf),
T("%d"), value);
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 15:26:08 -06:00
if (nullptr == m_pIStorage
|| MUX_FAILED(m_pIStorage->SetChannelAttr(ch->name,
T("MAX_LOG"), vbuf)))
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
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Failed. Could not set the maximum for %s (the attribute is not writable here)."),
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
reinterpret_cast<const char *>(pChannel));
}
else if (bStale)
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s maximum history set, but older entries could not be cleared."),
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
reinterpret_cast<const char *>(pChannel));
}
else
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s maximum history set."),
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
reinterpret_cast<const char *>(pChannel));
}
msg = msgbuf;
}
break;
case CSET_LOG_TIME:
{
if (nullptr == pValue || '\0' == pValue[0])
{
msg = reinterpret_cast<const UTF8 *>("@cset: Failed.");
break;
}
int64_t value = mux_atoi64(pValue);
if (value != 0 && value != 1)
{
msg = reinterpret_cast<const UTF8 *>("@cset: Failed.");
break;
}
bool bObjValid = false;
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(ch->chan_obj, &bObjValid);
}
if (!bObjValid)
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Failed. Is logging enabled for %s?"),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
}
// Verify logging is enabled (MAX_LOG exists).
//
UTF8 logbuf[64];
size_t nLogLen = 0;
MUX_RESULT mr2 = m_pIAttributeAccess->GetAttribute(
executor, ch->chan_obj, T("MAX_LOG"),
logbuf, sizeof(logbuf) - 1, &nLogLen);
if (MUX_FAILED(mr2) || 0 == nLogLen)
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Failed. Is logging enabled for %s?"),
reinterpret_cast<const char *>(pChannel));
msg = msgbuf;
break;
}
// Set or clear LOG_TIMESTAMPS.
//
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
// #1585: this write is refused when the engine set the attribute
// (GOD + AF_CONST, which bCanSetAttr denies in every branch).
// Reporting "set." after a refusal told the player the opposite
// of what happened, and is how the divergence stayed hidden.
//
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 15:26:08 -06:00
MUX_RESULT mrTs = (nullptr != m_pIStorage)
? m_pIStorage->SetChannelAttr(ch->name, T("LOG_TIMESTAMPS"),
value ? T("1") : T(""))
: MUX_E_FAIL;
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
if (MUX_FAILED(mrTs))
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Failed. Timestamp logging for %s could not be changed (the attribute is not writable here)."),
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
reinterpret_cast<const char *>(pChannel));
}
else
{
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 20:31:58 -06:00
mux_sprintf(msgbuf, sizeof(msgbuf),
M_("@cset: Channel %s timestamp logging set."),
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
reinterpret_cast<const char *>(pChannel));
}
msg = msgbuf;
}
break;
}
sqlite_wt_channel(ch);
if (nullptr != msg)
{
m_pINotify->Notify(executor, msg);
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::EditChannel(dbref executor, const UTF8 *pChannel,
const UTF8 *pValue, int flag)
{
struct channel *ch = select_channel(pChannel);
if (nullptr == ch)
{
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Unknown channel %s."),
reinterpret_cast<const char *>(pChannel));
m_pINotify->Notify(executor, msg);
return MUX_S_OK;
}
// Permission check.
//
bool bControls = false;
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
if (!bControls && !bCommAll)
{
m_pINotify->Notify(executor, M_("Permission denied."));
return MUX_S_OK;
}
bool add_remove = true;
const UTF8 *s = pValue;
if (nullptr != s && '!' == *s)
{
add_remove = false;
s++;
}
switch (flag)
{
case EDIT_CHANNEL_CCHOWN:
{
dbref who = NOTHING;
if (nullptr != pValue)
{
m_pIObjectInfo->MatchThing(executor, pValue, &who);
}
bool bValid = false;
if (NOTHING != who)
{
m_pIObjectInfo->IsValid(who, &bValid);
}
if (bValid)
{
ch->charge_who = who;
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("Set."));
}
else
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("Invalid player."));
}
}
break;
case EDIT_CHANNEL_CCHARGE:
{
int64_t c_charge = 0;
if (nullptr != pValue)
{
c_charge = mux_atoi64(pValue);
}
if (0 <= c_charge && c_charge <= MAX_COST)
{
ch->charge = static_cast<int>(c_charge);
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("Set."));
}
else
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>(
"That is not a reasonable cost."));
}
}
break;
case EDIT_CHANNEL_CPFLAGS:
{
int access = 0;
if (nullptr != s)
{
if (strcmp(reinterpret_cast<const char *>(s), "join") == 0)
{
access = CHANNEL_PLAYER_JOIN;
}
else if (strcmp(reinterpret_cast<const char *>(s),
"receive") == 0)
{
access = CHANNEL_PLAYER_RECEIVE;
}
else if (strcmp(reinterpret_cast<const char *>(s),
"transmit") == 0)
{
access = CHANNEL_PLAYER_TRANSMIT;
}
}
if (access)
{
if (add_remove)
{
ch->type |= access;
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@cpflags: Set."));
}
else
{
ch->type &= ~access;
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@cpflags: Cleared."));
}
}
else
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@cpflags: Unknown Flag."));
}
}
break;
case EDIT_CHANNEL_COFLAGS:
{
int access = 0;
if (nullptr != s)
{
if (strcmp(reinterpret_cast<const char *>(s), "join") == 0)
{
access = CHANNEL_OBJECT_JOIN;
}
else if (strcmp(reinterpret_cast<const char *>(s),
"receive") == 0)
{
access = CHANNEL_OBJECT_RECEIVE;
}
else if (strcmp(reinterpret_cast<const char *>(s),
"transmit") == 0)
{
access = CHANNEL_OBJECT_TRANSMIT;
}
}
if (access)
{
if (add_remove)
{
ch->type |= access;
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@coflags: Set."));
}
else
{
ch->type &= ~access;
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@coflags: Cleared."));
}
}
else
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@coflags: Unknown Flag."));
}
}
break;
}
sqlite_wt_channel(ch);
return MUX_S_OK;
}
MUX_RESULT CComsysMod::CBoot(dbref executor, const UTF8 *pChannel,
const UTF8 *pVictim, int key)
{
struct channel *ch = select_channel(pChannel);
if (nullptr == ch)
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>("@cboot: Unknown channel."));
return MUX_S_OK;
}
struct comuser *user = select_user(ch, executor);
if (nullptr == user)
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>(
"@cboot: You are not on that channel."));
return MUX_S_OK;
}
// Permission check.
//
bool bControls = false;
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
if (!bControls && !bCommAll)
{
m_pINotify->Notify(executor,
reinterpret_cast<const UTF8 *>(
"@cboot: You cant do that!"));
return MUX_S_OK;
}
// Resolve victim name to dbref.
//
dbref thing = NOTHING;
if (nullptr != pVictim)
{
m_pIObjectInfo->MatchThing(executor, pVictim, &thing);
}
bool bValid = false;
if (NOTHING != thing)
{
m_pIObjectInfo->IsValid(thing, &bValid);
}
if (!bValid)
{
return MUX_S_OK;
}
struct comuser *vu = select_user(ch, thing);
if (nullptr == vu)
{
const UTF8 *pMoniker = nullptr;
m_pIObjectInfo->GetMoniker(thing, &pMoniker);
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("@cboot: %s is not on the channel."),
pMoniker ? reinterpret_cast<const char *>(pMoniker)
: "???");
m_pINotify->Notify(executor, msg);
return MUX_S_OK;
}
// Notify executor and victim.
//
const UTF8 *pExecMoniker = nullptr;
m_pIObjectInfo->GetMoniker(executor, &pExecMoniker);
const UTF8 *pVictMoniker = nullptr;
m_pIObjectInfo->GetMoniker(thing, &pVictMoniker);
UTF8 msg[MOD_LBUF_SIZE];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("You boot %s off channel %s."),
pVictMoniker ? reinterpret_cast<const char *>(pVictMoniker)
: "???",
reinterpret_cast<const char *>(ch->name));
m_pINotify->Notify(executor, msg);
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s boots you off channel %s."),
pExecMoniker ? reinterpret_cast<const char *>(pExecMoniker)
: "???",
reinterpret_cast<const char *>(ch->name));
m_pINotify->Notify(thing, msg);
if (!(key & CBOOT_QUIET))
{
// Broadcast boot message to channel.
//
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("%s %s boots %s off the channel."),
reinterpret_cast<const char *>(ch->header),
pExecMoniker ? reinterpret_cast<const char *>(pExecMoniker)
: "???",
pVictMoniker ? reinterpret_cast<const char *>(pVictMoniker)
: "???");
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 20:31:58 -06:00
SendChannelMessage(executor, ch, msg, nullptr, false);
}
// Remove victim from channel.
//
do_delcomchannel(thing, ch->name, (key & CBOOT_QUIET) != 0);
return MUX_S_OK;
}
MUX_RESULT CComsysMod::ProcessCommand(dbref executor, const UTF8 *pCmd,
bool *pbHandled)
{
*pbHandled = false;
// Parse "alias message" pattern.
//
const char *t = strchr(reinterpret_cast<const char *>(pCmd), ' ');
if ( nullptr == t
|| t - reinterpret_cast<const char *>(pCmd) > MAX_ALIAS_LEN
|| t[1] == '\0')
{
return MUX_S_OK;
}
// Extract alias.
//
UTF8 alias[MAX_ALIAS_LEN + 1];
size_t nAlias = t - reinterpret_cast<const char *>(pCmd);
memcpy(alias, pCmd, nAlias);
alias[nAlias] = '\0';
// Look up channel from alias.
//
const UTF8 *ch = get_channel_from_alias(executor, alias);
if (ch[0] == '\0')
{
return MUX_S_OK;
}
// Found a valid alias — dispatch to the channel message processor.
//
UTF8 *pMsg = const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(t + 1));
do_processcom(executor, ch, pMsg);
*pbHandled = true;
return MUX_S_OK;
}
MUX_RESULT CComsysMod::GetRevision(int *pRev)
{
if (nullptr == pRev)
{
return MUX_E_INVALIDARG;
}
*pRev = m_revision;
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// Alias management.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::AddAlias(dbref executor, const UTF8 *pAlias,
const UTF8 *pChannel)
{
if (nullptr == pAlias || '\0' == pAlias[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You need to specify a valid alias."));
}
return MUX_E_INVALIDARG;
}
if (nullptr == pChannel || '\0' == pChannel[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You need to specify a channel."));
}
return MUX_E_INVALIDARG;
}
// Validate alias length.
//
size_t nAlias = strlen(reinterpret_cast<const char *>(pAlias));
if (nAlias > MAX_ALIAS_LEN)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You need to specify a valid alias."));
}
return MUX_E_INVALIDARG;
}
struct channel *ch = select_channel(pChannel);
if (nullptr == ch)
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Channel %s does not exist yet."),
reinterpret_cast<const char *>(pChannel));
m_pINotify->RawNotify(executor, msg);
}
return MUX_E_NOTFOUND;
}
if (!test_join_access(executor, ch))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Sorry, this channel type does not allow you to join."));
}
return MUX_E_PERMISSION;
}
comsys_t &c = get_comsys(executor);
if (static_cast<int>(c.aliases.size()) >= MAX_ALIASES_PER_PLAYER)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Sorry, but you have reached the maximum number of "
"aliases allowed."));
}
return MUX_E_FAIL;
}
// Check for duplicate alias.
//
for (const auto &ca : c.aliases)
{
if (ca.alias == reinterpret_cast<const char *>(pAlias))
{
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("That alias is already in use for channel %s."),
ca.channel.c_str());
m_pINotify->RawNotify(executor, msg);
}
return MUX_E_FAIL;
}
}
// Append and sort.
//
com_alias ca;
ca.alias = reinterpret_cast<const char *>(pAlias);
ca.channel = reinterpret_cast<const char *>(ch->name);
c.aliases.push_back(std::move(ca));
sort_com_aliases(c);
sqlite_wt_player_channel(executor, pAlias, ch->name);
// Join channel if not already a user.
//
if (nullptr == select_user(ch, executor))
{
do_joinchannel(executor, ch);
}
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Channel %s added with alias %s."),
reinterpret_cast<const char *>(ch->name),
reinterpret_cast<const char *>(pAlias));
m_pINotify->RawNotify(executor, msg);
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::DelAlias(dbref executor, const UTF8 *pAlias)
{
if (nullptr == pAlias || '\0' == pAlias[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("Need an alias to delete."));
}
return MUX_E_INVALIDARG;
}
comsys_t &c = get_comsys(executor);
for (auto it = c.aliases.begin(); it != c.aliases.end(); ++it)
{
if (it->alias != reinterpret_cast<const char *>(pAlias))
{
continue;
}
// Count other aliases for same channel.
//
int found = 0;
for (const auto &ca : c.aliases)
{
if (ca.channel == it->channel)
{
found++;
}
}
// If last alias for this channel, remove from channel.
//
if (found <= 1)
{
do_delcomchannel(executor,
reinterpret_cast<const UTF8 *>(it->channel.c_str()),
false);
}
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Alias %s for channel %s deleted."),
reinterpret_cast<const char *>(pAlias),
it->channel.c_str());
m_pINotify->RawNotify(executor, msg);
}
sqlite_wt_delete_player_channel(executor, pAlias);
c.aliases.erase(it);
return MUX_S_OK;
}
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor, M_("Unable to find that alias."));
}
return MUX_E_NOTFOUND;
}
MUX_RESULT CComsysMod::ClearAliases(dbref executor)
{
auto it = m_comsys.find(executor);
if (it == m_comsys.end())
{
return MUX_S_OK;
}
comsys_t &c = it->second;
// Walk backwards to safely remove each alias.
//
while (!c.aliases.empty())
{
std::string alias = c.aliases.back().alias;
DelAlias(executor, reinterpret_cast<const UTF8 *>(alias.c_str()));
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// Channel creation/destruction.
// ---------------------------------------------------------------------------
MUX_RESULT CComsysMod::CreateChannel(dbref executor, const UTF8 *pName)
{
if (nullptr == pName || '\0' == pName[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You must specify a channel to create."));
}
return MUX_E_INVALIDARG;
}
// Check permission.
//
if (nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
if (!bCommAll)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You do not have permission to create channels."));
}
return MUX_E_PERMISSION;
}
}
// #1874: reject overlong names before any truncate/duplicate check.
// Truncating to MAX_CHANNEL_LEN after select_channel(full name) let
// "existing50byte" + "x" collide on the map key and replace the old
// channel without SQLite cleanup.
//
const size_t nName = strlen(reinterpret_cast<const char *>(pName));
if (nName > MAX_CHANNEL_LEN)
{
if (nullptr != m_pINotify)
{
UTF8 msg[128];
mux_sprintf(msg, sizeof(msg),
M_("Channel names must be at most %d characters."),
MAX_CHANNEL_LEN);
m_pINotify->RawNotify(executor, msg);
}
return MUX_E_INVALIDARG;
}
// Check for existing channel (full name — same length as stored key).
//
if (nullptr != select_channel(pName))
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("That channel already exists."));
}
return MUX_E_FAIL;
}
// Create the channel.
//
auto ch = std::make_unique<channel>();
strncpy(reinterpret_cast<char *>(ch->name),
reinterpret_cast<const char *>(pName), MAX_CHANNEL_LEN);
ch->name[MAX_CHANNEL_LEN] = '\0';
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 20:31:58 -06:00
mux_sprintf(ch->header, MAX_HEADER_LEN + 1,
T("[%s]"), reinterpret_cast<const char *>(ch->name));
ch->type = CHANNEL_DEFAULT;
ch->charge_who = executor;
ch->chan_obj = NOTHING;
channel *pCh = ch.get();
std::vector<UTF8> key(ch->name,
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
m_channels[key] = std::move(ch);
sqlite_wt_channel(pCh);
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Channel %s created."),
reinterpret_cast<const char *>(pCh->name));
m_pINotify->RawNotify(executor, msg);
}
return MUX_S_OK;
}
MUX_RESULT CComsysMod::DestroyChannel(dbref executor, const UTF8 *pName)
{
if (nullptr == pName || '\0' == pName[0])
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You must specify a channel to destroy."));
}
return MUX_E_INVALIDARG;
}
struct channel *ch = select_channel(pName);
if (nullptr == ch)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("That channel does not exist."));
}
return MUX_E_NOTFOUND;
}
// Check permission: must be owner or Comm_All.
//
bool bAllowed = (ch->charge_who == executor);
if (!bAllowed && nullptr != m_pIPermissions)
{
bool bCommAll = false;
m_pIPermissions->HasCommAll(executor, &bCommAll);
bAllowed = bCommAll;
}
if (!bAllowed)
{
if (nullptr != m_pINotify)
{
m_pINotify->RawNotify(executor,
M_("You do not have permission to destroy that channel."));
}
return MUX_E_PERMISSION;
}
// Capture canonical name before the channel object goes away.
//
const std::string channelName(
reinterpret_cast<const char *>(ch->name));
// Remove from SQLite. ON DELETE CASCADE clears channel_users and
// player_channels rows for this channel name.
//
if (nullptr != m_pIStorage)
{
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
log_storage_failure(m_pIStorage->DeleteChannel(ch->name),
"DeleteChannel(channel=%s)",
reinterpret_cast<const char *>(ch->name));
bump_revision();
}
// #1199: purge other players' in-memory aliases that pointed here.
// (SQLite rows are already CASCADE-deleted; still drop local aliases.)
//
for (auto &kv : m_comsys)
{
comsys_t &c = kv.second;
for (auto ait = c.aliases.begin(); ait != c.aliases.end(); )
{
if (ait->channel == channelName)
{
sqlite_wt_delete_player_channel(c.who,
reinterpret_cast<const UTF8 *>(ait->alias.c_str()));
ait = c.aliases.erase(ait);
}
else
{
++ait;
}
}
}
// Notify before erasing (ch will be destroyed by unique_ptr).
//
if (nullptr != m_pINotify)
{
UTF8 msg[256];
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 20:31:58 -06:00
mux_sprintf(msg, sizeof(msg),
M_("Channel %s destroyed."),
reinterpret_cast<const char *>(pName));
m_pINotify->RawNotify(executor, msg);
}
// Remove from map — unique_ptr cleans up channel and all users.
//
std::vector<UTF8> key(ch->name,
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
m_channels.erase(key);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// mux_IServerEventsSink implementation — server lifecycle events.
// ---------------------------------------------------------------------------
void CComsysMod::startup(void)
{
if (nullptr != m_pILog)
{
bool fStarted;
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
T("INI"), T("INFO"));
if (MUX_SUCCEEDED(mr) && fStarted)
{
m_pILog->log_text(T("Comsys module startup complete."));
m_pILog->end_log();
}
}
}
void CComsysMod::presync_database(void)
{
// Nothing needed — SQLite write-through handles persistence.
}
void CComsysMod::presync_database_sigsegv(void)
{
// Nothing.
}
void CComsysMod::dump_database(int dump_type)
{
UNUSED_PARAMETER(dump_type);
// Nothing needed — SQLite WAL checkpoint handles durability.
}
void CComsysMod::dump_complete_signal(void)
{
// Nothing.
}
void CComsysMod::shutdown(void)
{
if (nullptr != m_pIStorage)
{
m_pIStorage->Release();
m_pIStorage = nullptr;
}
if (nullptr != m_pILog)
{
bool fStarted;
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
T("INI"), T("INFO"));
if (MUX_SUCCEEDED(mr) && fStarted)
{
m_pILog->log_text(T("Comsys module shutting down."));
m_pILog->end_log();
}
}
}
void CComsysMod::dbck(void)
{
// Channel consistency checks (#1195):
// 1. Prune users with invalid dbrefs only — object listeners stay.
// 2. Reassign channel ownership if owner is gone.
// 3. Prune comsys alias rows for destroyed players.
//
for (auto &kv : m_channels)
{
struct channel *ch = kv.second.get();
for (auto it = ch->users.begin(); it != ch->users.end(); )
{
bool bValid = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsValid(it->second.who, &bValid);
}
if (!bValid)
{
it = ch->users.erase(it);
}
else
{
++it;
}
}
if (ch->charge_who != NOTHING && nullptr != m_pIObjectInfo)
{
bool bValid = false;
m_pIObjectInfo->IsValid(ch->charge_who, &bValid);
if (!bValid)
{
ch->charge_who = 1; // GOD
}
}
}
for (auto it = m_comsys.begin(); it != m_comsys.end(); )
{
bool bPlayer = false;
if (nullptr != m_pIObjectInfo)
{
m_pIObjectInfo->IsPlayer(it->first, &bPlayer);
}
if (!bPlayer)
{
it = m_comsys.erase(it);
}
else
{
++it;
}
}
}
void CComsysMod::connect(dbref player, int isnew, int num)
{
UNUSED_PARAMETER(isnew);
UNUSED_PARAMETER(num);
PlayerConnect(player);
}
void CComsysMod::disconnect(dbref player, int num)
{
UNUSED_PARAMETER(num);
PlayerDisconnect(player);
}
void CComsysMod::data_create(dbref object)
{
UNUSED_PARAMETER(object);
}
void CComsysMod::data_clone(dbref clone, dbref source)
{
UNUSED_PARAMETER(clone);
UNUSED_PARAMETER(source);
}
void CComsysMod::data_free(dbref object)
{
PlayerNuke(object);
}
// ---------------------------------------------------------------------------
// CComsysModFactory — boilerplate.
// ---------------------------------------------------------------------------
CComsysModFactory::CComsysModFactory(void) : m_cRef(1)
{
}
CComsysModFactory::~CComsysModFactory()
{
}
MUX_RESULT CComsysModFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CComsysModFactory::AddRef(void)
{
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
}
uint32_t CComsysModFactory::Release(void)
{
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
if (1 == prev)
{
delete this;
return 0;
}
return prev - 1;
}
MUX_RESULT CComsysModFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CComsysMod *pComsysMod = nullptr;
try
{
pComsysMod = new CComsysMod;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pComsysMod)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pComsysMod->FinalConstruct();
if (MUX_FAILED(mr))
{
pComsysMod->Release();
return mr;
}
mr = pComsysMod->QueryInterface(iid, ppv);
pComsysMod->Release();
return mr;
}
MUX_RESULT CComsysModFactory::LockServer(bool bLock)
{
if (bLock)
{
g_cServerLocks++;
}
else
{
g_cServerLocks--;
}
return MUX_S_OK;
}