The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`. Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.
The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.
New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.
The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper). const_cast budget: zero
added.
Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW). delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup. After any signature flip, grep the
old spelling.
Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mail_mod and comsys_mod used C atoi for folders, charges, dbrefs, and
history config — the same silent 32-bit truncation as int sinks after
mux_atoi64. Route those through mux_atoi64 (mathutil.h) and widen
locals where they matter.
Also: DESC idle timeout clamps after int64 parse; engine/module
@cset/log MAX_LOG uses int64_t end-to-end with mux_i64toa_t.
Convert remaining engine `int x = mux_atoi64(...)` (and explicit
static_cast<int> forms) to int64_t so parse width is not discarded on
any platform. Includes functions/funceval/funmath, mail folder numbers,
comsys charge parse, JIT register indices, and HIR lower mid/left/right.
dbref parse keeps full-width parse then rejects values outside int
range. Channel charge still stores int after an in-range check.
The scope Brazil set on #1723: join/leave are the server speaking about an
event, so they translate; the speech wrapper (" says, ") is scaffolding
around a player's own words and deliberately stays T().
The engine built all four announcements as pose fragments through
BuildChannelMessage -- T(":has joined this channel.") appended after a
composed comtitle+name prefix -- so no translatable unit existed, and the
same catalogue translated channel traffic under the module but not under
the built-in engine every game actually runs.
## Shape
BuildChannelSpeaker the comtitle/name/SPOOF logic extracted from
BuildChannelMessage; the pose path is byte-identical.
BuildChannelAnnounce the sentence path: an M_()-resolved format with one
%s, receiving the comtitle-decorated speaker, header
prepended. Four sites converted: join, leave (both
leavechannel and delcom), connect, disconnect.
Join/leave reuse the msgids the module has always used, so the two
implementations converge on the same catalogue entries (#1614). Connect/
disconnect already existed in the .pot from engine_com.cpp's login
announcements; only the es/ko fills were new.
Measured, built-in engine, LANG=C:
en [prueba] Wizard has joined this channel.
es [prueba] Wizard se ha unido a este canal.
ko [prueba] Wizard님이 이 채널에 참여했습니다.
English output is byte-identical to before -- header + ' ' + speaker +
sentence composes the same text either way -- and the conformance baseline
passed WITHOUT re-blessing, which is the proof of that.
## SPEECHMOD: kept, with a documented change
The old pose path ran the SPEECHMOD hook over the bare verb phrase. The
verb phrase no longer exists as a unit, so %0 now carries the whole
announcement ("Jess has joined this channel."), and an evaluated attribute
runs once per audience variant rather than once in total. Kept rather than
dropped because it is a live softcode hook; changed rather than emulated
because the old input cannot be reconstructed from a translated sentence.
## The format guard fired, correctly
check_formats.py rejected the new call site -- safe_tprintf_str() with a
non-literal format -- which is exactly the ratchet working on new code.
The parameter is named pChannelAnnounceFmt and allowlisted beside
pRoomAnnounceFmt / pMonitorAnnounceFmt, the prior catalogue-resolved
announcement formats, with the justification recorded in the allowlist:
check_nls.py verifies every msgstr keeps its msgid's conversion types,
which is the property the guard exists to protect.
es.po 282 -> 284/977 (29.1%)
ko.po 313 -> 315/977 (32.2%)
make test: Smoke 1561 x3, conformance PASSED (no re-bless), handoff 13,
format 31749, tests/nls 7, ko 4, plural 43. TESTEXIT=0.
Refs #1419, #1614, #1622, #1702, #1723.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dual-path module already used the executor moniker for
"%s boots you off channel %s."; engine mistakenly used Moniker(thing)
so the victim was told they booted themselves.
Residual Phase 3 player/staff prose that still used T() on live paths:
connect/reconnect/disconnect room and monitor lines, [Suspect] flags,
GAME: restart/shutdown/signal/log-full, dual-path channel boot, and
@email hostname resolution. pot + xx complete + ko msgmerge append;
tools/merge_nls_markings.py for Windows agents without xgettext
(canonical pot remains make -C mux/po pot on Linux).
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>
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>
The engine wrote "1" to enable and atr_clr()ed to disable, then read the
attribute back with atr_get_info -- a presence test that never looks at the
value. Self-consistent for engine-written data, and wrong the moment the
comsys module writes it.
The module cannot clear. mux_IAttributeAccess exposes SetAttribute and no
remove (modules.h:552), so its only way to say "off" is to write ""
(comsys_mod.cpp:2742). Under a presence test that reads as ON, so a
channel whose timestamps were turned off under the module came back
timestamped under the engine.
Read the value instead. The engine never writes empty -- it writes "1" or
clears -- so no engine-written data changes meaning, and non-empty is
already how the module reads it (comsys_mod.cpp:1126). Both
implementations now agree: "1" is on, empty or absent is off. No module
API change, which is what makes this the small end of the fix; adding a
clear to mux_IAttributeAccess is a larger question and #1572 needs it
decided anyway.
Measured with two muxscript passes over ONE database -- the first with
`module comsys_mod` configured, the second without, so the module writes
the attribute and the built-in engine reads it. Linux runs the module
implementation under the harness (#1581), which is what makes this
reachable here at all:
pass 1 (module): @cset/timestamp_logs tschan=1 -> LOG_TIMESTAMPS "1"
@cset/timestamp_logs tschan=0 -> LOG_TIMESTAMPS ""
pass 2 (built-in): crecall shows the line
before: [Mon Jul 27 11:02:17 2026] <TS> Wizard says, "hello there"
after: <TS> Wizard says, "hello there"
Negative control: with the value test forced back to presence, the
timestamp returns. Worth recording that the control also caught a bad
assertion in my probe -- it matched "[<digit>" while the timestamp begins
"[Mon", so the check passed vacuously until the control disagreed with it.
make test green, both smoke routes 1560/1560.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
do_chanlog and do_chanlog_timestamps each fail for several unrelated
reasons and returned a bare bool, so do_cset rendered one message for all
of them -- the one blaming the argument. On a channel with no channel
object:
@cset/log cgtest=20
"@cset: Maximum history must be a number less than or equal to 200."
for a value that plainly satisfies that condition. The history buffer
lives on the channel object, so with no object there is nowhere to record
it; that is a missing prerequisite, not a bad argument.
The downstream symptom does not point back here either. Until the buffer
is set, cbuffer() is 0 and crecall() returns empty on a channel with
obvious traffic -- so it reads as a broken crecall, and nothing connects
that to @cset/object. I lost a probe cycle to exactly this while writing
comsys coverage, concluding @cset/log was flaky before noticing the working
case had an object attached and the failing one did not.
Both helpers now return a ChanLogResult naming the reason, and do_cset
picks the message:
CHANLOG_OK unchanged success text
CHANLOG_BAD_VALUE the existing value message (still correct here)
CHANLOG_NO_OBJECT "... has no channel object. Use @cset/object ..."
CHANLOG_NOT_LOGGING "... has no history buffer. Use @cset/log ..."
CHANLOG_INTERNAL a plain failure, for mkattr failing
do_chanlog_timestamps is fixed alongside rather than left as the odd one
out: it is the same defect in the twin function, and its old message ("Is
logging enabled for %s?") was right for only one of its four failure paths.
A bad value there now says so instead.
Message style follows the rest of the do_cset switch: tprintf(T(...)),
since every one of these takes a format argument. M_() is used in this
file only for literals passed straight to raw_notify, and tprintf(M_(...))
appears nowhere in the tree, so no NLS decision is implied.
Verified on Windows Server 2022, MSVC 14.51, Release x64. Clean build, 0
warnings. Every path exercised live:
no object -> "Channel cgtest has no channel object. Use ..."
value 9999 -> the value message (unchanged, still correct)
value abc -> the value message (unchanged, still correct)
ts, no object -> "... has no channel object. Use @cset/object ..."
ts, no buffer -> "... has no history buffer. Use @cset/log ..."
ts, value 7 -> "Timestamp logging for channel cgtest must be 0 or 1."
success paths -> byte-identical to before, and cbuffer reports 20
Smoke 1524 succeeded / 17 failed, 318/318 dispatched -- but that is a
regression check only, not evidence for the fix: nothing in testcases/
invokes @cset/log or @cset/timestamp_logs today, so the suite cannot
distinguish this change. The live transcript above is the evidence.
PR #1554 adds a fixture that does use @cset/log, which will cover the
success path once it lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A few sites wrapped constant prose in tprintf() with no format arguments
(comsys max-aliases message, Lua-module-loaded line, three mail admin
notices). Drop the no-op tprintf and mark with M_() so they enter the
catalog like other constant notify prose. Leave real format strings as
T(). Regenerate pot/xx without fuzzy entries.
Replace ~900 typographic \xE2\x80\x.. escapes (curly quotes, en dashes)
and a few \xE2\x80\230 octal workarounds with real UTF-8 in message
strings under mux/modules and mux/src. Leave stringutil and convert
charset mapping tables as explicit byte sequences.
Mechanical hygiene under the opt-in M_() design: replace T("#-1…") and
T("#-2…") with S_() so softcode ABI tokens are obvious in source and
cannot enter a player catalog. ~400 call sites across engine, exp3,
mail, and driver. Assembled/library-spliced diagnostics (plan §4.2)
are unchanged where they are not a single literal.
Completes the sweep the issue called for. mux_atol returns long, which
is 32-bit on LLP64, so every caller silently truncated on Windows. Two
of those were real defects (the truthiness family and cf_size, fixed in
the preceding commits); the rest were latent, waiting for a value large
enough to matter.
Rather than audit 290 sites for whether each can reach 2^31 today, use
the 64-bit parser everywhere and remove the class. A dbref cannot
overflow now, but nothing stops a later caller passing that same site a
timestamp or a byte count.
Pure 1:1 substitution: 285 lines changed, and every removed line
contained mux_atol while every added line contains mux_atoi64. No
control flow, no types, no behaviour beyond the wider parse.
This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so
the generated code there is unchanged. It only widens the parse on
Windows. Narrowing destinations are unaffected either way: `int x =
mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both
models.
Left alone: mux_atol itself in mathutil, its declaration, and three
comments that name it. Callers that genuinely want 32-bit semantics can
still ask for them; none appear to.
Verified on Windows: full solution builds clean with no new warnings,
smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched --
identical to before the sweep, with the same 16 build-configuration
failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST).
Spot checks after the change: the boolean family returns 1 for multiples
of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited,
and arithmetic, string and list functions are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- #1204: fun_chanfind matches channel name or header (case-insensitive).
Default colored [name] headers made name-only lookups fail while help
examples implied reverse header lookup; both are now supported.
- #1199: do_destroychannel, do_channelnuke, and module DestroyChannel
sweep other players' aliases that pointed at the destroyed channel
so in-memory comsys state matches SQLite CASCADE cleanup.
- Update help.txt and add smoke TC015b for name-based chanfind.
Review findings on #1191:
1. Re-entrancy. sqlite_load_comsys() calls select_channel() for every
channel_users row, and select_channel() now calls
ensure_comsys_softcode_sync() -- which assigns s_seen_rev only AFTER
the load returns, so the nested call saw the same stale revision and
started the load again. Instrumenting the hook showed it really does
re-enter (depth 2 on a @ccreate + addcom + chanusers probe with the
modules active); it terminated incidentally rather than by design, so
a future change to the loader could turn it into unbounded recursion.
Guard both hooks on mudstate.bSQLiteLoading -- depth is now 1 with the
same results.
2. IID bumps. GetRevision (comsys) and GetRevision/SoftcodeSend (mail)
grow the control-interface vtables, so a stale comsys_mod.so /
mail_mod.so must fail mux_CreateInstance discovery instead of being
called through slots it does not implement. Bump IID_IComsysControl
and IID_IMailControl (the #817 pattern, matching the IID_IMailStorage
bump that went with #1203). Verified the modules still load and the
gen-sync still works after the bump.
Verified: @list modules reports comsys_mod/mail_mod loaded (the PR's
mechanism works -- all four required exports are now present); module
-owned channels are visible to softcode chanusers(); full make test
green, smoke 1407/1407, 0 crashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: comsys_mod/mail_mod lacked mux_CanUnloadNow, so ModuleLoad
never set bLoaded and CreateInstance always returned CLASSNOTAVAILABLE —
commands and softcode both used the engine path by accident.
With modules actually loaded:
- Bump module revision on every write-through; softcode reloads engine
maps from SQLite when the revision advances (select_channel, mail_fetch,
MailList, etc.).
- CComsysStorage::SyncChannel sets has_comsys so softcode reload works.
- Channel Sync uses ON CONFLICT DO UPDATE so updates do not CASCADE-wipe
channel_users/player_channels.
- mailsend() goes through IMailControl::SoftcodeSend; bodies and
mail_db_top are write-through for softcode mail_* fields.
load_comsystem_V0123 converts the channel-name line to UTF-8 (pBufferUnicode,
updating nChannel), and when it exceeds MAX_CHANNEL_LEN backs off to a whole-
character boundary — but the backoff loop read temp (the pre-conversion line)
while the bytes copied come from pBufferUnicode. The header case just below does
it right (reads pBufferUnicode), so this was a copy-paste inconsistency.
Not memory-unsafe (nChannel <= 50 and temp is an LBUF, so in bounds; memcpy
count never exceeds pBufferUnicode), but loading a pre-2007 V0123 comsys file
with a channel name > 50 bytes after conversion could truncate the name mid-
character, leaving invalid UTF-8 in ch->name. Read pBufferUnicode[nChannel-1]
(matching the header path). Legacy load path only; smoke 1255/1255.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found verifying #783: the SQLite loaders are gated on metadata keys --
sqlite_load_comsys() on has_comsys, sqlite_load_mail() on mail_db_top
-- but ONLY the DbConvert -C/-m import path ever set them. Channel
and mail mutations write through to the tables row-by-row, so a
channel created with @ccreate (or mail sent in-game) on a game that
never went through a -C/-m import landed in the tables and was then
ignored forever: the next warm boot found no gate, fell back to the
legacy comsys.db/mail.db flatfiles, and the runtime came up without
them while the orphaned rows lingered.
The write-throughs now maintain the gates: sqlite_wt_channel() sets
has_comsys, and sqlite_wt_mail_body() keeps mail_db_top current (it
sizes the loader's body array). One cheap metadata upsert alongside
mutations that are already rare.
Verified: a channel @ccreate'd and mail sent in one muxscript session
now warm-load in a fresh process -- db_unload -C/-m exports both,
where before it exported nothing ("has no comsys/mail") despite the
rows sitting in the tables.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Incremental progress on the alloc_lbuf/free_lbuf -> LBuf RAII migration, adding
the structural enabler the issue calls for and converting a small, individually
verified set of the harder (ownership-escaping) sites.
alloc.h:
- LBuf gains release() (relinquish ownership to a caller) and reset() (free
now, become empty). release() lets a producer that returns an lbuf use RAII
for its early/error paths and hand the buffer off at the success return.
- New LBufPtr: an owning, nullable, movable handle. Unlike LBuf (which always
allocates and suits scoped scratch), LBufPtr can be empty, reseated, and
released — for conditional ownership, storing an lbuf in a struct/queue
entry, or producing a buffer to return. LBufPtr_Src / LBufPtr_Adopt mirror
the LBuf macros.
Converted sites (each verified to neither double-free nor leak on any path):
- flags.cpp: unparse_object_numonly, unparse_object — single-exit producers,
now LBuf + release().
- comsys.cpp: call_mogrifier — LBufPtr + release() (its early nullptr returns
precede the allocation, so they are unaffected).
- functions.cpp: switch_handler / switchall_handler — the ping-pong mbuff/tbuff
scratch buffers now use LBuf RAII, removing six manual free_lbuf() calls
including the error-prone early-return path in switch_handler. mbuff is only
transiently aliased into mudstate.switch_token (restored before scope exit),
so RAII scope-exit freeing preserves its lifetime.
The remaining ~145 manual sites (fargs[] stores, did_it() charge/runout swaps,
cross-function/struct lifetimes) still need per-site structural work; #717 stays
open. Build clean, all 1053 smoke tests pass (the corpus exercises switch()/
case()/unparse_object heavily).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use the new LBuf_Adopt() macro to take RAII ownership of caller-owned
pool buffers returned by atr_get, atr_pget, and atr_get_LEN. This
eliminates ~82 explicit free_lbuf calls and automatically covers
early-return paths that previously required careful manual placement
of the free.
Heaviest conversions: player.cpp (13), comsys.cpp (14), command.cpp (9).
Complex interleaved patterns (did_it charge/runout swaps, PureName
reassignment, process_cmdent loops) left manual for now.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fourth wave: ast (11), funceval (10), funceval2 (7), comsys (3),
speech (10), set (6), engine (7), command (10). Covers the NOEVAL
handlers (cand/cor/if/switch/iter), function evaluators (ifelse,
letq, objeval, sortby, munge, while, sandbox), notify_check message
buffers, and command dispatch paths. Buffers stored in fargs[]
arrays, returned through output pointers, or used in ping-pong swap
patterns are left manual.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes silent LBUF truncation on large channel/mailbox lists (GitHub #704).
All four list-returning functions now accept optional offset/limit args
for softcode pagination. Backward compatible — existing calls unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LBuf class to alloc.h: an RAII wrapper around alloc_lbuf/free_lbuf
that moves LBUF_SIZE buffers from the stack to the heap pool. Convert
all 108 non-static UTF8 xxx[LBUF_SIZE] stack arrays across 25 source
files. Static BSS buffers (24) are unchanged.
This eliminates LBUF_SIZE from recursive stack frames, making it safe
to increase LBUF_SIZE without risking stack overflow in the evaluation
pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses staff feedback about channel name/header correlation and the
cost of looping over chanusers() to get per-user data.
chanfind(header):
Given a channel header string (e.g. "<PublicServices>"), returns the
canonical channel name (e.g. "PubServ"). Case-insensitive match with
subscriber-aware visibility. Solves the problem of players seeing a
display header in chat and not knowing the internal channel name.
chanusers(channel[, sep[, field]]):
Optional third argument returns per-user field values instead of
dbrefs. Supported fields: title, status, flags, gagjoin, comtitles,
alias (Wizard-only), name. This avoids the N-call loop pattern:
chanusers(PubServ, |, title) => Cherry|Trees|Muffin
5 new smoke tests (636/641 total, 5 pre-existing failures).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses staff feedback about @clist/full being effectively broken when
games disable the cp/co permission flags in favor of lock-based access
control.
@clist/full changes:
- Add Header column so players can correlate display names with channels
- Show owner by name (Moniker) instead of raw integer dbref
- Replace raw flag bits with effective access for the querying player
(JXR based on test_*_access which respects both flags and locks)
- Drop dead Balance/Charge/Obj columns
- Fix subscriber visibility (same bug fixed for channels() in prior commit)
Also fix subscriber visibility in normal @clist.
New chaninfo() fields:
- canjoin: 1/0 based on test_join_access(executor)
- cantransmit: 1/0 based on test_transmit_access(executor)
- canreceive: 1/0 based on test_receive_access(executor)
These let softcode build rich channel listings that reflect actual
access control (flags + locks), not just the raw permission bits.
3 new smoke tests (631/636 total, 5 pre-existing failures).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the softcode accessor redesign (docs/design-softcode-accessors.md).
Three new generic field-based channel query functions that expose all
schema-stored comsys data with subscriber-aware visibility:
- chaninfo(channel, field): 11 fields (name, header, owner, object,
type, flags, charge, users, msgs, desc, buffer). The object field
preserves the existing Wizard-only restriction from chanobj().
- chanusers(channel[, sep]): delimited list of subscriber dbrefs.
- chanuser(channel, player, field): 6 per-user fields (alias, title,
status, flags, gagjoin, comtitles) with per-field permissions matching
existing comalias/comtitle/cflags semantics.
Also fixes channels() to include non-public channels where the executor
is a subscriber — previously subscribers were locked out of their own
channel metadata.
22 new smoke tests (628/633 total, 5 pre-existing failures).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The modular architecture (netmux.exe, libmux.dll, engine.dll, etc.)
with static CRT (/MT) gave each module its own CRT heap. FILE* handles
from mux_fopen (in libmux) crashed when used by stdio functions in
other modules — fclose in write_pidfile, fgets in cf_include, etc.
Switch all 11 vcxproj files to /MD (shared CRT DLL) so all modules
share one CRT instance. Ship msvcp140.dll, vcruntime140.dll, and
vcruntime140_1.dll in the binary distribution.
Also add mux_fclose to libmux as good hygiene (pairs with mux_fopen),
and replace all cross-module fclose calls. This change is safe on Unix
where everything links into one process.
Add Startmux.bat as a replacement for Startmux.wsf since Windows
Script Host is no longer associated by default on modern Windows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Players can set a CHATFORMAT attribute on themselves to customize how
channel messages appear. When a channel message is delivered, if the
recipient has CHATFORMAT, it is evaluated with:
%0 = channel name
%1 = default formatted message
%2 = sender dbref
The output replaces the default message for that player only.
Example: &CHATFORMAT me=\[[ansi(hc,%0)]\] %1
Engine-side implementation (comsys.cpp SendChannelMessage).
Lazy-initialized attribute lookup — zero overhead when no players
use CHATFORMAT. Help text added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real-world comsys.db files can contain player aliases pointing to
channels that no longer exist (e.g., a deleted "Mudnet" channel).
The FK constraint on player_channels.channel_name → channels.name
rejects the insert, aborting the entire comsys import.
Skip orphaned aliases via select_channel() check before inserting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite notify_check() from mux_string to raw PUA-encoded UTF-8 lbufs.
The engine now passes PUA strings through; the network layer converts
PUA to ANSI/XTERM/HTML per client. html_escape() replaces encode_Html(),
co_strip_color() replaces export_TextPlain() for @listen matching.
Remove mux_string overloads: raw_notify, raw_notify_html,
send_text_to_player (session.cpp, conn_bridge.cpp, net.cpp).
Convert handle_ears, look_for_exits (predicates.cpp), notify_comsys
(comsys.cpp), and small uses in mail.cpp/match.cpp.
Add extern "C" guards to color_ops.h for C++ inclusion.
593/593 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>