tinymux/mux/modules/engine/session.cpp

550 lines
12 KiB
C++
Raw Permalink Normal View History

/*! \file session.cpp
* \brief Engine-facing session and notification routines.
*
* Functions in this file implement game-level session semantics
* (notification, connection events, softcode helpers) using the
* connection accessor layer defined in net.cpp.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
// ---------------------------------------------------------------------------
// make_port_ulist: Make a list of connected user numbers for the LPORTS function.
// ---------------------------------------------------------------------------
struct port_ulist_context
{
dbref viewer;
ITL *pitl;
UTF8 *tmp;
};
static void port_ulist_callback(dbref player, SOCKET sock, void *ctx)
{
port_ulist_context *puc = static_cast<port_ulist_context *>(ctx);
if ( !See_Hidden(puc->viewer)
&& Hidden(player))
{
return;
}
UTF8* p = puc->tmp;
p += mux_ltoa(player, p);
*p++ = ':';
p += mux_i64toa(sock, p);
const size_t n = p - puc->tmp;
ItemToList_AddStringLEN(puc->pitl, n, puc->tmp);
}
void make_port_ulist(dbref player, UTF8 *buff, UTF8 **bufc)
{
ITL itl;
UTF8 *tmp = alloc_sbuf("make_port_ulist");
ItemToList_Init(&itl, buff, bufc, '#');
port_ulist_context ctx = { player, &itl, tmp };
for_each_connected_desc(port_ulist_callback, &ctx);
ItemToList_Final(&itl);
free_sbuf(tmp);
}
/* ---------------------------------------------------------------------------
* update_quotas: Update timeslice quotas
*/
void update_quotas(CLinearTimeAbsolute& ltaLast, const CLinearTimeAbsolute& ltaCurrent)
{
if (ltaCurrent < ltaLast)
{
ltaLast = ltaCurrent;
return;
}
const CLinearTimeDelta ltdDiff = ltaCurrent - ltaLast;
if (ltdDiff < mudconf.timeslice)
{
return;
}
const int nSlices = ltdDiff / mudconf.timeslice;
const int nExtraQuota = mudconf.cmd_quota_incr * nSlices;
if (nExtraQuota > 0)
{
update_all_desc_quotas(nExtraQuota, mudconf.cmd_quota_max);
}
ltaLast += mudconf.timeslice * nSlices;
}
/* raw_notify_html() -- raw_notify() without the newline */
void raw_notify_html(dbref player, const UTF8 *msg)
{
if (!msg || !*msg)
{
return;
}
if ( mudstate.inpipe
&& player == mudstate.poutobj)
{
safe_str(msg, mudstate.poutnew, &mudstate.poutbufc);
return;
}
if ( !Connected(player)
|| !Html(player))
{
return;
}
send_text_to_player(player, msg);
}
/* ---------------------------------------------------------------------------
* raw_notify: write a message to a player
*/
void raw_notify(dbref player, const UTF8 *msg)
{
if (!msg || !*msg)
{
return;
}
if ( mudstate.inpipe
&& player == mudstate.poutobj)
{
safe_str(msg, mudstate.poutnew, &mudstate.poutbufc);
safe_str(T("\r\n"), mudstate.poutnew, &mudstate.poutbufc);
return;
}
if (!Connected(player))
{
return;
}
send_text_to_player(player, msg);
send_raw_to_player(player, T("\r\n"), 2);
}
void raw_notify_newline(dbref player)
{
if ( mudstate.inpipe
&& player == mudstate.poutobj)
{
safe_str(T("\r\n"), mudstate.poutnew, &mudstate.poutbufc);
return;
}
if (!Connected(player))
{
return;
}
send_raw_to_player(player, T("\r\n"), 2);
}
/* ---------------------------------------------------------------------------
* raw_broadcast: Send message to players who have indicated flags
*/
void DCL_CDECL raw_broadcast(int inflags, const UTF8 *fmt, ...)
{
if (!fmt || !*fmt)
{
return;
}
LBuf buff = LBuf_Src("raw_notify_printf");
va_list ap;
va_start(ap, fmt);
mux_vsnprintf(buff, LBUF_SIZE, fmt, ap);
va_end(ap);
broadcast_and_flush(inflags, buff);
}
int fetch_session(dbref target)
{
return count_player_descs(target);
}
void check_events(void)
{
dbref thing, parent;
int lev;
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
FIELDEDTIME ft;
if (!ltaNow.ReturnFields(&ft))
{
return;
}
// Resetting every midnight.
//
static int iLastHourChecked = 25;
if ( iLastHourChecked == 23
&& ft.iHour < iLastHourChecked)
{
mudstate.events_flag &= ~ET_DAILY;
}
iLastHourChecked = ft.iHour;
if ( ft.iHour == mudconf.events_daily_hour
&& !(mudstate.events_flag & ET_DAILY))
{
mudstate.events_flag |= ET_DAILY;
DO_WHOLE_DB(thing)
{
if (Going(thing))
{
continue;
}
ITER_PARENTS(thing, parent, lev)
{
if (H_Daily(thing))
{
did_it(Owner(thing), thing, 0, nullptr, 0, nullptr, A_DAILY, 0,
nullptr, 0);
break;
}
}
}
}
}
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 *MakeCanonicalDoing(const UTF8 *pDoing, size_t *pnValidDoing, bool *pbValidDoing)
{
*pnValidDoing = 0;
*pbValidDoing = false;
if (!pDoing)
{
return nullptr;
}
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 szFittedDoing[SIZEOF_DOING_STRING+1];
mux_field nDoing = StripTabsAndTruncate( pDoing, szFittedDoing,
SIZEOF_DOING_STRING, WIDTHOF_DOING_STRING);
*pnValidDoing = nDoing.m_byte;
*pbValidDoing = true;
return szFittedDoing;
}
// ---------------------------------------------------------------------------
// do_doing: Set the doing string that appears in the WHO report.
// Idea from R'nice@TinyTIM.
//
void do_doing(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *arg, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
// Make sure there can be no embedded newlines from %r
//
static UTF8 Empty[] = {'\0'};
UTF8 *szValidDoing = Empty;
bool bValidDoing;
size_t nValidDoing = 0;
if (arg)
{
szValidDoing = MakeCanonicalDoing(arg, &nValidDoing, &bValidDoing);
if (!bValidDoing)
{
szValidDoing = Empty;
nValidDoing = 0;
}
}
const bool bQuiet = ((key & DOING_QUIET) == DOING_QUIET);
key &= DOING_MASK;
if (key == DOING_MESSAGE)
{
int count = count_player_descs(executor);
if (count > 0)
{
set_doing_all(executor, szValidDoing, nValidDoing);
if ( !bQuiet
&& !Quiet(executor))
{
notify(executor, M_("Set."));
}
}
else
{
notify(executor, M_("Not connected."));
}
}
else if (key == DOING_UNIQUE)
{
if (set_doing_least_idle(executor, szValidDoing, nValidDoing))
{
if ( !bQuiet
&& !Quiet(executor))
{
notify(executor, M_("Set."));
}
}
else
{
notify(executor, M_("Not connected."));
}
}
else if (key == DOING_HEADER)
{
if (!Can_Poll(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
if (nValidDoing == 0)
{
mux_strncpy(mudstate.doing_hdr, M_("Doing"), sizeof(mudstate.doing_hdr)-1);
}
else
{
memcpy(mudstate.doing_hdr, szValidDoing, nValidDoing+1);
}
if ( !bQuiet
&& !Quiet(executor))
{
notify(executor, M_("Set."));
}
}
else // if (key == DOING_POLL)
{
notify(executor, tprintf(M_("Poll: %s"), mudstate.doing_hdr));
}
}
void list_siteinfo(dbref player)
{
list_siteinfo_via_driver(player);
}
/* ---------------------------------------------------------------------------
* make_ulist: Make a list of connected user numbers for the LWHO function.
*/
struct ulist_context
{
dbref viewer;
ITL *pitl;
};
static void ulist_callback(dbref player, void *ctx)
{
ulist_context *uc = static_cast<ulist_context *>(ctx);
if ( !See_Hidden(uc->viewer)
&& Hidden(player))
{
return;
}
ItemToList_AddInteger(uc->pitl, player);
}
void make_ulist(dbref player, UTF8 *buff, UTF8 **bufc, bool bPorts)
{
if (bPorts)
{
make_port_ulist(player, buff, bufc);
}
else
{
ITL pContext;
ItemToList_Init(&pContext, buff, bufc, '#');
ulist_context ctx = { player, &pContext };
for_each_connected_player(ulist_callback, &ctx);
ItemToList_Final(&pContext);
}
}
/* ---------------------------------------------------------------------------
* find_connected_name: Resolve a playername from the list of connected
* players using prefix matching. We only return a match if the prefix
* was unique.
*/
struct connected_name_context
{
dbref viewer;
const UTF8 *name;
dbref found;
bool ambiguous;
};
static void connected_name_callback(dbref player, void *ctx)
{
connected_name_context *cnc = static_cast<connected_name_context *>(ctx);
if (cnc->ambiguous)
{
return;
}
if ( Good_obj(cnc->viewer)
&& !See_Hidden(cnc->viewer)
&& Hidden(player))
{
return;
}
if (!string_prefix(Name(player), cnc->name))
{
return;
}
if ( cnc->found != NOTHING
&& cnc->found != player)
{
cnc->ambiguous = true;
return;
}
cnc->found = player;
}
dbref find_connected_name(const dbref player, const UTF8 *name)
{
connected_name_context ctx = { player, name, NOTHING, false };
for_each_connected_player(connected_name_callback, &ctx);
return ctx.ambiguous ? NOTHING : ctx.found;
}
FUNCTION(fun_poll)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_str(mudstate.doing_hdr, buff, bufc);
}
FUNCTION(fun_motd)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(fargs);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
safe_str(mudconf.motd_msg, buff, bufc);
}
// fetch_cmds - Retrieve Player's number of commands entered.
//
int fetch_cmds(const dbref target)
{
return sum_player_command_count(target);
}
static void ParseConnectionInfoString(UTF8 *pConnInfo, UTF8 *pFields[5])
{
string_token st(pConnInfo, T(" "));
for (int i = 0; i < 5; i++)
{
pFields[i] = st.parse();
}
}
void fetch_ConnectionInfoFields(dbref target, int64_t anFields[4])
{
dbref aowner;
int aflags;
LBuf pConnInfo = LBuf_Adopt(atr_get("fetch_ConnectionInfoFields.3263", target, A_CONNINFO, &aowner, &aflags));
UTF8 *aFields[5];
ParseConnectionInfoString(pConnInfo, aFields);
for (int i = 0; i < 4; i++)
{
int64_t result;
if ( !aFields[i]
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
|| (result = mux_atoi64(aFields[i])) < 0)
{
result = 0;
}
anFields[i] = result;
}
}
void put_ConnectionInfoFields
(
dbref target,
int64_t anFields[4],
CLinearTimeAbsolute &ltaLogout
)
{
LBuf pConnInfo = LBuf_Src("put_CIF");
UTF8 *p = pConnInfo.get();
for (int i = 0; i < 4; i++)
{
p += mux_i64toa(anFields[i], p);
*p++ = ' ';
}
p += mux_i64toa(ltaLogout.ReturnSeconds(), p);
*p++ = 0;
atr_add_raw_LEN(target, A_CONNINFO, pConnInfo, p - pConnInfo.get());
}
int64_t fetch_ConnectionInfoField(dbref target, int iField)
{
dbref aowner;
int aflags;
LBuf pConnInfo = LBuf_Adopt(atr_get("fetch_ConnectionInfoField.3305", target, A_CONNINFO, &aowner, &aflags));
UTF8 *aFields[5];
ParseConnectionInfoString(pConnInfo, aFields);
int64_t result;
if ( !aFields[iField]
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
|| (result = mux_atoi64(aFields[iField])) < 0)
{
result = 0;
}
return result;
}
#define CIF_LOGOUTTIME 4
CLinearTimeAbsolute fetch_logouttime(dbref target)
{
dbref aowner;
int aflags;
LBuf pConnInfo = LBuf_Adopt(atr_get("fetch_logouttime.3325", target, A_CONNINFO, &aowner, &aflags));
UTF8 *aFields[5];
ParseConnectionInfoString(pConnInfo, aFields);
CLinearTimeAbsolute lta;
if (aFields[CIF_LOGOUTTIME])
{
lta.SetSecondsString(aFields[CIF_LOGOUTTIME]);
}
else
{
lta.SetSeconds(0);
}
return lta;
}