tinymux/mux/modules/engine/help.cpp
Stephen Dennis 8808b4c375 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

478 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \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.
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;
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);
htab_entry->key = nullptr;
}
delete htab_entry;
}
delete mudstate.aHelpDesc[iHelpfile].ht;
mudstate.aHelpDesc[iHelpfile].ht = nullptr;
}
static int lineno;
static int ntopics;
static FILE *rfp;
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;
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);
htab_entry->key = nullptr;
Log.tinyprintf(T("helpindex_read: duplicate %s entries for %s" ENDLINE),
szTextFilename, pCased);
}
delete htab_entry;
htab_entry = nullptr;
}
try
{
htab_entry = new struct help_entry;
}
catch (...)
{
; // Nothing.
}
if (htab_entry)
{
htab_entry->pos = pos;
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);
}
static const UTF8 *MakeCanonicalTopicName(const UTF8 *topic_arg, size_t &nTopic)
{
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)
{
dbref executor_for_help = executor;
if (Good_obj(mudconf.help_executor))
{
executor_for_help = mudconf.help_executor;
}
mux_exec(line, len, result, &bp, executor_for_help, executor, executor,
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);
}
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);
}
}