Commit graph

18 commits

Author SHA1 Message Date
Stephen Dennis
63a09f4f49 perf(engine): index exact names per container (#2058)
match_list() compared the query against every object in scope and could not
stop at the first hit, because ambiguity has to be detected.  Name resolution
was 66.8% of attrread and 74.8% of attrwrite once the storage costs were out
of the attribute path (#2073, #2084).

THE INDEX IS A FILTER, NOT THE DECIDER.  string_compare() folds Unicode case
through mux_tolower(), whose output is not a byte-for-byte map; reimplementing
that to build a hash key would be an approximation that silently disagrees
with the walk on some name.  So the bucket key is an ASCII normalisation only,
every candidate it yields is still confirmed with the real string_compare(),
and any name carrying a byte >= 0x80 is not bucketed -- it goes on a must-scan
list that suppresses the short-circuit for that container.  A wrong key costs
a fallback, never a wrong answer.

Skipping the remaining walk on a hit is sound because an exact match scores
CON_COMPLETE|CON_LOCAL (0x09 minimum) while a partial tops out at
CON_LOCAL|CON_TYPE|CON_LOCK (0x07): exact always outranks partial.

Only used when md.check_keys is false.  With it set, promote_match() evaluates
could_doit() per candidate -- softcode -- so skipping the walk would skip those
evaluations.  Exit and movement matching, the only check_keys users, keep the
existing behaviour exactly.

INVALIDATION.  s_Location/s_Contents/s_Next/s_Name carry hooks, which covers
the 45 call sites that mutate contents lists through them.  Three sites bypass
the setters and write the fields directly; grepping for those found them, and
they invalidate too:

  * db.cpp objref() fixup loop and the SQLite load path -- whole-index clear.
  * object.cpp force_reclaim_failed_create_slot(), which turns an object into
    GARBAGE while clearing its fields.  That one is in normal operation and
    would have left a container's index naming a destroyed object.

MEASURED.  Per lookup at 1000 objects, the tax that scaled with scope is gone
-- name lookup now costs what a dbref lookup costs:

  lookup            master     with index
  by dbref (ctrl)    26.7us       30.0us
  name at head       53.3us       26.7us
  name at middle     60.0us       23.3us
  name at tail       66.7us       26.7us

Per command, two runs, with three non-resolving controls flat:

  phase            before    run1    run2
  attrwrite         143us    46us    41us
  attrread          160us    51us    55us
  dispatch (ctrl)    31us    30us    32us
  softcode_arith     27us    28us    26us

Behaviour verified identical to master across fifteen cases: exact match,
case-insensitivity, partial-match fallthrough, space compression, true
ambiguity (two objects sharing a name still give #-2), rename, movement and
destruction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:04:37 -06:00
Stephen Dennis
bc4cec252e fix(nls): plural morphology baked into msgids, and a fragment that could not be translated (#1717)
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>
2026-07-28 21:31:32 +00:00
Stephen Dennis
8c466171c5 nls: mark object/flags/boolexp create-and-flag notify prose with M_()
Phase 3 coverage slice. Player and staff notifies that still used T()
(cast-only) now use M_() so they extract into the catalogue:

  object  name taken/silly, deposit refund, @chown summary, parent/zone/
          home/dropto clears, floating room
  flags   Flags: header, type/flag parse errors, @set decompile, flag
          name removed
  boolexp match “don’t see / which”
  wiz     password changed by %s
  player  @protect all listing line

Left alone: log messages, HTML, softcode/machine tokens, concatenated
fragments (set @chown owner line, rob give pieces), and punctuation
separators. pot 704 -> 722; xx filled; ko msgmerge only.
2026-07-28 13:06:50 +00:00
Stephen Dennis
301a7664d0 nls: mark db/cron/powers/session misc notify prose with M_ (#1419)
Remaining small engine notify piles (~45 sites across db, cron, powers,
session, object, conf, help, log). Leave tprintf formats as T().
Half-mark pass clean. Regenerate pot/xx without fuzzy entries.
2026-07-27 11:50:31 +00:00
Stephen Dennis
b5ee3b6f81 refactor: use UTF-8 text in user-facing string literals (#1513)
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.
2026-07-26 21:52:48 -06:00
Stephen Dennis
e7eb6ec76d nls: route literal #-1 softcode tokens through S_ (#1475)
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.
2026-07-27 01:00:55 +00:00
Stephen Dennis
2f106f200f fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373)
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>
2026-07-26 10:12:35 -06:00
Stephen Dennis
c33fa62213 fix(object): harden create freelist, purge_going, and reverse @open
- #1185: zero Powers/Powers2 on create_obj; require zero powers in IS_CLEAN
  so freelist recycle cannot inherit privilege from corrupt garbage slots
- #1183: validate A_DESTROYER with Good_owner before destroy_player/chown_all;
  fall back to GOD and log when the attribute is stale or corrupt
- #1188: refuse reverse @open when the destination is HOME with an explicit
  message instead of a silent open_exit bail
2026-07-25 11:09:32 -06:00
Stephen Dennis
553909fea2
Merge pull request #1209 from brazilofmux/fix/1180-pcache-destroy
fix(player): drop pcache entry on destroy to avoid recycle leak (#1180)
2026-07-25 08:29:26 -06:00
Stephen Dennis
f0230a66e4 fix(player): drop pcache entry on destroy to avoid recycle leak (#1180)
Add pcache_delete() and call it from destroy_obj/destroy_bad_obj before
TYPE_GARBAGE so freelist reuse cannot inherit a prior player's QueueMax
or residual queue depth.
2026-07-25 08:12:46 -06:00
Stephen Dennis
60a19670dd fix(db): never nuclear-clear SQLite after create-obj reconcile fail (#1179)
Remove sqlite_clear_after_reconcile_failure, which wiped all attributes,
objects, and meta (db_top=0) when a failed create_obj cleanup could not
resync. Log loudly and leave SQLite unchanged so RAM/backend divergence
does not become mass data loss on restart.
2026-07-25 08:09:55 -06:00
Stephen Dennis
900e3e3175 Convert ~75 atr_get/atr_pget sites to LBuf_Adopt across 17 files
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>
2026-04-05 18:34:51 -06:00
Stephen Dennis
87485bc401 Migrate 22 alloc_lbuf/free_lbuf sites to LBuf RAII
Replace manual alloc_lbuf/free_lbuf pairs with LBuf RAII wrappers
in 12 engine source files: rob, walk, quota, wiz, session, object,
log, match, move, create, flags, boolexp.  This eliminates ~40
explicit free_lbuf calls on error paths that are now handled by
destructors, removing leak risk in early-return and multi-exit
functions (boolexp alone had 13 exit-path frees across two
functions).  Buffers returned by atr_get/atr_pget or to callers
are left manual since LBuf cannot adopt externally-allocated
buffers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:46:22 -06:00
Stephen Dennis
a57b039e78 Fix bulk deletion mod_count: two-phase transactional invalidation
The previous sentinel-based approach (UINT32_MAX) wrapped to 0 on
the next write, breaking monotonicity.  The before-transaction
eager mutation approach diverged on rollback.

New design: two-phase collect/apply.

Phase 1 (before transaction): attr_mod_count_collect_object()
reads all attrs from SQLite and the in-memory map, returning a
vector of {key, current_value} pairs.  No in-memory mutation.

Phase 2 (after successful commit only):
attr_mod_count_apply_increments() writes current_value+1 to the
in-memory map for each collected entry.

On rollback: no in-memory mutation occurred, so the map stays
consistent with SQLite.  Monotonicity is preserved because the
increment is always current+1, never a sentinel or reset.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:33:33 -06:00
Stephen Dennis
7d7ea2ea79 Fix bulk deletion mod_count: monotonic increment, no sentinel
The UINT32_MAX sentinel wrapped to 0 on next write, breaking
monotonicity. Replace with:

- attr_mod_count_invalidate_object() now seeds all attr counters
  from SQLite BEFORE deletion (via GetAllAttrModCounts), then
  increments each by 1. Counters are strictly monotonic — no
  sentinel, no wrapping, no reuse across delete/recreate cycles.

- Call sites in object.cpp moved BEFORE DelAllAttributes so the
  SQLite rows are still readable when seeding.

- ClearAttributes() clears the in-memory map entirely (correct
  for full database reload where JIT cache is also cleared).

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:25:50 -06:00
Stephen Dennis
0f3d233616 Fix mod_count failure-path divergence and bulk deletion bypass
Two bugs from code review:

1. attr_mod_count_inc ran before cache_put/cache_del, so failed
   writes bumped the in-memory counter without changing SQLite.
   Fix: increment AFTER successful storage. PutAttribute reads
   the current counter and passes counter+1 to the upsert
   independently.

2. DelAllAttributes and ClearAttributes bypassed mod_count tracking.
   Fix: attr_mod_count_invalidate_object() sets all entries for a
   destroyed object to UINT32_MAX (always stale). ClearAttributes()
   calls attr_mod_count_invalidate_all() to clear the entire map.

Smoke tests: 610/610 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:20:16 -06:00
Stephen Dennis
0e619af49a Move pISlaveControl from mudstate to driver-side g_pISlaveControl
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>
2026-03-18 01:49:16 +00:00
Stephen Dennis
4ff1398de1 Restructure mux/ directory: component-based layout with proper build root
Move from flat mux/src/ layout to clean component hierarchy:
- mux/ is now the autoconf/automake build root (configure.ac lives here)
- mux/include/ — shared headers used by multiple components
- mux/lib/ — libmux.so (core utilities, no game state)
- mux/src/ — netmux driver only (thin networking shell)
- mux/modules/engine/ — engine.so (game logic)
- mux/modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ — external modules
- mux/ganl/ — GANL networking library
- mux/sqlite/ — SQLite amalgamation (builds libsqlite3.a)
- mux/announce/ — announce tool (was mux/src/tools/)

Build changes:
- SUBDIRS ordering: ganl sqlite lib src modules announce
- libmux.so gets -Wl,-soname,libmux.so; netmux links via -L -lmux
- engine.so links libsqlite3.a and libmux.so with -Wl,--no-undefined
- RPATH uses $ORIGIN for portable .so resolution
- Install hooks use absolute paths for game/bin symlinks

Bug fixes:
- engine.so mux_Register() now passes nullptr to mux_RegisterClassObjects
  (matches all other modules; libmux already has the factory via dlsym)
- DbConvert() now calls pcache_init() before db_write, fixing a latent
  crash (free(): invalid pointer) when exporting from SQLite databases

411/411 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:38:37 -06:00
Renamed from mux/src/object.cpp (Browse further)