tinymux/mux/modules/engine/help.cpp

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

/*! \file help.cpp
* \brief In-game help system.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
// Pointers to this struct is what gets stored in the help_htab's.
//
struct help_entry
{
size_t pos; // Position in file.
2018-10-03 17:54:51 +00:00
UTF8 *key; // The key this is stored under. nullptr if this is an
// automatically generated initial substring alias.
};
void helpindex_clean(int iHelpfile)
{
StringPtrMap *htab = mudstate.aHelpDesc[iHelpfile].ht;
2018-10-03 17:54:51 +00:00
if (nullptr == htab)
{
return;
}
for (auto &[key, val] : *htab)
{
struct help_entry *htab_entry = static_cast<struct help_entry *>(val);
if (htab_entry->key)
{
MEMFREE(htab_entry->key);
2018-10-03 17:54:51 +00:00
htab_entry->key = nullptr;
}
delete htab_entry;
}
delete mudstate.aHelpDesc[iHelpfile].ht;
2018-10-03 17:54:51 +00:00
mudstate.aHelpDesc[iHelpfile].ht = nullptr;
}
static int lineno;
static int ntopics;
static FILE *rfp;
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 Line[LBUF_SIZE];
static void HelpIndex_Start(FILE *fp)
{
lineno = 0;
ntopics = 0;
rfp = fp;
}
static bool HelpIndex_Read(size_t *pPos, size_t *nTopic, UTF8 pTopic[TOPIC_NAME_LEN+1])
{
size_t nLine = 0;
while ( 0 == nLine
|| '&' != Line[0])
{
if (fgets(reinterpret_cast<char *>(Line), LBUF_SIZE-2, rfp) == nullptr)
{
*pPos = 0L;
*nTopic = 0;
return false;
}
++lineno;
nLine = strlen(reinterpret_cast<char *>(Line));
*pPos += nLine;
if ( 0 < nLine
&& '\n' != Line[nLine - 1])
{
Log.tinyprintf(T("HelpIndex_Read, line %d: line too long" ENDLINE), lineno);
}
}
++ntopics;
UTF8 *topic = Line + 1;
while ( ' ' == *topic
|| '\t' == *topic
|| '\r' == *topic)
{
topic++;
}
UTF8 *s = topic;
size_t i = 0;
while ( '\n' != *s
&& '\r' != *s
&& '\0' != *s
&& i < TOPIC_NAME_LEN)
{
if ( ' ' != *s
|| ( 0 < i
&& ' ' != pTopic[i-1]))
{
pTopic[i++] = *s;
}
s++;
}
*nTopic = i;
pTopic[i] = '\0';
return true;
}
static void HelpIndex_End(void)
{
lineno = 0;
ntopics = 0;
2018-10-03 17:54:51 +00:00
rfp = nullptr;
}
static void helpindex_read(int iHelpfile)
{
helpindex_clean(iHelpfile);
mudstate.aHelpDesc[iHelpfile].ht = new StringPtrMap;
StringPtrMap *htab = mudstate.aHelpDesc[iHelpfile].ht;
UTF8 szTextFilename[SBUF_SIZE+8];
mux_sprintf(szTextFilename, sizeof(szTextFilename), T("%s.txt"),
mudstate.aHelpDesc[iHelpfile].pBaseFilename);
FILE *fp;
if (!mux_fopen(&fp, szTextFilename, T("rb")))
{
STARTLOG(LOG_PROBLEMS, "HLP", "RINDX");
LBuf p = LBuf_Src("helpindex_read.LOG");
mux_sprintf(p.get(), LBUF_SIZE, M_("Cant open %s for reading."), szTextFilename);
log_text(p);
ENDLOG;
return;
}
size_t pos = 0;
size_t nTopicOriginal = 0;
UTF8 topic[TOPIC_NAME_LEN+1];
HelpIndex_Start(fp);
while (HelpIndex_Read(&pos, &nTopicOriginal, topic))
{
// Convert the entry to all lowercase letters and add all leftmost
// substrings.
//
// Topic names which appear earlier in the help file have priority
// over topics names which appear later in the help file. That is,
// we do not associate prefixes with this topic if they have already
// been used on a previous topic.
//
size_t nCased;
UTF8 *pCased = mux_strlwr(topic, nCased);
bool bOriginal = true; // First is the longest.
for (size_t nTopic = nCased; 0 < nTopic; nTopic--)
{
// Avoid adding any entries with a trailing space.
//
if (mux_isspace(pCased[nTopic-1]))
{
continue;
}
std::vector<UTF8> vkey(pCased, pCased + nTopic);
auto it = htab->find(vkey);
struct help_entry *htab_entry = (it != htab->end())
? static_cast<struct help_entry *>(it->second) : nullptr;
if (htab_entry)
{
if (!bOriginal)
{
continue;
}
htab->erase(it);
if (htab_entry->key)
{
MEMFREE(htab_entry->key);
2018-10-03 17:54:51 +00:00
htab_entry->key = nullptr;
Log.tinyprintf(T("helpindex_read: duplicate %s entries for %s" ENDLINE),
szTextFilename, pCased);
}
delete htab_entry;
2018-10-03 17:54:51 +00:00
htab_entry = nullptr;
}
try
{
htab_entry = new struct help_entry;
}
catch (...)
{
; // Nothing.
}
if (htab_entry)
{
htab_entry->pos = pos;
2018-10-03 17:54:51 +00:00
htab_entry->key = bOriginal ? StringCloneLen(pCased, nTopic) : nullptr;
bOriginal = false;
htab->emplace(vkey, htab_entry);
}
}
}
HelpIndex_End();
mux_fclose(fp);
}
void helpindex_load(dbref player)
{
for (int i = 0; i < mudstate.nHelpDesc; i++)
{
helpindex_read(i);
}
if ( player != NOTHING
&& !Quiet(player))
{
notify(player, M_("Cache for help indexes refreshed."));
}
}
void helpindex_init(void)
{
helpindex_load(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
static const UTF8 *MakeCanonicalTopicName(const UTF8 *topic_arg, size_t &nTopic)
{
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 Buffer[LBUF_SIZE];
const UTF8 *topic;
if (topic_arg[0] == '\0')
{
topic = T("help");
nTopic = 4;
}
else
{
topic = mux_strlwr(topic_arg, nTopic);
memcpy(Buffer, topic, nTopic+1);
topic = Buffer;
}
return topic;
}
static void ReportMatchedTopics(dbref executor, const UTF8 *topic, StringPtrMap *htab)
{
bool matched = false;
LBuf topic_list = LBuf_Src("help_write");
UTF8 *buffp = topic_list.get();
for (auto &[key, val] : *htab)
{
struct help_entry *htab_entry = static_cast<struct help_entry *>(val);
mudstate.wild_invk_ctr = 0;
if ( htab_entry->key
&& quick_wild(topic, htab_entry->key))
{
matched = true;
safe_str(htab_entry->key, topic_list, &buffp);
safe_chr(' ', topic_list, &buffp);
safe_chr(' ', topic_list, &buffp);
}
}
if (!matched)
{
notify(executor, tprintf(M_("No entry for %s."), topic));
}
else
{
notify(executor, tprintf(M_("Here are the entries which match %s:"), topic));
*buffp = '\0';
notify(executor, topic_list);
}
}
static bool ReportTopic(dbref executor, struct help_entry *htab_entry, int iHelpfile,
UTF8 *result)
{
UTF8 szTextFilename[SBUF_SIZE+8];
mux_sprintf(szTextFilename, sizeof(szTextFilename), T("%s.txt"),
mudstate.aHelpDesc[iHelpfile].pBaseFilename);
size_t offset = htab_entry->pos;
FILE *fp;
if (!mux_fopen(&fp, szTextFilename, T("rb")))
{
STARTLOG(LOG_PROBLEMS, "HLP", "OPEN");
LBuf line = LBuf_Src("ReportTopic.open");
mux_sprintf(line.get(), LBUF_SIZE, M_("Cant open %s for reading."), szTextFilename);
log_text(line);
ENDLOG;
return false;
}
if (fseek(fp, static_cast<long>(offset), 0) < 0L)
{
STARTLOG(LOG_PROBLEMS, "HLP", "SEEK");
LBuf line = LBuf_Src("ReportTopic.seek");
mux_sprintf(line.get(), LBUF_SIZE, M_("Seek error in file %s."), szTextFilename);
log_text(line);
ENDLOG;
mux_fclose(fp);
return false;
}
LBuf line = LBuf_Src("ReportTopic");
UTF8 *bp = result;
bool bInTopicAliases = true;
for (;;)
{
if ( fgets(reinterpret_cast<char *>(line.get()), LBUF_SIZE - 2, fp) == nullptr
|| '\0' == line[0])
{
break;
}
if ('&' == line[0])
{
if (bInTopicAliases)
{
continue;
}
else
{
break;
}
}
bInTopicAliases = false;
// Transform LF into CRLF to be telnet-friendly.
//
size_t len = strlen(reinterpret_cast<char *>(line.get()));
if ( 0 < len
&& '\n' == line[len-1]
&& ( 1 == len
|| '\r' != line[len-2]))
{
line[len-1] = '\r';
line[len ] = '\n';
line[++len] = '\0';
}
bool bEval = mudstate.aHelpDesc[iHelpfile].bEval;
if (bEval)
{
2007-09-25 13:47:09 -07:00
dbref executor_for_help = executor;
if (Good_obj(mudconf.help_executor))
{
2007-09-25 13:47:09 -07:00
executor_for_help = mudconf.help_executor;
}
mux_exec(line, len, result, &bp, executor_for_help, executor, executor,
2018-10-03 17:54:51 +00:00
EV_NO_COMPRESS | EV_FIGNORE | EV_EVAL, nullptr, 0);
}
else
{
safe_str(line, result, &bp);
}
}
// Zap trailing CRLF if present.
//
if ( result < bp - 1
&& '\r' == bp[-2]
&& '\n' == bp[-1])
{
bp -= 2;
}
*bp = '\0';
mux_fclose(fp);
return true;
}
static void help_write(dbref executor, UTF8 *topic_arg, int iHelpfile)
{
size_t nTopic;
const UTF8 *topic = MakeCanonicalTopicName(topic_arg, nTopic);
StringPtrMap *htab = mudstate.aHelpDesc[iHelpfile].ht;
auto it = htab->find(std::vector<UTF8>(topic, topic + nTopic));
struct help_entry *htab_entry = (it != htab->end())
? static_cast<struct help_entry *>(it->second) : nullptr;
if (htab_entry)
{
LBuf result = LBuf_Src("help_write");
if (ReportTopic(executor, htab_entry, iHelpfile, result))
{
notify(executor, result);
}
else
{
notify(executor, M_("Sorry, that function is temporarily unavailable."));
}
}
else
{
ReportMatchedTopics(executor, topic, htab);
return;
}
}
static bool ValidateHelpFileIndex(int iHelpfile)
{
if ( iHelpfile < 0
|| mudstate.mHelpDesc <= iHelpfile)
{
UTF8 *buf = alloc_mbuf("do_help.LOG");
STARTLOG(LOG_BUGS, "BUG", "HELP");
mux_sprintf(buf, MBUF_SIZE, M_("Unknown help file number: %d"), iHelpfile);
log_text(buf);
ENDLOG;
free_mbuf(buf);
return false;
}
return true;
}
/*
* ---------------------------------------------------------------------------
* * do_help: display information from new-format news and help files
*/
void do_help(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *message, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
int iHelpfile = key;
if (!ValidateHelpFileIndex(iHelpfile))
{
notify(executor, M_("No such indexed file found."));
return;
}
help_write(executor, message, iHelpfile);
}
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
void help_helper(dbref executor, int iHelpfile, const UTF8 *topic_arg,
UTF8 *buff, UTF8 **bufc)
{
if (!ValidateHelpFileIndex(iHelpfile))
{
return;
}
size_t nTopic;
const UTF8 *topic = MakeCanonicalTopicName(topic_arg, nTopic);
StringPtrMap *htab = mudstate.aHelpDesc[iHelpfile].ht;
auto it = htab->find(std::vector<UTF8>(topic, topic + nTopic));
struct help_entry *htab_entry = (it != htab->end())
? static_cast<struct help_entry *>(it->second) : nullptr;
if (htab_entry)
{
LBuf result = LBuf_Src("help_helper");
if (ReportTopic(executor, htab_entry, iHelpfile, result))
{
safe_str(result, buff, bufc);
}
else
{
safe_str(S_("#-1 ERROR"), buff, bufc);
}
}
else
{
safe_str(S_("#-1 TOPIC DOES NOT EXIST"), buff, bufc);
}
}