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>
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.
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>
Correctness sweep of the command-side verb handlers. Five confirmed bugs
plus a help-text correction, verified by dual-lens review and code reading
(the read-only smoke harness can't exercise these verbs directly):
- @clone/cost on an exit bypassed the "must control current location"
check (it lived only on the non-/cost path), letting a builder splice a
cloned exit into a room they do not control. (#855)
- The @mark/@mark_all/@apply_marked DB-cleaning refusal cited @unmark_all,
which does not exist (produces "Huh?"); corrected to @mark_all/clear.
(#856)
- @ps <object> reported nothing for a controlled object owned by another
player; do_ps was missing the non-player-target else clause that the
sibling @halt has (clear the owner filter). (#857)
- whisper "<quoted name>" skipped the locality/connected gate the unquoted
form applies, giving a success confirmation plus a delivery error and
polluting A_LASTWHISPER; also fixed an adjacent quoted-name continue that
did not advance the parser. (#858)
- @flag/remove of an unknown/empty flag name was silent; now reports an
error like the other flag-name failure paths. (#859)
- report help said 8-hour segments but the code uses 4 (deliberately, per
4a845139f); corrected the help. (#860)
Also restores the "## JIT / DBT Engine" CHANGES heading dropped during an
earlier 2.14.0.8 edit. Build clean, all 1264 smoke tests still pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
@dolist/now executed each iteration's body via a single process_command(),
so it could not split a ';'-separated body and never honored break_called.
Worse, running the body inline could mutate the executor's own attributes
and free the lbufs the 'list'/'command' arguments pointed into, corrupting
the walk so the loop stopped after the first iteration. Net effect: the
"@break stops the loop" idiom the assert_cmd tests wanted was impossible,
and @dolist/now over an accumulating body silently truncated.
do_dolist / bind_and_process now (for the /now path):
- iterate over private copies of the list and command so inline body
execution can't pull the rug out from under the walk,
- run the body as an action list (split on ';', honoring break_called),
mirroring do_include(), and
- stop the loop when @break/@assert fires, while saving/restoring
break_called so the break is contained to the dolist and does not leak
into the surrounding command list.
The queued (default) @dolist path is unchanged in behavior.
Adds assert_cmd TC005/006/007 to lock in the inline semantics: break
stops the loop (-> "a"), a non-firing loop records every element
(-> "a b c", the truncation regression guard), and an inline break stays
contained (a command after @dolist/now in the same list still runs).
Verified: full smoke 1081 ok / 0 new failures; standalone repros confirm
break->"a", no-break->"a b c", and outer-command-still-runs. Closes the
follow-up noted in #765.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Third wave: convert manual alloc/free pairs in walkdb (3), htab (3),
dbt_spike (1), plusemail (2), mguests (2), file_c (2), powers (1),
jit_compiler (2). Completes all convertible sites in the small and
medium engine source files.
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>