Empty TEXT/BINARY messages no longer call save_command (they bypassed
input_limit while still growing the input queue). Payloads larger than
LBUF_SIZE-1 fail the connection with close 1009 instead of reaching
engine command buffers.
Harden space_compress to leave room for the trailing NUL when filling
SpaceCompressCommand (one-byte OOB at full LBUF of non-space).
Unit tests: empty flood, LBUF and LBUF-1 TEXT boundaries.
The gates in #1325 are met and the closing of #1433 was the last one:
- The differential harness executes every supported shape -- 140
chunks, EXEC requiring lua_run_ok to advance, declines all deliberate
(os.time, two pins, select arity).
- All three loop forms run under an aborting instruction budget whose
exhaustion surfaces the interpreter's own error, verbatim (#1732).
- The compiled path wins or ties every benched shape; loops beat the
VM's marginal per-iteration cost after #1743 (#1741).
- #1433 closed unreproduced: three machines, thousands of process
runs, including the original commit on the machine that first saw it.
- Windows gates closed by the maintainer.
A chunk the compiler cannot honestly run declines to the interpreter,
so OFF now buys nothing but the slower path; lua_jit=0 remains the
diagnostic switch. With the default on, ordinary smoke exercises the
engaged path -- TC006 asserts lua_run_ok advances under the default
config -- and the explicit lua_jit-0 cases keep the gate itself tested.
smoke 1561/0 with the JIT engaged; luajit harness 140 chunks, 0 wrong;
full make test clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found while translating mail (#1419). The tell was a line wrong in English
before translation is even involved:
MAIL: 1 messages in folder 0 [unnamed] (1 unread, 0 cleared).
MN_() exists for this (#1622) and #1655 converted the @mail/stats family;
this set was missed. A translator cannot work around it -- the msgid has no
msgid_plural, so there is no second form for a catalogue to supply, and es
must pick one wording for all counts while ru and pl need three and can
express none of them. The fix is in the C++, not the .po.
## Converted, 11 sites
mail.cpp, mail_mod.cpp mailbox is full; URGENT MAIL urgent messages
walkdb.cpp universe contains; objects @chowned; objects marked
object.cpp objects @chowned to you
wiz.cpp You toaded ... objects @chowned
command.cpp at most N players logged in
The mail module duplicates are converted alongside the engine's so the two
implementations do not drift (#1614).
## One was a different bug than the issue described
walkdb.cpp built its message as M_("%d objects %smarked") with %s = "" or
"un". That is fragment assembly, not a missing plural: a translator handed
that %s cannot use it, because the prefix is English morphology rather than
a word. Split into two whole sentences, each pluralised -- the #1575 /
#1588 rule. Now renders "1 object marked" / "8 objects unmarked" instead of
a %s a catalogue has no way to fill.
## Two deliberately NOT converted
MAIL: %d messages in folder %d [%s] (%d unread, %d cleared).
%d objects = %d rooms, %d exits, %d things, %d players. (%d garbage)
MN_(s, p, n) selects one form from one n. Four independent counts are four
independent plural decisions, and in Russian each noun needs its own form.
These want rewording, which is a design call rather than a mechanical
conversion, so they are left and recorded rather than half-fixed.
## Catalogue fallout
.pot 18 -> 27 plural entries; all three catalogues msgmerge'd.
xx is `complete` policy, so its nine new entries had to be filled and
unfuzzied or the guard fails -- which it duly did, first try. msgmerge had
copied the old singular-only text into BOTH forms, so msgstr[0] said
"messages"; each form is now prefixed from its own msgid.
es and ko received real translations for all nine.
es.po 190 -> 198/976 (20.3%)
ko.po 229 -> 236/976 (24.2%)
## Verified end to end, at n=1 and n>1
en 1 object marked 8 objects marked
es 1 objeto marcado 8 objetos marcados
ko 사물 1개를 표시했습니다 사물 8개를 표시했습니다
xx [xx] 1 object marked [xx] 8 objects marked
Korean renders identically for both, which is correct: it declares
nplurals=1, so the catalogue's own rule is being honoured rather than
English's imposed on it.
The unconverted multi-count line still reads "1 messages", as documented.
make test: Smoke 1561 x3, tests/nls 7, ko 4, plural 43. TESTEXIT=0.
Refs #1419, #1575, #1588, #1614, #1622, #1655, #1702.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regenerate pot/xx/ko for new per-label msgids (Function Name, Hook Mask
Values, …). Attribute @hook rows still used printf while the header and
command rows used mux_table — call hook_loop so both tables share C7.
Regenerate pot/xx/ko after Phase 4 C* (drop blob headers; add per-label
msgids). Row right-just must use RightJustifyNumber — mux_vsnprintf does
not implement %* width (#1429), so the filed %*d/%*lld path would echo
literally and drop the rest of each stats line.
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.
list_modules() enumerates a second time through the stubslave under
STUB_SLAVE, printing each entry as "<name> (loaded) by stubslave". It
obtains the management interface with no else on failure, so the whole
slave section vanished silently and @list modules answered as though the
slave held nothing -- while the slave process demonstrably had the module
mapped. The operator could not tell "nothing is loaded there" from "I
could not ask", which on the release build is the only question this
command exists to answer about the slave.
It fails every time today. Measured on aarch64 with a throwaway game,
`module comsys_mod slave` in the config, and comsys_mod.so confirmed
present in /proc/<slave pid>/maps while the process ran:
Modules:
engine (loaded)
libmux (unloaded)
No slave line, no error. Instrumenting the path: list_modules creates a
second CID_StubSlave instance, Pipe_SendCallPacketAndWait succeeds, the
reply frame comes back with 0 of 8 bytes for cidProxy, and
mux_UnmarshalInterface reports MUX_E_CLASSNOTAVAILABLE (-3). So the pipe
round trip happens and returns nothing to unmarshal.
That defect is NOT fixed here. Repairing a repeat CreateInstance in the
marshalling layer is delicate work on a path with no test coverage that I
cannot validate on Windows, and it deserves its own change. This makes it
visible instead of invisible -- the same fail-loudly argument as #1512,
where a silent fall-through turned an unreachable path into wrong data.
After:
Modules:
engine (loaded)
libmux (unloaded)
Could not query the stubslave module list (error -3).
Uses T() to match the surrounding raw_notify calls; whether operator
diagnostics should be M_() is an #1419 question, not one to settle inside
a one-branch fix.
Textually inside #ifdef STUB_SLAVE, so a default build is unaffected.
Verified on a --enable-stubslave build: make test smoke route 1 passes
1509/1509, 316/316 dispatched.
Note for anyone running the suite on this configuration: route 2 hit an
intermittent SEGV at shutdown, after all 316 files dispatched and all 1509
assertions passed, and did not recur in three further runs. That is
#1433's character rather than anything this touches, and is reported
there; it is not caused by this change and is not fixed by it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Next notify slice while wiz/cque is in review: permission/lock-style
messages in predicates.cpp and command-processing chatter in
command.cpp (~45 sites). Leave tprintf formats as T(). Half-mark pass
clean. 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.
Phase 1 of the Lua JIT bring-up: mudconf.lua_jit (GOD/WIZARD) defaults
to false so the interpreter path remains the only production path while
hir_lower_lua correctness work continues.
TryJIT no-ops when the gate is off; @list lua reports enabled/disabled.
Documents the full phase plan in docs/plan-lua-jit-bringup.md.
Paired with the #1278 loader cherry-pick on this branch: master can take
both and stay green; developers flip lua_jit 1 to exercise the JIT.
process_hook() returns a bool: true means "proceed", false means a PERMIT
hook denies or an IGNORE hook swallows the command. Under AF_NOEVAL -- or
a NOEVAL hook_obj -- it discarded the attribute's content outright and
returned true, so a NOEVAL permit hook allowed every command. A
permission control silently disabled, and indistinguishable from having no
hook configured at all.
Call sites confirm the polarity: command.cpp:2163-2170 (and the four
copies at 2350, 2513, 2680, 2775) set hval on `!process_hook(...)`, so
true is "allow". The default `retval = true` is correct for "no hook
attribute" -- it is only the NOEVAL branch that wrongly joins it.
Fixed by deriving the boolean from the unevaluated text. That is the
semantic the rest of the NOEVAL work already uses rather than a preference
of mine: 4cb904fcc introduced AF_NOEVAL as "return raw text with no
function calls" across a list of sites, and eval_boolexp -- the only other
*boolean* consumer in that list -- compares the raw text
(`bCheck = !string_compare(buff, b->sub1)`) instead of short-circuiting.
process_hook was the outlier. A literal "0" or "#-1 ..." hook now blocks
as written, and a literal "1" still permits.
Adds tests/scenario/hook_noeval.py. Not in the smoke corpus: @admin
hook_obj / hook_cmd are CA_GOD for both read and write, and smoke test
cases run as the test object rather than #1. The ufunction_fn.mux
@startup-on-#1 pattern does not carry over, because that publishes a
global any object can then call, whereas this needs the config pointed at
an object and the hook attribute mutated at run time.
The test validates against the unfixed build -- and the first draft did
not. It used `@set obj/attr=NOEVAL`, but the attribute flag is NO_EVAL
(`noeval` is an unrelated command switch), so it set nothing, the hook was
evaluated normally, and all three cases passed against the *broken* build.
Corrected, and it now asserts the flag is really present (flags() must
report 'E') so it cannot silently degrade that way again:
ok 1 - permit hook attribute is actually marked NO_EVAL
not ok 2 - NOEVAL permit hook denying '0' blocks the command
(lastcreate before='#17' after='#20')
ok 3 - NOEVAL permit hook allowing '1' still permits
Case 3 passing in both builds is deliberate: it stops the fix from being
"deny all NOEVAL hooks", which would have been fail-closed but wrong.
Verified: build clean; smoke 1427/1427, 0 crashes, 305/305 dispatched;
scenario 7/7 drivers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restart (#1039–#1043): zero process-local DESC fields after load,
restore peer via getpeername, strip non-survivable TLS/WS flags,
drop TLS and WebSocket before exec, tear down adopt failures with
shutdownsock, abort partial restart loads cleanly.
DB (#1044–#1047): cache_flush_writes returns success only after
Commit; leave dirty queue on failure; atr_head union SQLite with
pending cache/queue; set mudstate.bStandAlone in DbConvert; flush
attr queue before import Commit.
Queue (#1048–#1052): setup_que refunds on OOM; sql_que frees BQUE
on Query fail and inits waittime; do_wait rolls back semaphore if
wait_que fails; include nest depth limit 50; CPU halt_que uses
Owner+object for machines.
JIT (#1053–#1057): full 64-bit rv_load_i64; setq/r single-char
RegisterSet; decline long CARGS; accumulate inline u() watermarks;
bounded guest_strnlen on ECALL paths.
Conf/convert (#1058–#1061): @enable/@disable sync g_dc; live
driver knobs for output/retry/max_players/timeouts/quota; safe
heap encode for t5x/t6h/r7h attributes.
Two follow-ups to the @dolist/now rewrite (37e18bef5):
- New /break switch: an @break inside the body still stops the loop,
but with /break it also propagates to the enclosing command list,
aborting the commands after the @dolist. This restores the one
ability the containment change removed (using a break inside the
loop to abort the rest of a $-command), with sane loop-stop
semantics, and mirrors @include (propagate by default, /nobreak to
contain). Without /now the switch is inert.
- ;| pipe segments now work in inline command lists. The queued
runner has always special-cased '|' after ';' to capture the
previous command's output as %|; the inline loops in @dolist/now
and @include treated such a segment as a literal command starting
with '|' ("Huh?"). The splitting/pipe/break loop is factored into
process_command_list_inline() in cque.cpp, shared by both callers
and kept next to its queued twin so the semantics cannot drift.
The inline list is its own pipe domain (save/restore of the
enclosing pipe context), matching how each queued entry starts with
a clean pipe state -- an inline list running inside a piped segment
cannot corrupt the outer capture.
help @dolist documents /break and the ;| support.
Verified: default containment, /break propagation, and /break with no
inner break all behave as specified; pipes carry %| through inline
dolist and include bodies; smoke 1115/0/0 with three new test cases;
2x200 jit_diff clean.
Closes#788.
Co-Authored-By: Claude Fable 5 <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>
Eliminate the ISOUTOFMEMORY macro that unconditionally aborted on allocation
failure. Each of the 23 call sites now handles OOM appropriately:
- Fatal sites (buffer pools, db array, anum table): mux_assert or OutOfMemory
- Recoverable sites (queue, mail, commands, guests, vattrs, config, restart,
forward lists): log the failure and return gracefully
Also fix g_dump_child_pid portability: volatile pid_t -> volatile sig_atomic_t
with explicit casts in ganl_adapter.cpp.
Close integer overflow issue as false alarm (getstring_noalloc uses a bounded
static buffer, so nBuffer+1 cannot wrap).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add server-side @walk <npc>=<destination> that moves an object one hop
per second along the routed path until arrival, and @patrol <npc>=<rooms>
for continuous waypoint loop movement. Both use the system scheduler
(DeferTask self-rescheduling, same pattern as @cron) and call move_exit()
for full movement side effects.
New route_next_exit() C-level API returns the next-hop exit dbref
directly, avoiding text format/parse overhead for the walk subsystem.
Switches: /stop cancels, /quiet suppresses notifications, /locked
validates exit locks during movement.
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>
Compiled Lua chunks are cached as Lua registry references keyed by
source text. LRU eviction when cache exceeds lua_cache_size (default
256 entries). Second call to the same script skips compilation entirely.
Cache implementation:
- std::unordered_map<string, cache_entry> for O(1) lookup
- std::list<string> for LRU ordering
- luaL_ref/luaL_unref for Lua registry management
- CacheClear on shutdown/destructor
GetStats extended with cache_hits, cache_misses, cache_entries.
@list lua and @lua/stats now report cache statistics.
525/525 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mudstate lives in engine.so (loaded via dlopen at runtime), so the
driver binary cannot reference it at link time — stricter linkers
reject the unresolved symbol. Move the StubSlave lifetime pointer
to a driver-side global in driverstate.h. Engine-side code that
needs mux_ISlaveControl now obtains its own proxy on demand through
mux_CreateInstance via libmux, matching the existing pattern used
for CID_QueryServer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New switches modeled after RhostMUSH's @protect:
@protect/alias <name> — set a protected name as the player's alias.
The name must already be in the player's protected list. Replaces
any existing alias (TinyMUX supports one alias per player).
@protect/unalias <name> — remove the player's alias (must match).
@protect/all — Wizard-only. Lists all protected names across every
player in the database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four new features identified from the TinyMUSH/PennMUSH/RhostMUSH survey:
MSSP (MUD Server Status Protocol, telnet option 70):
- Server sends structured key-value data (NAME, PLAYERS, UPTIME, PORT,
CODEBASE, FAMILY) to MU* directory crawlers on IAC DO MSSP
- Stateless response via send_mssp() in telnet.cpp using g_dc config basket
- start_time_utc added to DRIVER_CONFIG for uptime calculation
GMCP (Generic MUD Communication Protocol, telnet option 201):
- Protocol negotiation: server offers WILL GMCP, tracks gmcp_enabled per DESC
- Inbound: GMCP subneg queued as synthetic "\x01GMCP" command, dispatched
to handle_gmcp() which fires A_GMCP attribute with %0=package %1=json
- Outbound: gmcp(<player>, <package>, <json>) softcode function sends
GMCP frames to all GMCP-enabled descriptors via SendGmcp COM method
- Full COM architecture: mux_IConnectionManager::SendGmcp in driver,
send_gmcp() bridge in engine, CConnectionManager impl in modules.cpp
@protect (player name reservation):
- @protect[/add] <name>, @protect/del <name>, @protect/list [<player>]
- A_PROTECTNAME attribute (234) stores space-separated protected names
- protectname_check() hooked into create_player() and do_name()
- max_name_protect config param (default 5)
benchmark(<expression>, <iterations>):
- FN_NOEVAL, CA_PUBLIC, 10000 iteration cap
- Uses clock_gettime(CLOCK_MONOTONIC) / QueryPerformanceCounter
- Returns elapsed seconds as floating point
Also fixes: add unicode_tables.c to libmux.so LIBMUX_C_SRC (resolves
pre-existing tr_tolower_sbt etc. link errors).
505/505 smoke tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5-field Unix cron syntax (minute hour dom month dow) with ranges,
lists, steps, and month/day-of-week names. Computed next-fire-time
scheduling integrates with CScheduler — no polling loop. In-memory
storage, recreated via @startup. DOM/DOW OR-semantics per Vixie cron.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>