tinymux/mux/modules/engine/predicates.cpp

3243 lines
83 KiB
C++
Raw Permalink Normal View History

/*! \file predicates.cpp
* \brief Miscellaneous commands and functions.
*
* In theory, most of these functions could plausibly be called
* "predicates", either because they determine some boolean property
* of the input, or because they perform some action that makes them
* verb-like. In practice, this is a miscellany.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "mux_table.h"
extern "C" {
#include "color_ops.h"
}
static inline const UTF8 *utf8_advance_predicate(const UTF8 *p)
{
if (nullptr == p || '\0' == *p)
{
return p;
}
size_t n = utf8_FirstByte[static_cast<unsigned char>(*p)];
if (n < 1 || n >= UTF8_CONTINUE)
{
return p + 1;
}
for (size_t i = 1; i < n; i++)
{
if ( '\0' == p[i]
|| UTF8_CONTINUE != utf8_FirstByte[static_cast<unsigned char>(p[i])])
{
return p + 1;
}
}
return p + n;
}
#include "ganl_stub.h"
/* ---------------------------------------------------------------------------
* insert_first, remove_first: Insert or remove objects from lists.
*/
dbref insert_first(dbref head, dbref thing)
{
s_Next(thing, head);
return thing;
}
dbref remove_first(dbref head, dbref thing)
{
if (head == thing)
{
return Next(thing);
}
dbref prev;
DOLIST(prev, head)
{
if (Next(prev) == thing)
{
s_Next(prev, Next(thing));
return head;
}
}
return head;
}
/* ---------------------------------------------------------------------------
* reverse_list: Reverse the order of members in a list.
*/
dbref reverse_list(dbref list)
{
dbref newlist, rest;
newlist = NOTHING;
while (list != NOTHING)
{
rest = Next(list);
s_Next(list, newlist);
newlist = list;
list = rest;
}
return newlist;
}
/* ---------------------------------------------------------------------------
* member - indicate if thing is in list
*/
bool member(dbref thing, dbref list)
{
DOLIST(list, list)
{
if (list == thing)
{
return true;
}
}
return false;
}
bool could_doit(dbref player, dbref thing, int locknum)
{
if (thing == HOME)
{
return true;
}
// If nonplayer tries to get key, then no.
//
if ( !isPlayer(player)
&& Key(thing))
{
return false;
}
if (Pass_Locks(player))
{
return true;
}
dbref aowner;
int aflags;
LBuf key = LBuf_Adopt(atr_get("could_doit.134", thing, locknum, &aowner, &aflags));
bool doit = eval_boolexp_atr(player, thing, thing, key);
return doit;
}
bool can_see(dbref player, dbref thing, bool can_see_loc)
{
// Don't show if all the following apply: Sleeping players should not be
// seen. The thing is a disconnected player. The player is not a
// puppet.
//
if ( mudconf.dark_sleepers
&& isPlayer(thing)
&& !Connected(thing)
&& !Puppet(thing))
{
return false;
}
// You don't see yourself or exits.
//
if ( player == thing
|| isExit(thing))
{
return false;
}
2008-01-18 14:04:32 -08:00
// To be visible, light must come from either the location (can_see_loc)
// or the object itself (Light(thing)). This light is then blocked
// by the object itself being dark (it blocked its own light), by not
// passing the visibility lock, or by being in a different reality.
//
2008-01-18 14:04:32 -08:00
// The exception to the above is mudconf.see_own_dark which allows a
// myopic self-examination.
//
return ( ( ( can_see_loc
|| Light(thing))
&& !Dark(thing)
#ifdef REALITY_LVLS
2008-01-18 14:04:32 -08:00
&& IsReal(player, thing)
#endif // REALITY_LVLS
&& could_doit(player, thing, A_LVISIBLE))
2008-01-18 14:04:32 -08:00
|| ( mudconf.see_own_dark
&& MyopicExam(player, thing)));
}
fix(win32): clamp A_RQUOTA instead of narrowing it through mux_ltoa (#1408) add_quota passed mux_atoi64's int64_t straight into mux_ltoa, which takes long. A no-op on LP64; on LLP64 the high word was simply dropped. Measured on Win64, quotas enabled, one object refunding one quota: RQUOTA 8589934592 -> 1 (should be 8589934593) RQUOTA 4294967296 -> 1 (should be 4294967297) Both lose the entire high word, and both are correct on Linux, so the same database gives different quota values depending on the platform that last touched it. Clamped rather than widened, which is the decision the issue asked to have recorded. The quota domain is 32-bit by construction everywhere else: pay_quota, mung_quotas and do_quota all hold it in int, destroy_obj's refund is int, and both writers format into UTF8[I32BUF_SIZE]. Widening add_quota alone would make it the only writer able to store a value every other reader still truncates, turning a visible error into silent drift. The stored attribute is only text, and mux_atoi64 parses whatever is in it, so the clamp is where the domain gets enforced. Two helpers: quota_from_attr reads A_RQUOTA into the int the rest of the system expects, and quota_add sums without overflowing (signed overflow is UB, and INT32_MAX + refund was reachable). pay_quota is fixed the same way. It had "int quota = mux_atoi64(...)", which truncates on *every* platform, and I hit it while reproducing this: a stored 8589934592 silently became -1 there before add_quota ever ran. That is beyond the issue's literal scope, but it is the same defect in the same code path, and leaving it means the clamp only half holds. Happy to split it out if preferred. After the fix, on Win64: 8589934592 -> 2147483647 clamped, deliberate, platform-independent 4294967296 -> 2147483647 42 -> 43 ordinary values unaffected -3 -> -2 negatives still work 2147483647 -> 2147483647 saturates instead of overflowing Because it clamps, behaviour is now identical on LP64 and LLP64. No smoke test, deliberately. I wrote one and it broke four other cases: A_RQUOTA is AF_GOD so only #1 can plant the value, but inside the harness "me" is the test object, so the set silently no-ops; and reaching add_quota needs mudconf.quotas plus a nonzero thing_quota plus a @dbck, all of which are server-wide. quotas=1 made permission_paths' object creation fail and the @dbck perturbed the dolist queue timing. A case that breaks four others is worth less than none -- #1391's lesson. The verification above is by hand on Win64 instead. Smoke: 314 dispatched, 1471 succeeded, 17 failed -- the known build-configuration failures on this box (exp3, UNIX_DIGEST, REALITY_LVLS), unchanged from master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:26:57 -06:00
// Read A_RQUOTA as the 32-bit quantity the rest of the quota system treats
// it as.
//
// The quota domain is 32-bit by construction everywhere else: pay_quota and
// mung_quotas hold it in int, do_quota parses into int, destroy_obj's refund
// is int, and both writers format into a UTF8[I32BUF_SIZE]. But the stored
// attribute is just text, and mux_atoi64 will parse whatever is in it -- so a
// corrupted or hand-crafted A_RQUOTA delivers a value no caller can represent.
//
// Widening the callers instead was the alternative, and is wrong: it would
// make one writer able to store a value every other reader still truncates,
// which converts a visible error into silent drift. Clamping keeps the
// domain honest and identical on LP64 and LLP64 (#1408).
//
static int quota_from_attr(const UTF8 *pQuota)
{
int64_t q = mux_atoi64(pQuota);
if (q > INT32_MAX)
{
return INT32_MAX;
}
if (q < INT32_MIN)
{
return INT32_MIN;
}
return static_cast<int>(q);
}
// Add two quota quantities without overflowing int. Both operands are
// already in range; their sum need not be.
//
static int quota_add(int a, int b)
{
int64_t sum = static_cast<int64_t>(a) + static_cast<int64_t>(b);
if (sum > INT32_MAX)
{
return INT32_MAX;
}
if (sum < INT32_MIN)
{
return INT32_MIN;
}
return static_cast<int>(sum);
}
static bool pay_quota(dbref who, int cost)
{
// If no cost, succeed
//
if (cost <= 0)
{
return true;
}
// determine quota
//
dbref aowner;
int aflags;
LBuf quota_str = LBuf_Adopt(atr_get("pay_quota.200", Owner(who), A_RQUOTA, &aowner, &aflags));
fix(win32): clamp A_RQUOTA instead of narrowing it through mux_ltoa (#1408) add_quota passed mux_atoi64's int64_t straight into mux_ltoa, which takes long. A no-op on LP64; on LLP64 the high word was simply dropped. Measured on Win64, quotas enabled, one object refunding one quota: RQUOTA 8589934592 -> 1 (should be 8589934593) RQUOTA 4294967296 -> 1 (should be 4294967297) Both lose the entire high word, and both are correct on Linux, so the same database gives different quota values depending on the platform that last touched it. Clamped rather than widened, which is the decision the issue asked to have recorded. The quota domain is 32-bit by construction everywhere else: pay_quota, mung_quotas and do_quota all hold it in int, destroy_obj's refund is int, and both writers format into UTF8[I32BUF_SIZE]. Widening add_quota alone would make it the only writer able to store a value every other reader still truncates, turning a visible error into silent drift. The stored attribute is only text, and mux_atoi64 parses whatever is in it, so the clamp is where the domain gets enforced. Two helpers: quota_from_attr reads A_RQUOTA into the int the rest of the system expects, and quota_add sums without overflowing (signed overflow is UB, and INT32_MAX + refund was reachable). pay_quota is fixed the same way. It had "int quota = mux_atoi64(...)", which truncates on *every* platform, and I hit it while reproducing this: a stored 8589934592 silently became -1 there before add_quota ever ran. That is beyond the issue's literal scope, but it is the same defect in the same code path, and leaving it means the clamp only half holds. Happy to split it out if preferred. After the fix, on Win64: 8589934592 -> 2147483647 clamped, deliberate, platform-independent 4294967296 -> 2147483647 42 -> 43 ordinary values unaffected -3 -> -2 negatives still work 2147483647 -> 2147483647 saturates instead of overflowing Because it clamps, behaviour is now identical on LP64 and LLP64. No smoke test, deliberately. I wrote one and it broke four other cases: A_RQUOTA is AF_GOD so only #1 can plant the value, but inside the harness "me" is the test object, so the set silently no-ops; and reaching add_quota needs mudconf.quotas plus a nonzero thing_quota plus a @dbck, all of which are server-wide. quotas=1 made permission_paths' object creation fail and the @dbck perturbed the dolist queue timing. A case that breaks four others is worth less than none -- #1391's lesson. The verification above is by hand on Win64 instead. Smoke: 314 dispatched, 1471 succeeded, 17 failed -- the known build-configuration failures on this box (exp3, UNIX_DIGEST, REALITY_LVLS), unchanged from master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:26:57 -06:00
int quota = quota_from_attr(quota_str);
// enough to build? Wizards always have enough.
//
fix(win32): clamp A_RQUOTA instead of narrowing it through mux_ltoa (#1408) add_quota passed mux_atoi64's int64_t straight into mux_ltoa, which takes long. A no-op on LP64; on LLP64 the high word was simply dropped. Measured on Win64, quotas enabled, one object refunding one quota: RQUOTA 8589934592 -> 1 (should be 8589934593) RQUOTA 4294967296 -> 1 (should be 4294967297) Both lose the entire high word, and both are correct on Linux, so the same database gives different quota values depending on the platform that last touched it. Clamped rather than widened, which is the decision the issue asked to have recorded. The quota domain is 32-bit by construction everywhere else: pay_quota, mung_quotas and do_quota all hold it in int, destroy_obj's refund is int, and both writers format into UTF8[I32BUF_SIZE]. Widening add_quota alone would make it the only writer able to store a value every other reader still truncates, turning a visible error into silent drift. The stored attribute is only text, and mux_atoi64 parses whatever is in it, so the clamp is where the domain gets enforced. Two helpers: quota_from_attr reads A_RQUOTA into the int the rest of the system expects, and quota_add sums without overflowing (signed overflow is UB, and INT32_MAX + refund was reachable). pay_quota is fixed the same way. It had "int quota = mux_atoi64(...)", which truncates on *every* platform, and I hit it while reproducing this: a stored 8589934592 silently became -1 there before add_quota ever ran. That is beyond the issue's literal scope, but it is the same defect in the same code path, and leaving it means the clamp only half holds. Happy to split it out if preferred. After the fix, on Win64: 8589934592 -> 2147483647 clamped, deliberate, platform-independent 4294967296 -> 2147483647 42 -> 43 ordinary values unaffected -3 -> -2 negatives still work 2147483647 -> 2147483647 saturates instead of overflowing Because it clamps, behaviour is now identical on LP64 and LLP64. No smoke test, deliberately. I wrote one and it broke four other cases: A_RQUOTA is AF_GOD so only #1 can plant the value, but inside the harness "me" is the test object, so the set silently no-ops; and reaching add_quota needs mudconf.quotas plus a nonzero thing_quota plus a @dbck, all of which are server-wide. quotas=1 made permission_paths' object creation fail and the @dbck perturbed the dolist queue timing. A case that breaks four others is worth less than none -- #1391's lesson. The verification above is by hand on Win64 instead. Smoke: 314 dispatched, 1471 succeeded, 17 failed -- the known build-configuration failures on this box (exp3, UNIX_DIGEST, REALITY_LVLS), unchanged from master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:26:57 -06:00
quota = quota_add(quota, -cost);
if ( quota < 0
&& !Free_Quota(who)
&& !Free_Quota(Owner(who)))
{
return false;
}
// Dock the quota.
//
UTF8 buf[I32BUF_SIZE];
mux_ltoa(quota, buf);
atr_add_raw(Owner(who), A_RQUOTA, buf);
return true;
}
bool canpayfees(dbref player, dbref who, int pennies, int quota)
{
if ( !Wizard(who)
&& !Wizard(Owner(who))
&& !Free_Money(who)
&& !Free_Money(Owner(who))
&& (Pennies(Owner(who)) < pennies))
{
if (player == who)
{
notify(player, tprintf(M_("Sorry, you dont have enough %s."),
mudconf.many_coins));
}
else
{
notify(player, tprintf(M_("Sorry, that player doesnt have enough %s."),
mudconf.many_coins));
}
return false;
}
if (mudconf.quotas)
{
if (!pay_quota(who, quota))
{
if (player == who)
{
notify(player, M_("Sorry, your building contract has run out."));
}
else
{
notify(player,
M_("Sorry, that players building contract has run out."));
}
return false;
}
}
payfor(who, pennies);
return true;
}
bool payfor(dbref who, int cost)
{
if ( Wizard(who)
|| Wizard(Owner(who))
|| Free_Money(who)
|| Free_Money(Owner(who)))
{
return true;
}
who = Owner(who);
int tmp;
if ((tmp = Pennies(who)) >= cost)
{
s_Pennies(who, tmp - cost);
return true;
}
return false;
}
void add_quota(dbref who, int payment)
{
dbref aowner;
int aflags;
UTF8 buf[I32BUF_SIZE];
LBuf quota = LBuf_Adopt(atr_get("add_quota.288", who, A_RQUOTA, &aowner, &aflags));
fix(win32): clamp A_RQUOTA instead of narrowing it through mux_ltoa (#1408) add_quota passed mux_atoi64's int64_t straight into mux_ltoa, which takes long. A no-op on LP64; on LLP64 the high word was simply dropped. Measured on Win64, quotas enabled, one object refunding one quota: RQUOTA 8589934592 -> 1 (should be 8589934593) RQUOTA 4294967296 -> 1 (should be 4294967297) Both lose the entire high word, and both are correct on Linux, so the same database gives different quota values depending on the platform that last touched it. Clamped rather than widened, which is the decision the issue asked to have recorded. The quota domain is 32-bit by construction everywhere else: pay_quota, mung_quotas and do_quota all hold it in int, destroy_obj's refund is int, and both writers format into UTF8[I32BUF_SIZE]. Widening add_quota alone would make it the only writer able to store a value every other reader still truncates, turning a visible error into silent drift. The stored attribute is only text, and mux_atoi64 parses whatever is in it, so the clamp is where the domain gets enforced. Two helpers: quota_from_attr reads A_RQUOTA into the int the rest of the system expects, and quota_add sums without overflowing (signed overflow is UB, and INT32_MAX + refund was reachable). pay_quota is fixed the same way. It had "int quota = mux_atoi64(...)", which truncates on *every* platform, and I hit it while reproducing this: a stored 8589934592 silently became -1 there before add_quota ever ran. That is beyond the issue's literal scope, but it is the same defect in the same code path, and leaving it means the clamp only half holds. Happy to split it out if preferred. After the fix, on Win64: 8589934592 -> 2147483647 clamped, deliberate, platform-independent 4294967296 -> 2147483647 42 -> 43 ordinary values unaffected -3 -> -2 negatives still work 2147483647 -> 2147483647 saturates instead of overflowing Because it clamps, behaviour is now identical on LP64 and LLP64. No smoke test, deliberately. I wrote one and it broke four other cases: A_RQUOTA is AF_GOD so only #1 can plant the value, but inside the harness "me" is the test object, so the set silently no-ops; and reaching add_quota needs mudconf.quotas plus a nonzero thing_quota plus a @dbck, all of which are server-wide. quotas=1 made permission_paths' object creation fail and the @dbck perturbed the dolist queue timing. A case that breaks four others is worth less than none -- #1391's lesson. The verification above is by hand on Win64 instead. Smoke: 314 dispatched, 1471 succeeded, 17 failed -- the known build-configuration failures on this box (exp3, UNIX_DIGEST, REALITY_LVLS), unchanged from master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:26:57 -06:00
// mux_ltoa takes long, so passing the int64_t from mux_atoi64 directly
// narrowed it -- a no-op on LP64, but on LLP64 the high word was dropped
// and a stored 8589934592 came back as 1 (#1408).
//
mux_ltoa(quota_add(quota_from_attr(quota), payment), buf);
atr_add_raw(who, A_RQUOTA, buf);
}
void giveto(dbref who, int pennies)
{
if ( Wizard(who)
|| Wizard(Owner(who))
|| Free_Money(who)
|| Free_Money(Owner(who)))
{
return;
}
who = Owner(who);
s_Pennies(who, Pennies(who) + pennies);
}
// Every character in the name must be allowed by one of the character sets mentioned.
// If no character sets are mentions, everything is allowed.
//
bool IsRestricted(const UTF8 *pName, int charset)
{
if (0 == charset)
{
return false;
}
while ('\0' != pName[0])
{
bool bAllowed = false;
if ( (ALLOW_CHARSET_ASCII & charset)
&& (0x80 & pName[0]) == 0)
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_8859_1 & charset)
&& mux_is8859_1(pName))
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_8859_2 & charset)
&& mux_is8859_2(pName))
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_HANGUL & charset)
&& mux_ishangul(pName))
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_HIRAGANA & charset)
&& mux_ishiragana(pName))
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_KANJI & charset)
&& mux_iskanji(pName))
{
bAllowed = true;
}
else if ( (ALLOW_CHARSET_KATAKANA & charset)
&& mux_iskatakana(pName))
{
bAllowed = true;
}
if (!bAllowed)
{
return true;
}
pName = utf8_advance_predicate(pName);
}
return false;
}
// The following function validates that the object names (which will be
// used for things and rooms, but not for players or exits) and generates
// a canonical form of that name (with optimized ANSI).
//
UTF8 *MakeCanonicalObjectName(const UTF8 *pName, size_t *pnName, bool *pbValid, int charset)
{
Convert all static scratch buffers to thread_local 43 static scratch-buffer arrays across 21 files (5 in mux/src, 38 in mux/modules/) changed from `static` to `thread_local`. Under the current single-threaded evaluator this is a zero-behavior-change swap — `thread_local` storage has the same lifetime and zero-allocation properties as `static` — but each thread gets its own copy, which makes these functions safe for a future multi-threaded evaluator without any locking. Read-only constant tables (`aRadix64`, `aRadixPenn36`, `aRadixPenn64`, `Empty`) left as `static` because they are immutable shared data. Affected areas: net.cpp — queue_string co_buf, trimmed_site, dump_users NameField signals.cpp — signal_desc stubslave.cpp — Stub_PipePump attrcache.cpp — sqlite_attr_buf boolexp.cpp — parsestore command.cpp — preserve_cmd, SpaceCompressCommand, LowerCaseCommand comsys.cpp — NewTitle, Buffer, temp db.cpp — tbuff, Buffer (x2) flags.cpp — buff funceval.cpp — textbuff functions.cpp — TimeBuffer64, TimeBuffer80, Buffer help.cpp — Line, Buffer mail.cpp — aFolders, Buffer, res, szFittedMailAliasDesc match.cpp — buffer player.cpp — szSalt, buf (x2), buff plusemail.cpp — buf predicates.cpp — Buf (x2), pName session.cpp — szFittedDoing set.cpp — pRestrictedKeyText unparse.cpp — buf, boolexp_buf mail_mod.cpp — result, res, buf All 21 files verified with g++ -std=c++17 -fsyntax-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:21:14 -06:00
thread_local UTF8 Buf[MBUF_SIZE];
*pnName = 0;
*pbValid = false;
if (!pName)
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Build up what the real name would be. If we pass all the
// checks, this is what we will return as a result.
//
mux_field fldLen = StripTabsAndTruncate(pName, Buf, MBUF_SIZE-1, MBUF_SIZE-1);
// Disallow pure ANSI names. There must be at least -something-
// visible.
//
if (0 == fldLen.m_column)
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Get the stripped version (Visible parts without color info).
//
size_t nStripped;
const UTF8 *pStripped = strip_color(Buf, &nStripped);
// Do not allow LOOKUP_TOKEN, NUMBER_TOKEN, NOT_TOKEN, or SPACE
// as the first character, or SPACE as the last character
//
if ( reinterpret_cast<const UTF8 *>(strchr(reinterpret_cast<const char *>("*!#"), pStripped[0]))
|| mux_isspace(pStripped[0])
|| mux_isspace(pStripped[nStripped-1]))
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Only printable characters besides ARG_DELIMITER, AND_TOKEN,
// and OR_TOKEN are allowed.
//
const UTF8 *p = pStripped;
while ('\0' != *p)
{
if (!mux_isobjectname(p))
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
p = utf8_advance_predicate(p);
}
// Special names are specifically dis-allowed.
//
if ( (nStripped == 2 && memcmp("me", pStripped, 2) == 0)
|| (nStripped == 4 && ( memcmp("home", pStripped, 4) == 0
|| memcmp("here", pStripped, 4) == 0)))
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
if (IsRestricted(pStripped, charset))
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
// Reject names that would be silently truncated.
//
size_t nOrigStripped;
strip_color(pName, &nOrigStripped);
if (nOrigStripped > nStripped)
{
return nullptr;
}
*pnName = fldLen.m_byte;
*pbValid = true;
return Buf;
}
// The following function validates exit names.
//
UTF8 *MakeCanonicalExitName(const UTF8 *pName, size_t *pnName, bool *pbValid)
{
Convert all static scratch buffers to thread_local 43 static scratch-buffer arrays across 21 files (5 in mux/src, 38 in mux/modules/) changed from `static` to `thread_local`. Under the current single-threaded evaluator this is a zero-behavior-change swap — `thread_local` storage has the same lifetime and zero-allocation properties as `static` — but each thread gets its own copy, which makes these functions safe for a future multi-threaded evaluator without any locking. Read-only constant tables (`aRadix64`, `aRadixPenn36`, `aRadixPenn64`, `Empty`) left as `static` because they are immutable shared data. Affected areas: net.cpp — queue_string co_buf, trimmed_site, dump_users NameField signals.cpp — signal_desc stubslave.cpp — Stub_PipePump attrcache.cpp — sqlite_attr_buf boolexp.cpp — parsestore command.cpp — preserve_cmd, SpaceCompressCommand, LowerCaseCommand comsys.cpp — NewTitle, Buffer, temp db.cpp — tbuff, Buffer (x2) flags.cpp — buff funceval.cpp — textbuff functions.cpp — TimeBuffer64, TimeBuffer80, Buffer help.cpp — Line, Buffer mail.cpp — aFolders, Buffer, res, szFittedMailAliasDesc match.cpp — buffer player.cpp — szSalt, buf (x2), buff plusemail.cpp — buf predicates.cpp — Buf (x2), pName session.cpp — szFittedDoing set.cpp — pRestrictedKeyText unparse.cpp — buf, boolexp_buf mail_mod.cpp — result, res, buf All 21 files verified with g++ -std=c++17 -fsyntax-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:21:14 -06:00
thread_local UTF8 Buf[MBUF_SIZE];
*pnName = 0;
*pbValid = false;
if (!pName)
{
2018-10-03 17:54:51 +00:00
return nullptr;
}
mux_strncpy(Buf, pName, MBUF_SIZE - 1);
// Sanitize the input before processing.
//
string_token st(Buf, T(";"));
// Break the exitname down into semi-colon-separated segments. The first
// segment can contain color as it is used for showing the exit, but the
// remaining segments are stripped of color. A valid exitname requires
// at least one (display) segment.
//
UTF8 *ptr;
UTF8 clean_buf[MBUF_SIZE];
UTF8 *bp_clean = clean_buf;
bool bHaveDisplay = false;
for (ptr = st.parse(); ptr; ptr = st.parse())
{
2018-10-03 17:54:51 +00:00
UTF8 *pTrimmedSegment = nullptr;
if (bHaveDisplay)
{
// No color allowed in segments after the first one.
//
UTF8 *pNoColor = strip_color(ptr);
pTrimmedSegment = trim_spaces(pNoColor);
}
else
{
// Color allowed in first segment.
//
pTrimmedSegment = trim_spaces(ptr);
}
// Ignore segments which contained nothing but spaces.
//
if ('\0' != pTrimmedSegment[0])
{
bool valid = false;
size_t len = 0;
UTF8 *pValidSegment = MakeCanonicalObjectName(pTrimmedSegment, &len, &valid, mudconf.exit_name_charset);
if (valid)
{
if (bHaveDisplay)
{
safe_mb_chr_ascii(';', clean_buf, &bp_clean);
safe_mb_str(pValidSegment, clean_buf, &bp_clean);
}
else
{
safe_mb_str(pValidSegment, clean_buf, &bp_clean);
bHaveDisplay = true;
}
}
}
free_lbuf(pTrimmedSegment);
2018-10-03 17:54:51 +00:00
pTrimmedSegment = nullptr;
}
*bp_clean = '\0';
*pbValid = bHaveDisplay;
if (!bHaveDisplay)
{
*pnName = 0;
return Buf;
}
mux_strncpy(Buf, clean_buf, MBUF_SIZE - 1);
*pnName = mux_strlen(Buf);
return Buf;
}
// The following function validates the player name. ANSI is not
// allowed in player names. However, a player name must satisfy
// the requirements of a regular name as well.
//
bool ValidatePlayerName(const UTF8 *pName)
{
if (!pName)
{
return false;
}
size_t nName = strlen(reinterpret_cast<const char *>(pName));
// Verify that name is not empty, but not too long, either.
//
if ( nName <= 0
|| PLAYER_NAME_LIMIT <= nName)
{
return false;
}
// Do not allow LOOKUP_TOKEN, NUMBER_TOKEN, NOT_TOKEN, or SPACE
// as the first character, or SPACE as the last character
//
if ( reinterpret_cast<const UTF8 *>(strchr(reinterpret_cast<const char *>("*!#"), pName[0]))
|| mux_isspace(pName[0])
|| mux_isspace(pName[nName-1]))
{
return false;
}
// Only printable characters besides ARG_DELIMITER, AND_TOKEN,
// and OR_TOKEN are allowed.
//
if ( mudstate.bStandAlone
|| mudconf.name_spaces)
{
const UTF8 *p = pName;
while ('\0' != *p)
{
if ( !mux_isplayername(p)
&& ' ' != *p)
{
return false;
}
p = utf8_advance_predicate(p);
}
}
else
{
const UTF8 *p = pName;
while ('\0' != *p)
{
if (!mux_isplayername(p))
{
return false;
}
p = utf8_advance_predicate(p);
}
}
// Special names are specifically dis-allowed.
//
if ( (nName == 2 && memcmp("me", pName, 2) == 0)
|| (nName == 4 && ( memcmp("home", pName, 4) == 0
|| memcmp("here", pName, 4) == 0)))
{
return false;
}
if (IsRestricted(pName, mudconf.player_name_charset))
{
return false;
}
return true;
}
bool ok_password(const UTF8 *password, const UTF8 **pmsg)
{
2018-10-03 17:54:51 +00:00
*pmsg = nullptr;
if (*password == '\0')
{
*pmsg = T("Null passwords are not allowed.");
return false;
}
int num_upper = 0;
int num_special = 0;
int num_lower = 0;
const UTF8 *scan = password;
for ( ; *scan; scan = utf8_advance_predicate(scan))
{
if ( !mux_isprint(scan)
|| mux_isspace(*scan))
{
*pmsg = T("Illegal character in password.");
return false;
}
if (mux_isupper_ascii(*scan))
{
num_upper++;
}
else if (mux_islower_ascii(*scan))
{
num_lower++;
}
else if ( *scan != '\''
&& *scan != '-')
{
num_special++;
}
}
if ( !mudstate.bStandAlone
&& mudconf.safer_passwords)
{
if (num_upper < 1)
{
*pmsg = T("The password must contain at least one capital letter.");
return false;
}
if (num_lower < 1)
{
*pmsg = T("The password must contain at least one lowercase letter.");
return false;
}
if (num_special < 1)
{
*pmsg = T("The password must contain at least one number or a symbol other than the apostrophe or dash.");
return false;
}
}
return true;
}
/* ---------------------------------------------------------------------------
* handle_ears: Generate the 'grows ears' and 'loses ears' messages.
*/
void handle_ears(dbref thing, bool could_hear, bool can_hear)
{
if (could_hear != can_hear)
{
LBuf buf = LBuf_Src("handle_ears");
UTF8 *bp = buf.get();
// Moniker returns PUA-encoded name.
//
const UTF8 *name = Moniker(thing);
if (isExit(thing))
{
// Truncate at first semicolon.
//
const UTF8 *semi = (const UTF8 *)strchr((const char *)name, ';');
if (semi)
{
safe_copy_buf(name, semi - name, buf, &bp);
}
else
{
safe_str(name, buf, &bp);
}
}
else
{
safe_str(name, buf, &bp);
}
const PRONOUN_SET *pg = get_pronoun_set(thing);
if (can_hear)
{
safe_tprintf_str(buf, &bp, T(" grow%s ears and can now hear."),
pg->plural ? "" : "s");
}
else
{
safe_tprintf_str(buf, &bp, T(" lose%s %s ears and become%s deaf."),
pg->plural ? "" : "s", pg->possessive,
pg->plural ? "" : "s");
}
*bp = '\0';
notify_check(thing, thing, buf, MSG_ME | MSG_NBR | MSG_LOC | MSG_INV);
}
}
// For lack of better place the @switch code is here.
//
void do_switch
(
dbref executor, dbref caller, dbref enactor,
int eval, int key,
UTF8 *expr,
UTF8 *args[], int nargs,
const UTF8 *cargs[], int ncargs
)
{
if ( !expr
|| nargs <= 0)
{
return;
}
bool bMatchOne;
switch (key & SWITCH_MASK)
{
case SWITCH_DEFAULT:
if (mudconf.switch_df_all)
{
bMatchOne = false;
}
else
{
bMatchOne = true;
}
break;
case SWITCH_ANY:
bMatchOne = false;
break;
case SWITCH_ONE:
default:
bMatchOne = true;
break;
}
// Now try a wild card match of buff with stuff in coms.
//
bool bAny = false;
int a;
LBuf buff = LBuf_Src("do_switch");
UTF8 *bp = buff.get();
CLinearTimeAbsolute lta;
for ( a = 0;
( !bMatchOne
|| !bAny)
&& a < nargs - 1
&& args[a]
&& args[a + 1];
a += 2)
{
bp = buff;
mux_exec(args[a], LBUF_SIZE-1, buff, &bp, executor, caller, enactor, eval|EV_FCHECK|EV_EVAL|EV_TOP,
cargs, ncargs);
*bp = '\0';
if (wild_match(buff, expr))
{
const UTF8 *save_switch = mudstate.switch_token;
mudstate.switch_token = expr;
if (key & SWITCH_NOW)
{
process_command(executor, caller, enactor, eval, false, args[a+1], cargs, ncargs);
}
else
{
{
// Preserve enclosing iter context (## from @dolist)
// so it survives into the queued @switch body.
// Respect safer_iter — same guard as @dolist uses.
//
const UTF8 *iter_tok = nullptr;
int iter_num = 0;
if (!mudconf.safer_iter)
{
int iLoop = mudstate.in_loop - 1;
if ( 0 <= iLoop
&& iLoop < MAX_ITEXT
&& mudstate.itext[iLoop])
{
iter_tok = mudstate.itext[iLoop];
iter_num = mudstate.inum[iLoop];
}
}
wait_que(executor, caller, enactor, eval, false, lta, NOTHING, 0,
args[a+1],
ncargs, cargs,
mudstate.global_regs,
nullptr, // named_sargs
iter_tok, iter_num, // iter context
expr); // switch_token
}
}
mudstate.switch_token = save_switch;
bAny = true;
}
}
if ( a < nargs
&& !bAny
&& args[a])
{
const UTF8 *save_switch = mudstate.switch_token;
mudstate.switch_token = expr;
if (key & SWITCH_NOW)
{
process_command(executor, caller, enactor, eval, false, args[a], cargs, ncargs);
}
else
{
{
const UTF8 *iter_tok = nullptr;
int iter_num = 0;
if (!mudconf.safer_iter)
{
int iLoop = mudstate.in_loop - 1;
if ( 0 <= iLoop
&& iLoop < MAX_ITEXT
&& mudstate.itext[iLoop])
{
iter_tok = mudstate.itext[iLoop];
iter_num = mudstate.inum[iLoop];
}
}
wait_que(executor, caller, enactor, eval, false, lta, NOTHING, 0,
args[a],
ncargs, cargs,
mudstate.global_regs,
nullptr, // named_sargs
iter_tok, iter_num, // iter context
expr); // switch_token
}
}
mudstate.switch_token = save_switch;
}
if (key & SWITCH_NOTIFY)
{
LBuf tbuf = LBuf_Src("switch.notify_cmd");
mux_strncpy(tbuf, T("@notify/quiet me"), LBUF_SIZE-1);
wait_que(executor, caller, enactor, eval, false, lta, NOTHING, A_SEMAPHORE,
tbuf,
ncargs, cargs,
mudstate.global_regs);
}
}
// Also for lack of better place the @ifelse code is here.
// Idea for @ifelse from ChaoticMUX.
//
void do_if
(
dbref player, dbref caller, dbref enactor,
int eval, int key,
UTF8 *expr,
UTF8 *args[], int nargs,
const UTF8 *cargs[], int ncargs
)
{
UNUSED_PARAMETER(key);
if ( !expr
|| nargs <= 0)
{
return;
}
CLinearTimeAbsolute lta;
LBuf buff = LBuf_Src("do_if");
UTF8 *bp = buff.get();
mux_exec(expr, LBUF_SIZE-1, buff, &bp, player, caller, enactor, eval|EV_FCHECK|EV_EVAL|EV_TOP,
cargs, ncargs);
*bp = '\0';
int a = !xlate(buff);
if (a < nargs)
{
wait_que(player, caller, enactor, eval, false, lta, NOTHING, 0,
args[a],
ncargs, cargs,
mudstate.global_regs);
}
}
void do_addcommand
(
dbref player,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *name,
UTF8 *command,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
bool bNoEval = (key & ADDCMD_NOEVAL) != 0;
// Validate command name.
//
Convert all static scratch buffers to thread_local 43 static scratch-buffer arrays across 21 files (5 in mux/src, 38 in mux/modules/) changed from `static` to `thread_local`. Under the current single-threaded evaluator this is a zero-behavior-change swap — `thread_local` storage has the same lifetime and zero-allocation properties as `static` — but each thread gets its own copy, which makes these functions safe for a future multi-threaded evaluator without any locking. Read-only constant tables (`aRadix64`, `aRadixPenn36`, `aRadixPenn64`, `Empty`) left as `static` because they are immutable shared data. Affected areas: net.cpp — queue_string co_buf, trimmed_site, dump_users NameField signals.cpp — signal_desc stubslave.cpp — Stub_PipePump attrcache.cpp — sqlite_attr_buf boolexp.cpp — parsestore command.cpp — preserve_cmd, SpaceCompressCommand, LowerCaseCommand comsys.cpp — NewTitle, Buffer, temp db.cpp — tbuff, Buffer (x2) flags.cpp — buff funceval.cpp — textbuff functions.cpp — TimeBuffer64, TimeBuffer80, Buffer help.cpp — Line, Buffer mail.cpp — aFolders, Buffer, res, szFittedMailAliasDesc match.cpp — buffer player.cpp — szSalt, buf (x2), buff plusemail.cpp — buf predicates.cpp — Buf (x2), pName session.cpp — szFittedDoing set.cpp — pRestrictedKeyText unparse.cpp — buf, boolexp_buf mail_mod.cpp — result, res, buf All 21 files verified with g++ -std=c++17 -fsyntax-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:21:14 -06:00
thread_local UTF8 pName[LBUF_SIZE];
if (1 <= nargs)
{
// Strip color, then strip \r\n\t and space.
//
{
size_t nRaw = strlen(reinterpret_cast<const char *>(name));
unsigned char plain[LBUF_SIZE];
size_t nPlain = co_strip_color(plain,
reinterpret_cast<const unsigned char *>(name), nRaw);
size_t j = 0;
for (size_t i = 0; i < nPlain; i++)
{
if (plain[i] != '\r' && plain[i] != '\n'
&& plain[i] != '\t' && plain[i] != ' ')
{
pName[j++] = plain[i];
}
}
pName[j] = '\0';
}
{
size_t nPlain = strlen(reinterpret_cast<const char *>(pName));
LBuf tmp = LBuf_Src("ok_name");
nPlain = co_tolower(tmp, pName, nPlain);
memcpy(pName, tmp, nPlain + 1);
}
}
if ( 0 == nargs
|| '\0' == pName[0]
|| ( pName[0] == '_'
&& pName[1] == '_'))
{
notify(player, M_("That is not a valid command name."));
return;
}
// Validate object/attribute.
//
dbref thing;
ATTR *pattr;
if ( !parse_attrib(player, command, &thing, &pattr)
|| !pattr)
{
notify(player, M_("No such attribute."));
return;
}
if (!See_attr(player, thing, pattr))
{
notify(player, NOPERM_MESSAGE);
return;
}
size_t nName = strlen(reinterpret_cast<char *>(pName));
auto it_old = mudstate.command_htab.find(std::vector<UTF8>(pName, pName + nName));
CMDENT *old = (it_old != mudstate.command_htab.end()) ? static_cast<CMDENT*>(it_old->second) : nullptr;
CMDENT *cmd;
if ( old
&& (old->callseq & CS_ADDED))
{
// Don't allow the same (thing,atr) in the list.
//
for (auto &entry : *old->addent)
{
if ( entry.thing == thing
&& entry.atr == pattr->number)
{
notify(player, tprintf(M_("%s already added."), pName));
return;
}
}
// Otherwise, add another (thing,atr) to the list.
//
old->addent->push_back(ADDENT{});
ADDENT &add = old->addent->back();
add.thing = thing;
add.atr = pattr->number;
add.name = reinterpret_cast<const char *>(pName);
if (bNoEval)
{
old->callseq |= CS_NOINTERP;
}
}
else
{
if (old)
{
// Delete the old built-in (which will later be added back as
// __name).
//
mudstate.command_htab.erase(std::vector<UTF8>(pName, pName + nName));
}
2018-10-03 17:54:51 +00:00
cmd = nullptr;
try
{
cmd = new CMDENT;
}
catch (...)
{
; // Nothing.
}
if (nullptr == cmd)
{
notify(player, OUT_OF_MEMORY);
return;
}
cmd->cmdname = StringClone(pName);
2018-10-03 17:54:51 +00:00
cmd->switches = nullptr;
cmd->perms = 0;
cmd->extra = 0;
if ( old
&& (old->callseq & CS_LEADIN))
{
cmd->callseq = CS_ADDED|CS_ONE_ARG|CS_LEADIN;
}
else
{
cmd->callseq = CS_ADDED|CS_ONE_ARG;
}
if (bNoEval)
{
cmd->callseq |= CS_NOINTERP;
}
cmd->flags = CEF_ALLOC;
cmd->addent = new std::vector<ADDENT>();
cmd->addent->push_back(ADDENT{});
ADDENT &add = cmd->addent->back();
add.thing = thing;
add.atr = pattr->number;
add.name = reinterpret_cast<const char *>(pName);
mudstate.command_htab.emplace(std::vector<UTF8>(pName, pName + nName), cmd);
if ( old
&& strcmp(reinterpret_cast<char *>(pName), reinterpret_cast<char *>(old->cmdname)) == 0)
{
// We are @addcommand'ing over a built-in command by its
// unaliased name, therefore, we want to re-target all the
// aliases.
//
UTF8 *p = tprintf(T("__%s"), pName);
size_t nP = strlen(reinterpret_cast<char *>(p));
mudstate.command_htab.erase(std::vector<UTF8>(p, p + nP));
for (auto &[k, v] : mudstate.command_htab) { if (v == old) v = cmd; }
mudstate.command_htab.emplace(std::vector<UTF8>(p, p + nP), old);
}
}
// We reset the one letter commands here so you can overload them.
//
cache_prefix_cmds();
notify(player, tprintf(M_("Command %s added."), pName));
}
void do_listcommands(dbref player, dbref caller, dbref enactor, int eval,
int key, UTF8 *name, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
CMDENT *old;
bool didit = false;
// Let's make this case insensitive...
//
size_t nCased;
UTF8 *pCased = mux_strlwr(name, nCased);
if (*pCased)
{
{
auto it = mudstate.command_htab.find(std::vector<UTF8>(pCased, pCased + nCased));
old = (it != mudstate.command_htab.end()) ? static_cast<CMDENT*>(it->second) : nullptr;
}
if ( old
&& (old->callseq & CS_ADDED))
{
// If it's already found in the hash table, and it's being added
// using the same object and attribute...
//
for (auto &entry : *old->addent)
{
ATTR *ap = reinterpret_cast<ATTR *>(atr_num(entry.atr));
const UTF8 *pName = T("(WARNING: Bad Attribute Number)");
if (ap)
{
pName = ap->name;
}
notify(player, tprintf(T("%s: #%d/%s"), reinterpret_cast<const UTF8 *>(entry.name.c_str()), entry.thing, pName));
}
}
else
{
notify(player, tprintf(M_("%s not found in command table."), pCased));
}
return;
}
else
{
for (auto &[key, val] : mudstate.command_htab)
{
old = static_cast<CMDENT*>(val);
if (old->callseq & CS_ADDED)
{
const UTF8 *pKeyName = key.data();
int nKeyName = static_cast<int>(key.size());
for (auto &entry : *old->addent)
{
if ( static_cast<size_t>(nKeyName) != entry.name.size()
|| memcmp(pKeyName, entry.name.c_str(), nKeyName) != 0)
{
continue;
}
ATTR *ap = reinterpret_cast<ATTR *>(atr_num(entry.atr));
const UTF8 *pName = T("(WARNING: Bad Attribute Number)");
if (ap)
{
pName = ap->name;
}
notify(player, tprintf(T("%s: #%d/%s"), reinterpret_cast<const UTF8 *>(entry.name.c_str()),
entry.thing, pName));
didit = true;
}
}
}
}
if (!didit)
{
notify(player, M_("No added commands found in command table."));
}
}
void do_delcommand
(
dbref player,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *name,
UTF8 *command,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (!*name)
{
notify(player, M_("Sorry."));
return;
}
dbref thing = NOTHING;
int atr = NOTHING;
ATTR *pattr;
if (*command)
{
if ( !parse_attrib(player, command, &thing, &pattr)
|| !pattr)
{
notify(player, M_("No such attribute."));
return;
}
if (!See_attr(player, thing, pattr))
{
notify(player, NOPERM_MESSAGE);
return;
}
atr = pattr->number;
}
// Let's make this case insensitive...
//
size_t nCased;
UTF8 *pCased = mux_strlwr(name, nCased);
CMDENT *old, *cmd;
{
auto it = mudstate.command_htab.find(std::vector<UTF8>(pCased, pCased + nCased));
old = (it != mudstate.command_htab.end()) ? static_cast<CMDENT*>(it->second) : nullptr;
}
if ( old
&& (old->callseq & CS_ADDED))
{
UTF8 *p__Name = tprintf(T("__%s"), pCased);
size_t n__Name = strlen(reinterpret_cast<char *>(p__Name));
if (command[0] == '\0')
{
// Delete all @addcommand'ed associations with the given name.
//
delete old->addent;
old->addent = nullptr;
mudstate.command_htab.erase(std::vector<UTF8>(pCased, pCased + nCased));
{
auto it_cmd = mudstate.command_htab.find(std::vector<UTF8>(p__Name, p__Name + n__Name));
cmd = (it_cmd != mudstate.command_htab.end()) ? static_cast<CMDENT*>(it_cmd->second) : nullptr;
}
if (cmd)
{
size_t nCmdName = strlen(reinterpret_cast<char *>(cmd->cmdname));
mudstate.command_htab.emplace(std::vector<UTF8>(cmd->cmdname, cmd->cmdname + nCmdName), cmd);
if (strcmp(reinterpret_cast<char *>(pCased), reinterpret_cast<char *>(cmd->cmdname)) != 0)
{
mudstate.command_htab.emplace(std::vector<UTF8>(pCased, pCased + nCased), cmd);
}
mudstate.command_htab.erase(std::vector<UTF8>(p__Name, p__Name + n__Name));
mudstate.command_htab.emplace(std::vector<UTF8>(p__Name, p__Name + n__Name), cmd);
for (auto &[k, v] : mudstate.command_htab) { if (v == old) v = cmd; }
}
else
{
// No backup command to restore. Remove all hash
// entries that still reference 'old'.
//
mudstate.command_htab.erase(std::vector<UTF8>(p__Name, p__Name + n__Name));
for (auto it2 = mudstate.command_htab.begin(); it2 != mudstate.command_htab.end(); )
{
if (it2->second == old)
{
it2 = mudstate.command_htab.erase(it2);
}
else
{
++it2;
}
}
}
MEMFREE(old->cmdname);
2018-10-03 17:54:51 +00:00
old->cmdname = nullptr;
MEMFREE(old);
2018-10-03 17:54:51 +00:00
old = nullptr;
cache_prefix_cmds();
notify(player, M_("Done."));
}
else
{
// Remove only the (name,thing,atr) association.
//
for (auto it = old->addent->begin(); it != old->addent->end(); ++it)
{
if ( it->thing == thing
&& it->atr == atr)
{
old->addent->erase(it);
if (old->addent->empty())
{
delete old->addent;
old->addent = nullptr;
mudstate.command_htab.erase(std::vector<UTF8>(pCased, pCased + nCased));
{
auto it_cmd2 = mudstate.command_htab.find(std::vector<UTF8>(p__Name, p__Name + n__Name));
cmd = (it_cmd2 != mudstate.command_htab.end()) ? static_cast<CMDENT*>(it_cmd2->second) : nullptr;
}
if (cmd)
{
size_t nCmdName = strlen(reinterpret_cast<char *>(cmd->cmdname));
mudstate.command_htab.emplace(std::vector<UTF8>(cmd->cmdname, cmd->cmdname + nCmdName),
cmd);
if (strcmp(reinterpret_cast<char *>(pCased), reinterpret_cast<char *>(cmd->cmdname)) != 0)
{
mudstate.command_htab.emplace(std::vector<UTF8>(pCased, pCased + nCased),
cmd);
}
mudstate.command_htab.erase(std::vector<UTF8>(p__Name, p__Name + n__Name));
mudstate.command_htab.emplace(std::vector<UTF8>(p__Name, p__Name + n__Name),
cmd);
for (auto &[k, v] : mudstate.command_htab) { if (v == old) v = cmd; }
}
MEMFREE(old->cmdname);
old->cmdname = nullptr;
MEMFREE(old);
old = nullptr;
}
cache_prefix_cmds();
notify(player, M_("Done."));
return;
}
}
notify(player, M_("Command not found in command table."));
}
}
else
{
notify(player, M_("Command not found in command table."));
}
}
void do_quitprog(dbref player, dbref caller, dbref enactor, int eval, int key, UTF8 *name, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref doer;
if (*name)
{
doer = match_thing(player, name);
}
else
{
doer = player;
}
if ( !( Prog(player)
|| Prog(Owner(player)))
&& player != doer)
{
notify(player, NOPERM_MESSAGE);
return;
}
if ( !Good_obj(doer)
|| !isPlayer(doer))
{
notify(player, M_("That is not a player."));
return;
}
if (!Connected(doer))
{
notify(player, M_("That player is not connected."));
return;
}
if (!player_has_program(doer))
{
notify(player, M_("Player is not in an @program."));
return;
}
program_data* program = detach_player_program(doer);
if (program)
{
for (auto& wait_reg : program->wait_regs)
{
if (wait_reg)
{
RegRelease(wait_reg);
wait_reg = nullptr;
}
}
NamedRegsClear(program->named_wait_regs);
MEMFREE(program);
program = nullptr;
}
atr_clr(doer, A_PROGCMD);
notify(player, M_("@program cleared."));
notify(doer, M_("Your @program has been terminated."));
}
void do_prog
(
dbref player,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *name,
UTF8 *command,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if ( !name
|| !*name)
{
notify(player, M_("No players specified."));
return;
}
const dbref doer = match_thing(player, name);
if ( !( Prog(player)
|| Prog(Owner(player)))
&& player != doer)
{
notify(player, NOPERM_MESSAGE);
return;
}
if ( !Good_obj(doer)
|| !isPlayer(doer))
{
notify(player, M_("That is not a player."));
return;
}
if (!Connected(doer))
{
notify(player, M_("That player is not connected."));
return;
}
// Check to see if the enactor already has an @prog input pending.
//
if (player_has_program(doer))
{
notify(player, M_("Input already pending."));
return;
}
UTF8 *msg = command;
const UTF8 *attrib = parse_to(&msg, ':', 1);
if (msg && *msg)
{
notify(doer, msg);
}
dbref thing;
ATTR *ap;
if (!parse_attrib(player, attrib, &thing, &ap))
{
notify(player, NOMATCH_MESSAGE);
return;
}
if (ap)
{
dbref aowner;
int aflags;
int lev;
dbref parent;
2018-10-03 17:54:51 +00:00
UTF8 *pBuffer = nullptr;
bool bFound = false;
ITER_PARENTS(thing, parent, lev)
{
pBuffer = atr_get("do_prog.1405", parent, ap->number, &aowner, &aflags);
if (pBuffer[0])
{
bFound = true;
break;
}
free_lbuf(pBuffer);
}
if (bFound)
{
if ( ( God(player)
|| !God(thing))
&& See_attr(player, thing, ap))
{
atr_add_raw(doer, A_PROGCMD, pBuffer);
}
else
{
notify(player, NOPERM_MESSAGE);
free_lbuf(pBuffer);
return;
}
free_lbuf(pBuffer);
}
else
{
notify(player, M_("Attribute not present on object."));
return;
}
}
else
{
notify(player, M_("No such attribute."));
return;
}
const auto program = static_cast<program_data*>(MEMALLOC(sizeof(program_data)));
if (nullptr == program)
{
notify(player, OUT_OF_MEMORY);
return;
}
program->wait_enactor = player;
for (int i = 0; i < MAX_GLOBAL_REGS; i++)
{
program->wait_regs[i] = mudstate.global_regs[i];
if (mudstate.global_regs[i])
{
RegAddRef(mudstate.global_regs[i]);
}
}
program->named_wait_regs = NamedRegsCopy(mudstate.named_regs);
// Now, start waiting.
//
set_player_program(doer, program);
send_prog_prompt(doer);
}
/* ---------------------------------------------------------------------------
* do_restart: Restarts the game.
*/
void do_restart(dbref executor, dbref caller, dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
if (!Can_SiteAdmin(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
bool bDenied = false;
#if defined(HAVE_WORKING_FORK)
if (mudstate.dumping)
{
notify(executor, M_("Dumping. Please try again later."));
bDenied = true;
}
#endif // HAVE_WORKING_FORK
if (!mudstate.bCanRestart)
{
notify(executor, M_("Server just started. Please try again in a few seconds."));
bDenied = true;
}
if (bDenied)
{
STARTLOG(LOG_ALWAYS, "WIZ", "RSTRT");
log_text(T("Restart requested but not executed by "));
log_name(executor);
ENDLOG;
return;
}
#ifdef UNIX_SSL
raw_broadcast(0, M_("GAME: Restart by %s, please wait. (All SSL connections will be dropped.)"), Moniker(Owner(executor)));
#else
raw_broadcast(0, M_("GAME: Restart by %s, please wait."), Moniker(Owner(executor)));
#endif
STARTLOG(LOG_ALWAYS, "WIZ", "RSTRT");
log_text(T("Restart by "));
log_name(executor);
ENDLOG;
fix(net): decline the restart when restart.db cannot be written (#2043) #2042 made a failed dump visible; it could not make it survivable, because by the time dump_restart_db() ran the game was already dismantled. This moves the dump ahead of the teardown so the restart can simply not happen. Before, do_restart() ran prepare_for_restart(), final_modules(), pcache_sync, dump_database_internal, SYNC and CLOSE, and only then wrote restart.db. A failed write was therefore discovered with nothing left to return to, and the successor exec'd into a game it could not serve: prepare_for_restart() detaches the listener fds but deliberately leaves them OPEN for the successor to adopt, so without restart.db the successor collides with its own inheritance -- [Kqueue:7] Listener detached (fd left open). [Kqueue:6] bind() failed: Address already in use -- and exits. @restart on a full disk made the game disappear. The ordering that makes declining possible: listener detachment is step 6 of prepare_for_restart(), not step 1. Steps 1 (record listeners) and 2 (close TLS/WebSocket) are split into their own methods so DumpRestartDb() can run exactly what the dump needs, leaving the engine up and the listeners attached when the result is known. prepare_for_restart() still calls both, so there is one implementation and any other caller is unaffected; both are idempotent so re-running them costs nothing. Step 2 must precede the dump, not follow it: a TLS descriptor written into restart.db is restored by a successor with no TLS state for it (#2032). That is also why declining is not free, and the messages say so rather than implying nothing happened -- those sessions are already gone. This is abstention, not recovery. No retry, no fd reclamation, no heuristic about whether the failure looked transient. The dump either produced a complete file or it did not, and the restart proceeds or does not. Plumbing: dump_restart_db() returns bool through externs.h, the engine shim in conn_bridge.cpp, and CDriverControl::DumpRestartDb() -- which already returned MUX_RESULT and always said MUX_S_OK. No interface method was added, so the module ABI is unchanged. A missing driver control now reads as "cannot dump" rather than silent success; proceeding would be the exact failure this reports. Verified live on macOS/arm64, past the 15s bCanRestart gate: normal @restart 2 game logs, 725 bytes answered, no restart.db.tmp dump fails 1 game log, 725 bytes answered, game ALIVE RST/DUMP : Could not write restart.db (write failed)... WIZ/RSTRT : Restart CANCELLED ... Requested by Wizard(#1) neither restart.db nor restart.db.tmp published Before this change that second row was ConnectionRefusedError and a dead process. The new broadcast is an M_() string, so mux/po/tinymux.pot is regenerated and es/ko/xx merged; xx.po is a complete-policy pseudo-locale and gets its "[xx] " entry. The catalogue guard caught the omission -- test-nls failed with "marked in source but absent from .pot" and named the fix. make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 09:57:42 -06:00
// Write restart.db BEFORE the teardown, and decline the restart if it
// cannot be written (#2043).
//
// This used to happen after prepare_for_restart(), final_modules() and
// CLOSE, i.e. after the point of no return -- so a failed write was
// discovered when there was nothing left to return to, and the successor
// exec'd into a game it could not serve: prepare_for_restart() detaches
// the listener fds but leaves them open for the successor to adopt, so
// without restart.db the successor collides with its own inheritance and
// dies on "bind() failed: Address already in use".
//
// DumpRestartDb() records the listeners and closes the TLS/WebSocket
// sessions itself, because the dump needs both done first. Those are the
// only destructive steps that precede the decision, which is why the
// decline below is not free and says so.
//
#if defined(HAVE_WORKING_FORK)
if (!dump_restart_db())
{
raw_broadcast(0, M_("GAME: Restart could not be prepared and has been cancelled. The game is still running."));
STARTLOG(LOG_ALWAYS, "WIZ", "RSTRT");
log_text(T("Restart CANCELLED: restart.db could not be written. Requested by "));
log_name(executor);
ENDLOG;
return;
}
#endif // HAVE_WORKING_FORK
g_GanlAdapter.prepare_for_restart();
local_presync_database();
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->presync_database();
p = p->pNext;
}
#if defined(STUB_SLAVE)
final_stubslave();
#endif // STUB_SLAVE
final_modules();
pcache_sync();
dump_database_internal(DUMP_I_RESTART);
SYNC;
CLOSE;
2008-01-29 12:16:54 -08:00
#if defined(WINDOWS_PROCESSES)
exit(12345678);
2008-01-29 12:16:54 -08:00
#elif defined(UNIX_PROCESSES)
fix(net): decline the restart when restart.db cannot be written (#2043) #2042 made a failed dump visible; it could not make it survivable, because by the time dump_restart_db() ran the game was already dismantled. This moves the dump ahead of the teardown so the restart can simply not happen. Before, do_restart() ran prepare_for_restart(), final_modules(), pcache_sync, dump_database_internal, SYNC and CLOSE, and only then wrote restart.db. A failed write was therefore discovered with nothing left to return to, and the successor exec'd into a game it could not serve: prepare_for_restart() detaches the listener fds but deliberately leaves them OPEN for the successor to adopt, so without restart.db the successor collides with its own inheritance -- [Kqueue:7] Listener detached (fd left open). [Kqueue:6] bind() failed: Address already in use -- and exits. @restart on a full disk made the game disappear. The ordering that makes declining possible: listener detachment is step 6 of prepare_for_restart(), not step 1. Steps 1 (record listeners) and 2 (close TLS/WebSocket) are split into their own methods so DumpRestartDb() can run exactly what the dump needs, leaving the engine up and the listeners attached when the result is known. prepare_for_restart() still calls both, so there is one implementation and any other caller is unaffected; both are idempotent so re-running them costs nothing. Step 2 must precede the dump, not follow it: a TLS descriptor written into restart.db is restored by a successor with no TLS state for it (#2032). That is also why declining is not free, and the messages say so rather than implying nothing happened -- those sessions are already gone. This is abstention, not recovery. No retry, no fd reclamation, no heuristic about whether the failure looked transient. The dump either produced a complete file or it did not, and the restart proceeds or does not. Plumbing: dump_restart_db() returns bool through externs.h, the engine shim in conn_bridge.cpp, and CDriverControl::DumpRestartDb() -- which already returned MUX_RESULT and always said MUX_S_OK. No interface method was added, so the module ABI is unchanged. A missing driver control now reads as "cannot dump" rather than silent success; proceeding would be the exact failure this reports. Verified live on macOS/arm64, past the 15s bCanRestart gate: normal @restart 2 game logs, 725 bytes answered, no restart.db.tmp dump fails 1 game log, 725 bytes answered, game ALIVE RST/DUMP : Could not write restart.db (write failed)... WIZ/RSTRT : Restart CANCELLED ... Requested by Wizard(#1) neither restart.db nor restart.db.tmp published Before this change that second row was ConnectionRefusedError and a dead process. The new broadcast is an M_() string, so mux/po/tinymux.pot is regenerated and es/ko/xx merged; xx.po is a complete-policy pseudo-locale and gets its "[xx] " entry. The catalogue guard caught the omission -- test-nls failed with "marked in source but absent from .pot" and named the fix. make test 35 passed / 1 skipped / 0 failed (jit=yes stubslave=no nls=yes realitylvls=yes wodrealms=yes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 09:57:42 -06:00
// restart.db was written above, before the teardown, and the restart was
// declined if it could not be (#2043). Dumping here as well would
// re-serialise descriptors the teardown has since closed.
Log.StopLogging();
fix(restart): preserve the -p and -e command-line values across @restart (#2199) do_restart() rebuilds its exec argv from mudconf.pid_file and mudconf.log_dir. Both were read in three places and written in NONE: predicates.cpp the execl <- read engine_com.cpp GetConfig -> DRIVER_CONFIG.pid_file <- read engine_com.cpp GetConfig -> DRIVER_CONFIG.log_dir <- read There is no `pid_file` or `log_dir` config directive either, so nothing in netmux.conf could populate them. Both are driver-owned CLI values -- driver.cpp says so outright ("pid_file is driver-owned -- set from CLI or default") -- and the engine had no way to learn them. So every @restart on every site exec'd with an empty -p and -e. Impact is quiet, which is why it lasted: a site running netmux -p /var/run/mux/netmux.pid has its pidfile revert to the default netmux.pid in the working directory after the first restart, and stops maintaining the configured path. The pid inside the stale file stays correct, because execve preserves it, so nothing breaks until something else moves. Same for -e. The fingerprint is visible in ps on any restarted 2.14: `-p` with nothing after it. Fix follows #817's precedent exactly -- driver-owned strings the engine needs, handed over at bridge init: - g_driver_pid_file / g_driver_log_dir replace two file-static CLI variables in driver.cpp (pErrorBasename was also a poor name for something that is a log directory). Globals for the same reason g_version is one: the CDriverControl bridge is a different TU. - mux_IDriverControl gains GetInvocationPaths. IID bumped D4D6 -> D4D7; the comment now records both bumps and why a vtable change needs one. - conn_bridge_init caches them into mudconf. Safe there because Startup() runs after LoadGame(), so cf_init() cannot wipe them afterwards. Pointers are stored, not copied: they are driver-owned storage that lives for the process, and nothing in the engine frees or reassigns either field. - CScriptDriverControl implements it too, returning empty strings. muxscript writes no pidfile and has no @restart; an invented default would only be wrong somewhere later. The argv is now built dynamically and omits an option it has no value for. -c, -p and -e are all CLI_REQUIRED, so a bare flag makes the successor log "Option 'x' requires an argument, but none was found." Populating the fields alone would have left that warning for any site that supplies only one of the two -- which is what test-scenario showed, since run.sh passes -p and not -e. Verified on a live restart, reading the successor's argv (execve replaces argv and preserves the pid, so this is direct evidence): boot: ./bin/netmux -c netmux.conf -p custom-name.pid -e logs restart: netmux -c netmux.conf -p custom-name.pid -e logs ok - successor kept -p custom-name.pid ok - successor kept -e logs ok - no missing-argument warning in the log Catch-verified by removing only the bridge caching: restart: netmux -c netmux.conf -p not ok - successor lost the -p value not ok - successor lost the -e value not ok - successor logged a missing-argument warning: 15:Warning: Option '-p' requires an argument, but none was found. tests/scenario/restart_helpers.py gains the assertion; it already pays for a restart, so this costs no extra cycle. It skips if the server under test was not started with -p. Clean rebuild, since the IDriverControl vtable changed. make test: 36 targets, 34 passed, 2 skipped (NLS), 0 failed config: jit=yes stubslave=yes nls=no realitylvls=yes wodrealms=yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:39:34 -06:00
// #2199: build argv, omitting any option whose value is empty.
//
// -c, -p and -e are all CLI_REQUIRED, so passing a bare flag makes the
// successor log "Option 'x' requires an argument, but none was found."
// That is what a restarted server used to do for BOTH -p and -e, every
// time, because mudconf.pid_file and mudconf.log_dir were read here and
// written nowhere. They are populated now, but a site that supplies
// only one of the two would still collect a warning for the other on
// every restart -- so skip what we do not have rather than pass "".
//
const char *argvRestart[9];
int argcRestart = 0;
argvRestart[argcRestart++] = "netmux";
argvRestart[argcRestart++] = "-c";
argvRestart[argcRestart++] =
reinterpret_cast<const char *>(mudconf.config_file);
if ( nullptr != mudconf.pid_file
&& '\0' != mudconf.pid_file[0])
{
argvRestart[argcRestart++] = "-p";
argvRestart[argcRestart++] =
reinterpret_cast<const char *>(mudconf.pid_file);
}
if ( nullptr != mudconf.log_dir
&& '\0' != mudconf.log_dir[0])
{
argvRestart[argcRestart++] = "-e";
argvRestart[argcRestart++] =
reinterpret_cast<const char *>(mudconf.log_dir);
}
argvRestart[argcRestart] = nullptr;
execv("bin/netmux", const_cast<char *const *>(argvRestart));
mux_assert(false);
2008-01-29 12:16:54 -08:00
#endif // UNIX_PROCESSES
}
/* ---------------------------------------------------------------------------
* do_comment: Implement the @@ (comment) command. Very cpu-intensive :-)
*/
void do_comment(dbref executor, dbref caller, dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
}
void do_eval(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *arg1, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(arg1);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
}
static dbref promote_dflt(dbref old, dbref new0)
{
if ( old == NOPERM
|| new0 == NOPERM)
{
return NOPERM;
}
if ( old == AMBIGUOUS
|| new0 == AMBIGUOUS)
{
return AMBIGUOUS;
}
return NOTHING;
}
feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced 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>
2026-08-06 14:34:23 -06:00
dbref match_possessed(dbref player, dbref thing, const UTF8 *target, dbref dflt, bool check_enter)
{
// First, check normally.
//
if (Good_obj(dflt))
{
return dflt;
}
// Didn't find it directly. Recursively do a contents check.
//
dbref result, result1;
feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced 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>
2026-08-06 14:34:23 -06:00
UTF8 *d1;
const UTF8 *place, *s1, *temp;
const UTF8 *start = target;
while (*target)
{
// Fail if no ' characters.
//
place = target;
feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced 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>
2026-08-06 14:34:23 -06:00
target = reinterpret_cast<const UTF8 *>(strchr(reinterpret_cast<const char *>(place), '\''));
2018-10-03 17:54:51 +00:00
if ( target == nullptr
|| !*target)
{
return dflt;
}
// If string started with a ', skip past it
//
if (place == target)
{
target++;
continue;
}
// If next character is not an s or a space, skip past
//
temp = target++;
if (!*target)
{
return dflt;
}
if ( *target != 's'
&& *target != 'S'
&& *target != ' ')
{
continue;
}
// If character was not a space make sure the following character is
// a space.
//
if (*target != ' ')
{
target++;
if (!*target)
{
return dflt;
}
if (*target != ' ')
{
continue;
}
}
// Copy the container name to a new buffer so we can terminate it.
//
{
LBuf buff = LBuf_Src("is_posess");
d1 = buff.get();
for (s1 = start; *s1 && (s1 < temp); *d1++ = (*s1++))
{
; // Nothing.
}
*d1 = '\0';
// Look for the container here and in our inventory. Skip past if we
// can't find it.
//
init_match(thing, buff, NOTYPE);
if (player == thing)
{
match_neighbor();
match_possession();
}
else
{
match_possession();
}
result1 = match_result();
}
if (!Good_obj(result1))
{
dflt = promote_dflt(dflt, result1);
continue;
}
// If we don't control it and it is either dark or opaque, skip past.
//
bool control = Controls(player, result1);
if ( ( Dark(result1)
|| Opaque(result1))
&& !control)
{
dflt = promote_dflt(dflt, NOTHING);
continue;
}
// Validate object has the ENTER bit set, if requested.
//
if ( check_enter
&& !Enter_ok(result1)
&& !control)
{
dflt = promote_dflt(dflt, NOPERM);
continue;
}
// Look for the object in the container.
//
init_match(result1, target, NOTYPE);
match_possession();
result = match_result();
result = match_possessed(player, result1, target, result, check_enter);
if (Good_obj(result))
{
return result;
}
dflt = promote_dflt(dflt, result);
}
return dflt;
}
/* ---------------------------------------------------------------------------
* parse_range: break up <what>,<low>,<high> syntax
*/
void parse_range(UTF8 **name, dbref *low_bound, dbref *high_bound)
{
UTF8 *buff1 = *name;
if (buff1 && *buff1)
{
*name = parse_to(&buff1, ',', EV_STRIP_TS);
}
if (buff1 && *buff1)
{
UTF8 *buff2 = parse_to(&buff1, ',', EV_STRIP_TS);
if (buff1 && *buff1)
{
while (mux_isspace(*buff1))
{
buff1++;
}
if (*buff1 == NUMBER_TOKEN)
{
buff1++;
}
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:03:45 -06:00
*high_bound = mux_atoi64(buff1);
if (*high_bound >= mudstate.db_top)
{
*high_bound = mudstate.db_top - 1;
}
}
else
{
*high_bound = mudstate.db_top - 1;
}
while (mux_isspace(*buff2))
{
buff2++;
}
if (*buff2 == NUMBER_TOKEN)
{
buff2++;
}
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:03:45 -06:00
*low_bound = mux_atoi64(buff2);
if (*low_bound < 0)
{
*low_bound = 0;
}
}
else
{
*low_bound = 0;
*high_bound = mudstate.db_top - 1;
}
}
bool parse_thing_slash(dbref player, const UTF8 *thing, const UTF8 **after, dbref *it)
{
// Get name up to '/'.
//
size_t i = 0;
while ( thing[i] != '\0'
&& thing[i] != '/')
{
i++;
}
// If no '/' in string, return failure.
//
if (thing[i] == '\0')
{
2018-10-03 17:54:51 +00:00
*after = nullptr;
*it = NOTHING;
return false;
}
*after = thing + i + 1;
// Look for the object.
//
init_match(player, thing, i, NOTYPE);
match_everything(MAT_EXIT_PARENTS);
*it = match_result();
// Return status of search.
//
return Good_obj(*it);
}
bool get_obj_and_lock(dbref player, const UTF8 *what, dbref *it, ATTR **attr, UTF8 *errmsg, UTF8 **bufc)
{
// Get name up to '/'.
//
size_t i = 0;
while ( what[i] != '\0'
&& what[i] != '/')
{
i++;
}
*it = match_thing_quiet(player, what, i);
if (!Good_obj(*it))
{
safe_match_result(*it, errmsg, bufc);
return false;
}
int anum;
if (what[i] == '/')
{
// <obj>/<lock> syntax, use the named lock.
//
if (!search_nametab(player, lock_sw, what + i + 1, &anum))
{
safe_str(S_("#-1 LOCK NOT FOUND"), errmsg, bufc);
return false;
}
}
else
{
// Not <obj>/<lock>, do a normal get of the default lock.
//
anum = A_LOCK;
}
// Get the attribute definition, fail if not found.
//
*attr = atr_num(anum);
2018-10-03 17:54:51 +00:00
if (nullptr == *attr)
{
safe_str(S_("#-1 LOCK NOT FOUND"), errmsg, bufc);
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// bCanReadAttr, bCanSetAttr: Verify permission to affect attributes.
// ---------------------------------------------------------------------------
bool bCanReadAttr(dbref executor, dbref target, ATTR *tattr, bool bCheckParent)
{
if (!tattr)
{
return false;
}
dbref aowner;
int aflags;
if ( !mudstate.bStandAlone
&& bCheckParent)
{
atr_pget_info(target, tattr->number, &aowner, &aflags);
}
else
{
atr_get_info(target, tattr->number, &aowner, &aflags);
}
int test_flags = tattr->flags;
if (mudstate.attrperm_list)
{
ATTRPERM *perm_walk = mudstate.attrperm_list;
2018-10-03 17:54:51 +00:00
while (nullptr != perm_walk)
{
if (quick_wild(perm_walk->wildcard, tattr->name))
{
test_flags |= perm_walk->flags;
}
perm_walk = perm_walk->next;
}
}
int mAllow = AF_VISUAL;
if ( (test_flags & mAllow)
|| (aflags & mAllow))
{
if ( mudstate.bStandAlone
|| tattr->number != A_DESC
|| mudconf.read_rem_desc
|| nearby(executor, target))
{
return true;
}
}
int mDeny = 0;
if (WizRoy(executor))
{
if (God(executor))
{
mDeny = AF_INTERNAL;
}
else
{
mDeny = AF_INTERNAL|AF_DARK;
}
}
else if ( Owner(executor) == aowner
|| Examinable(executor, target))
{
mDeny = AF_INTERNAL|AF_DARK|AF_MDARK;
}
if (mDeny)
{
if ( (test_flags & mDeny)
|| (aflags & mDeny))
{
return false;
}
else
{
return true;
}
}
return false;
}
bool bCanSetAttr(dbref executor, dbref target, ATTR *tattr)
{
if (!tattr)
{
return false;
}
if (NoModify(target) && !WizRoy(executor))
{
return false;
}
int mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST;
if (!God(executor))
{
if (God(target))
{
return false;
}
if (Wizard(executor))
{
mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST|AF_LOCK|AF_GOD;
}
else if (Controls(executor, target))
{
mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST|AF_LOCK|AF_WIZARD|AF_GOD;
}
else
{
return false;
}
}
dbref aowner;
int aflags;
bool info = atr_get_info(target, tattr->number, &aowner, &aflags);
int test_flags = tattr->flags;
if (mudstate.attrperm_list)
{
ATTRPERM *perm_walk = mudstate.attrperm_list;
2018-10-03 17:54:51 +00:00
while (nullptr != perm_walk)
{
if (quick_wild(perm_walk->wildcard,tattr->name))
{
test_flags |= perm_walk->flags;
}
perm_walk = perm_walk->next;
}
}
if ( (test_flags & mDeny)
|| (info && (aflags & mDeny)))
{
return false;
}
else
{
return true;
}
}
bool bCanLockAttr(dbref executor, dbref target, ATTR *tattr)
{
if (!tattr)
{
return false;
}
if (NoModify(target) && !WizRoy(executor))
{
return false;
}
int mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST;
if (!God(executor))
{
if (God(target))
{
return false;
}
if (Wizard(executor))
{
mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST|AF_GOD;
}
else
{
mDeny = AF_INTERNAL|AF_IS_LOCK|AF_CONST|AF_WIZARD|AF_GOD;
}
}
dbref aowner;
int aflags;
bool info = atr_get_info(target, tattr->number, &aowner, &aflags);
int test_flags = tattr->flags;
if (mudstate.attrperm_list)
{
ATTRPERM *perm_walk = mudstate.attrperm_list;
2018-10-03 17:54:51 +00:00
while (nullptr != perm_walk)
{
if (quick_wild(perm_walk->wildcard,tattr->name))
{
test_flags |= perm_walk->flags;
}
perm_walk = perm_walk->next;
}
}
if ( (test_flags & mDeny)
|| !info
|| (aflags & mDeny))
{
return false;
}
else if ( Wizard(executor)
|| Owner(executor) == aowner)
{
return true;
}
else
{
return false;
}
}
/* ---------------------------------------------------------------------------
* where_is: Returns place where obj is linked into a list.
* ie. location for players/things, source for exits, NOTHING for rooms.
*/
dbref where_is(dbref what)
{
if (!Good_obj(what))
{
return NOTHING;
}
dbref loc;
switch (Typeof(what))
{
case TYPE_PLAYER:
case TYPE_THING:
loc = Location(what);
break;
case TYPE_EXIT:
loc = Exits(what);
break;
default:
loc = NOTHING;
break;
}
return loc;
}
/* ---------------------------------------------------------------------------
* where_room: Return room containing player, or NOTHING if no room or
* recursion exceeded. If player is a room, returns itself.
*/
dbref where_room(dbref what)
{
for (int count = mudconf.ntfy_nest_lim; count > 0; count--)
{
if (!Good_obj(what))
{
break;
}
if (isRoom(what))
{
return what;
}
if (!Has_location(what))
{
break;
}
what = Location(what);
}
return NOTHING;
}
bool locatable(dbref player, dbref it, dbref enactor)
{
// No sense in trying to locate a bad object.
//
if (!Good_obj(it))
{
return false;
}
dbref loc_it = where_is(it);
// Succeed if we can examine the target, if we are the target, if we can
// examine the location, if the player is a wizard, or if the target
// caused the lookup.
//
if ( Examinable(player, it)
|| Find_Unfindable(player)
|| loc_it == player
|| ( loc_it != NOTHING
&& ( Examinable(player, loc_it)
|| loc_it == where_is(player))
&& ( !Hidden(it)
|| See_Hidden(player)))
|| Wizard(player)
|| it == enactor)
{
return true;
}
dbref room_it = where_room(it);
bool findable_room;
if (Good_obj(room_it))
{
findable_room = Findable(room_it);
}
else
{
findable_room = true;
}
// Succeed if we control the containing room or if the target is findable
// and the containing room is not unfindable.
//
if ( ( room_it != NOTHING
&& Examinable(player, room_it))
&& ( !Hidden(it)
|| See_Hidden(player))
|| Find_Unfindable(player)
|| ( Findable(it)
&& findable_room)
&& ( !Hidden(it)
|| See_Hidden(player)))
{
return true;
}
// We can't do it.
//
return false;
}
/* ---------------------------------------------------------------------------
* nearby: Check if thing is nearby player (in inventory, in same room, or
* IS the room.
*/
bool nearby(dbref player, dbref thing)
{
if ( !Good_obj(player)
|| !Good_obj(thing))
{
return false;
}
if ( Can_Hide(thing)
&& Hidden(thing)
&& !See_Hidden(player))
{
return false;
}
dbref thing_loc = where_is(thing);
if (thing_loc == player)
{
return true;
}
dbref player_loc = where_is(player);
if ( thing_loc == player_loc
|| thing == player_loc)
{
return true;
}
return false;
}
/*
* ---------------------------------------------------------------------------
* * exit_visible, exit_displayable: Is exit visible?
*/
bool exit_visible(dbref exit, dbref player, int key)
{
#ifdef WOD_REALMS
if (!mudstate.bStandAlone)
{
int iRealmDirective = DoThingToThingVisibility(player, exit,
ACTION_IS_STATIONARY);
if (REALM_DO_HIDDEN_FROM_YOU == iRealmDirective)
{
return false;
}
}
#endif // WOD_REALMS
#ifdef REALITY_LVLS
if (!mudstate.bStandAlone)
{
if (!IsReal(player, exit))
{
return false;
}
}
#endif // REALITY_LVLS
// Exam exit's location
//
if ( (key & VE_LOC_XAM)
|| Examinable(player, exit)
|| Light(exit))
{
return true;
}
// Dark location or base
//
if ( (key & (VE_LOC_DARK | VE_BASE_DARK))
|| Dark(exit))
{
return false;
}
// VisibleLock
//
if (!could_doit(player, exit, A_LVISIBLE))
{
return false;
}
// Default
//
return true;
}
// Exit visible to look
//
bool exit_displayable(dbref exit, dbref player, int key)
{
// Dark exit
//
if (Dark(exit))
{
return false;
}
#ifdef WOD_REALMS
if (!mudstate.bStandAlone)
{
int iRealmDirective = DoThingToThingVisibility(player, exit,
ACTION_IS_STATIONARY);
if (REALM_DO_HIDDEN_FROM_YOU == iRealmDirective)
{
return false;
}
}
#endif // WOD_REALMS
#ifdef REALITY_LVLS
if (!mudstate.bStandAlone)
{
if (!IsReal(player, exit))
{
return false;
}
}
#endif // REALITY_LVLS
// Light exit
//
if (Light(exit))
{
return true;
}
// Dark location or base.
//
if (key & (VE_LOC_DARK | VE_BASE_DARK))
{
return false;
}
// VisibleLock
//
if (!could_doit(player, exit, A_LVISIBLE))
{
return false;
}
// Default
//
return true;
}
/* ---------------------------------------------------------------------------
* did_it: Have player do something to/with thing
*/
void did_it(dbref player, dbref thing, int what, const UTF8 *def, int owhat,
const UTF8 *odef, int awhat, int ctrl_flags,
const UTF8 *args[], int nargs)
{
if (alarm_clock.alarmed)
{
return;
}
UTF8 *d, *buff, *act, *charges, *bp;
dbref loc, aowner;
int64_t num;
int aflags;
// If we need to call exec() from within this function, we first save
// the state of the global registers, in order to avoid munging them
// inappropriately. Do note that the restoration to their original
// values occurs BEFORE the execution of the @a-attribute. Therefore,
// any changing of setq() values done in the @-attribute and @o-attribute
// will NOT be passed on. This prevents odd behaviors that result from
// odd @verbs and so forth (the idea is to preserve the caller's control
// of the global register values).
//
bool need_pres = false;
2018-10-03 17:54:51 +00:00
reg_ref **preserve = nullptr;
// message to player.
//
if (what > 0)
{
d = atr_pget(thing, what, &aowner, &aflags);
if (*d)
{
if ((aflags & AF_NOEVAL) || NoEval(thing))
{
// Output raw text with no substitutions.
//
notify(player, d);
}
else
{
need_pres = true;
preserve = PushRegisters(MAX_GLOBAL_REGS);
save_global_regs(preserve);
buff = bp = alloc_lbuf("did_it.1");
mux_exec(d, LBUF_SIZE-1, buff, &bp, thing, player, player,
AttrTrace(aflags, EV_EVAL|EV_FIGNORE|EV_FCHECK|EV_TOP),
args, nargs);
*bp = '\0';
if ( (aflags & AF_HTML)
&& Html(player))
{
safe_str(T("\r\n"), buff, &bp);
*bp = '\0';
notify_html(player, buff);
}
else
{
notify(player, buff);
}
free_lbuf(buff);
}
}
else if (def)
{
notify(player, def);
}
free_lbuf(d);
}
else if (what < 0 && def)
{
notify(player, def);
}
// message to neighbors.
//
if ( 0 < owhat
&& Has_location(player)
&& Good_obj(loc = Location(player)))
{
d = atr_pget(thing, owhat, &aowner, &aflags);
if (*d)
{
if ((aflags & AF_NOEVAL) || NoEval(thing))
{
// Output raw text with no substitutions.
//
#ifdef REALITY_LVLS
if (aflags & AF_NONAME)
{
notify_except2_rlevel(loc, player, player, thing, d);
}
else
{
notify_except2_rlevel(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), d));
}
#else
if (aflags & AF_NONAME)
{
notify_except2(loc, player, player, thing, d);
}
else
{
notify_except2(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), d));
}
#endif // REALITY_LVLS
}
else
{
if (!need_pres)
{
need_pres = true;
preserve = PushRegisters(MAX_GLOBAL_REGS);
save_global_regs(preserve);
}
buff = bp = alloc_lbuf("did_it.2");
mux_exec(d, LBUF_SIZE-1, buff, &bp, thing, player, player,
AttrTrace(aflags, EV_EVAL|EV_FIGNORE|EV_FCHECK|EV_TOP),
args, nargs);
*bp = '\0';
if (*buff)
{
#ifdef REALITY_LVLS
if (aflags & AF_NONAME)
{
notify_except2_rlevel(loc, player, player, thing, buff);
}
else
{
notify_except2_rlevel(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), buff));
}
#else
if (aflags & AF_NONAME)
{
notify_except2(loc, player, player, thing, buff);
}
else
{
notify_except2(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), buff));
}
#endif // REALITY_LVLS
}
free_lbuf(buff);
} // else (not NOEVAL)
}
else if (odef)
{
#ifdef REALITY_LVLS
if (ctrl_flags & VERB_NONAME)
{
notify_except2_rlevel(loc, player, player, thing, odef);
}
else
{
notify_except2_rlevel(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), odef));
}
#else
if (ctrl_flags & VERB_NONAME)
{
notify_except2(loc, player, player, thing, odef);
}
else
{
notify_except2(loc, player, player, thing,
tprintf(T("%s %s"), Moniker(player), odef));
}
#endif // REALITY_LVLS
}
free_lbuf(d);
} else if ( owhat < 0
&& odef
&& Has_location(player)
&& Good_obj(loc = Location(player)))
{
#ifdef REALITY_LVLS
if (ctrl_flags & VERB_NONAME)
{
notify_except2_rlevel(loc, player, player, thing, odef);
}
else
{
notify_except2_rlevel(loc, player, player, thing, tprintf(T("%s %s"), Name(player), odef));
}
#else
if (ctrl_flags & VERB_NONAME)
{
notify_except2(loc, player, player, thing, odef);
}
else
{
notify_except2(loc, player, player, thing, tprintf(T("%s %s"), Name(player), odef));
}
#endif // REALITY_LVLS
}
// If we preserved the state of the global registers, restore them.
//
if (need_pres)
{
restore_global_regs(preserve);
PopRegisters(preserve, MAX_GLOBAL_REGS);
}
// Do the action attribute.
//
#ifdef REALITY_LVLS
if ( 0 < awhat
&& IsReal(thing, player))
#else
if (0 < awhat)
#endif // REALITY_LVLS
{
if (*(act = atr_pget(thing, awhat, &aowner, &aflags)))
{
dbref aowner2;
int aflags2;
charges = atr_pget(thing, A_CHARGES, &aowner2, &aflags2);
if (*charges)
{
// int64_t, not int (#1402).
//
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:03:45 -06:00
num = mux_atoi64(charges);
if (num > 0)
{
buff = alloc_sbuf("did_it.charges");
mux_i64toa(num - 1, buff);
atr_add_raw(thing, A_CHARGES, buff);
free_sbuf(buff);
}
else if (*(buff = atr_pget(thing, A_RUNOUT, &aowner2, &aflags2)))
{
free_lbuf(act);
act = buff;
}
else
{
free_lbuf(act);
free_lbuf(buff);
free_lbuf(charges);
return;
}
}
free_lbuf(charges);
if (!((aflags & AF_NOEVAL) || NoEval(thing)))
{
CLinearTimeAbsolute lta;
wait_que(thing, player, player, AttrTrace(aflags, 0), false, lta,
NOTHING, 0,
act,
nargs, args,
mudstate.global_regs);
}
}
free_lbuf(act);
}
}
/* ---------------------------------------------------------------------------
* do_verb: Command interface to did_it.
*/
void do_verb(dbref executor, dbref caller, dbref enactor, int eval, int key,
UTF8 *victim_str, UTF8 *args[], int nargs, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Look for the victim.
//
if ( !victim_str
|| !*victim_str)
{
notify(executor, M_("Nothing to do."));
return;
}
// Get the victim.
//
init_match(executor, victim_str, NOTYPE);
match_everything(MAT_EXIT_PARENTS);
dbref victim = noisy_match_result();
if (!Good_obj(victim))
{
return;
}
// Get the actor. Default is my cause.
//
dbref actor;
if ( nargs >= 1
&& args[0] && *args[0])
{
init_match(executor, args[0], NOTYPE);
match_everything(MAT_EXIT_PARENTS);
actor = noisy_match_result();
if (!Good_obj(actor))
{
return;
}
}
else
{
actor = enactor;
}
// Check permissions. There are two possibilities:
//
// 1. Executor controls both victim and actor. In this case,
// victim runs his action list.
//
// 2. Executor controls actor. In this case victim does not run
// his action list and any attributes that executor cannot read
// from victim are defaulted.
//
if (!Controls(executor, actor))
{
notify_quiet(executor, M_("Permission denied,"));
return;
}
ATTR *ap;
int what = -1;
int owhat = -1;
int awhat = -1;
2018-10-03 17:54:51 +00:00
const UTF8 *whatd = nullptr;
const UTF8 *owhatd = nullptr;
int nxargs = 0;
dbref aowner = NOTHING;
int aflags = NOTHING;
UTF8 *xargs[10];
switch (nargs) // Yes, this IS supposed to fall through.
{
case 7:
// Get arguments.
//
parse_arglist(victim, actor, actor, args[6],
2018-10-03 17:54:51 +00:00
EV_STRIP_LS | EV_STRIP_TS, xargs, 10, nullptr, 0, &nxargs);
case 6:
// Get action attribute.
//
ap = atr_str(args[5]);
if (ap)
{
awhat = ap->number;
}
case 5:
// Get others message default.
//
if (args[4] && *args[4])
{
owhatd = args[4];
}
case 4:
// Get others message attribute.
//
ap = atr_str(args[3]);
if (ap && (ap->number > 0))
{
owhat = ap->number;
}
case 3:
// Get enactor message default.
//
if (args[2] && *args[2])
{
whatd = args[2];
}
case 2:
// Get enactor message attribute.
//
ap = atr_str(args[1]);
if (ap && (ap->number > 0))
{
what = ap->number;
}
}
// If executor doesn't control both, enforce visibility restrictions.
//
if (!Controls(executor, victim))
{
2018-10-03 17:54:51 +00:00
ap = nullptr;
if (what != -1)
{
atr_get_info(victim, what, &aowner, &aflags);
ap = atr_num(what);
}
if ( !ap
|| !bCanReadAttr(executor, victim, ap, false)
|| ( ap->number == A_DESC
&& !mudconf.read_rem_desc
&& !Examinable(executor, victim)
&& !nearby(executor, victim)))
{
what = -1;
}
2018-10-03 17:54:51 +00:00
ap = nullptr;
if (owhat != -1)
{
atr_get_info(victim, owhat, &aowner, &aflags);
ap = atr_num(owhat);
}
if ( !ap
|| !bCanReadAttr(executor, victim, ap, false)
|| ( ap->number == A_DESC
&& !mudconf.read_rem_desc
&& !Examinable(executor, victim)
&& !nearby(executor, victim)))
{
owhat = -1;
}
awhat = 0;
}
// Go do it.
//
did_it(actor, victim, what, whatd, owhat, owhatd, awhat,
key & VERB_NONAME, (const UTF8 **)xargs, nxargs);
// Free user args.
//
for (int i = 0; i < nxargs; i++)
{
free_lbuf(xargs[i]);
}
}
// AssertionFailed and OutOfMemory moved to libmux.cpp.
static void ListReferences(dbref executor, UTF8 *reference_name)
{
dbref target = NOTHING;
bool global_only = false;
2018-10-03 17:54:51 +00:00
if ( nullptr == reference_name
2007-09-21 08:23:35 -07:00
|| '\0' == reference_name[0])
{
global_only = true;
}
else
{
global_only = false;
target = lookup_player(executor, reference_name, 1);
if (!Good_obj(target))
{
raw_notify(executor, M_("No such player."));
return;
}
if (!Controls(executor, target))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
}
// Listing:
// - if global_only is true, list all references that begin with _
// - Otherwise, list all references whose owner is target
//
reference_entry *htab_entry;
bool match_found = false;
for (auto &[ref_key, ref_val] : mudstate.reference_htab)
{
htab_entry = static_cast<reference_entry*>(ref_val);
if ( ( global_only
&& '_' == htab_entry->name[0])
|| ( !global_only
&& target == htab_entry->owner))
{
2007-09-21 08:23:35 -07:00
if (!Good_obj(htab_entry->target))
{
continue;
}
// @list ref schema (#1667 Phase 4 C5): name 12 / target 20 / owner 20.
//
static const size_t kRefNameCols = 12;
static const size_t kRefTargetCols = 20;
static const size_t kRefOwnerCols = 20;
2007-09-21 08:23:35 -07:00
if (!match_found)
{
match_found = true;
UTF8 header[LBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Reference"), kRefNameCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Target"), kRefTargetCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Owner"), kRefOwnerCols);
raw_notify(executor, header);
raw_notify(executor,
M_("-------------------------------------------------------"));
}
UTF8 *object_buf =
unparse_object(executor, htab_entry->target, false);
UTF8 line[LBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(line, sizeof(line), pos,
htab_entry->name, kRefNameCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
pos = mux_table_append_ljust(line, sizeof(line), pos,
object_buf, kRefTargetCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
pos = mux_table_append_ljust(line, sizeof(line), pos,
Moniker(htab_entry->owner), kRefOwnerCols);
raw_notify(executor, line);
free_lbuf(object_buf);
}
}
2007-09-21 08:23:35 -07:00
if (!match_found)
{
raw_notify(executor, M_("GAME: No references found."));
}
else
{
raw_notify(executor,
M_("---------------- End of Reference List ----------------"));
}
}
void do_reference
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *reference_name,
UTF8 *object_name,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(ncargs);
UNUSED_PARAMETER(cargs);
2007-09-21 08:23:35 -07:00
if (key & REFERENCE_LIST)
{
ListReferences(executor, reference_name);
return;
}
// References can only be set on objects the executor can examine.
//
dbref target = NOTHING;
2018-10-03 17:54:51 +00:00
if ( nullptr != object_name
2007-09-21 08:23:35 -07:00
&& '\0' != object_name[0])
{
target = match_thing_quiet(executor, object_name);
2007-09-21 08:23:35 -07:00
if (!Good_obj(target))
{
notify(executor, NOMATCH_MESSAGE);
return;
}
else if (!Examinable(executor, target))
{
notify(executor, NOPERM_MESSAGE);
return;
}
}
if ('_' == reference_name[0])
{
2007-09-21 08:23:35 -07:00
if (!Wizard(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
}
LBuf tbuf = LBuf_Src("do_name");
// mux_snprintf returns length written (0..count-1), not "would have".
//
size_t tbuf_len;
if ('_' == reference_name[0])
{
tbuf_len = mux_snprintf(tbuf.get(), LBUF_SIZE, T("%s"),
reference_name);
}
else
{
tbuf_len = mux_snprintf(tbuf.get(), LBUF_SIZE, T("%s.%lld"),
reference_name, static_cast<long long>(executor));
}
auto it_ref = mudstate.reference_htab.find(std::vector<UTF8>(tbuf.get(), tbuf.get() + tbuf_len));
struct reference_entry *result = (it_ref != mudstate.reference_htab.end())
? static_cast<reference_entry*>(it_ref->second) : nullptr;
enum { Delete, Add, Update, NotFound, Redundant, OutOfMemory } eOperation;
2018-10-03 17:54:51 +00:00
if (nullptr != result)
{
2007-09-21 08:23:35 -07:00
if (NOTHING == target)
{
eOperation = Delete;
}
2007-09-21 08:23:35 -07:00
else if (result->target == target)
{
eOperation = Redundant;
}
else // if (result->target != target)
{
eOperation = Update;
}
}
else
{
if (NOTHING == target)
{
eOperation = NotFound;
}
else
{
eOperation = Add;
if ( !Wizard(executor)
&& ThrottleReferences(executor))
{
raw_notify(executor, M_("References requested too quickly."));
return;
}
}
}
if ( Delete == eOperation
|| Update == eOperation)
{
// Release the existing reference.
//
MEMFREE(result->name);
2018-10-03 17:54:51 +00:00
result->name = nullptr;
MEMFREE(result);
2018-10-03 17:54:51 +00:00
result = nullptr;
mudstate.reference_htab.erase(std::vector<UTF8>(tbuf.get(), tbuf.get() + tbuf_len));
}
if ( Update == eOperation
|| Add == eOperation)
{
try
{
result = static_cast<reference_entry *>(MEMALLOC(sizeof(reference_entry)));
}
catch(...)
{
; // Nothing;
}
2018-10-03 17:54:51 +00:00
if (nullptr != result)
{
result->target = target;
result->owner = executor;
result->name = StringCloneLen(tbuf, tbuf_len);
mudstate.reference_htab.emplace(std::vector<UTF8>(tbuf.get(), tbuf.get() + tbuf_len), result);
}
else
{
eOperation = OutOfMemory;
}
}
if (Delete == eOperation)
{
raw_notify(executor, M_("Reference cleared."));
}
else if (Update == eOperation)
{
raw_notify(executor, M_("Reference updated."));
}
else if (Redundant == eOperation)
{
raw_notify(executor, M_("That reference already exists."));
}
else if (NotFound == eOperation)
{
raw_notify(executor, M_("No such reference to clear."));
}
else if (Add == eOperation)
{
raw_notify(executor, M_("Reference added."));
}
else if (OutOfMemory == eOperation)
{
raw_notify(executor, OUT_OF_MEMORY);
}
}