/*! \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(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(Line), LBUF_SIZE-2, rfp) == nullptr) { *pPos = 0L; *nTopic = 0; return false; } ++lineno; nLine = strlen(reinterpret_cast(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_("Can’t 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 vkey(pCased, pCased + nTopic); auto it = htab->find(vkey); struct help_entry *htab_entry = (it != htab->end()) ? static_cast(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(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_("Can’t open %s for reading."), szTextFilename); log_text(line); ENDLOG; return false; } if (fseek(fp, static_cast(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(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(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(topic, topic + nTopic)); struct help_entry *htab_entry = (it != htab->end()) ? static_cast(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(topic, topic + nTopic)); struct help_entry *htab_entry = (it != htab->end()) ? static_cast(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); } }