tinymux/mux/modules/engine/comsys.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

5757 lines
161 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 comsys.cpp
* \brief Channel Communication System.
*
* The functions here manage channels, channel membership, the comsys.db, and
* the interaction of players and other objects with channels.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "mux_table.h"
#include <algorithm>
#include <unordered_map>
using namespace std;
static unordered_map<dbref, comsys_t> comsys_table;
// Forward decl for softcode gen-sync used by get_comsys / select_channel.
//
static void ensure_comsys_softcode_sync(void);
#define DFLT_MAX_LOG 0
#define MIN_RECALL_REQUEST 1
#define DFLT_RECALL_REQUEST 10
#define MAX_RECALL_REQUEST 200
// ---------------------------------------------------------------------------
// SQLite write-through helpers for comsys mutations.
// ---------------------------------------------------------------------------
#include "sqlite_backend.h"
#define SQLITE_COMSYS_WRITABLE() (!mudstate.bSQLiteLoading)
static void sqlite_wt_channel(struct channel *ch)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.SyncChannel(ch->name, ch->header,
ch->type, ch->temp1, ch->temp2,
ch->charge, ch->charge_who, ch->amount_col,
ch->num_messages, ch->chan_obj))
{
Log.tinyprintf(T("comsys sqlite_wt_channel failed for %s" ENDLINE), ch->name);
}
// Keep the loader's gate current: sqlite_load_comsys() (warm boot
// and db_unload -C) only reads the channel tables when has_comsys
// is set, but until now ONLY a DbConvert -C import set it -- a
// channel created in-game wrote through to the tables and was then
// ignored forever (the boot fell back to the legacy comsys.db
// flatfile). (#783)
//
if (!sqldb.PutMeta("has_comsys", 1))
{
Log.tinyprintf(T("comsys sqlite_wt_channel: has_comsys meta update failed" ENDLINE));
}
}
static void sqlite_wt_delete_channel(const UTF8 *name)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.DeleteChannel(name))
{
Log.tinyprintf(T("comsys sqlite_wt_delete_channel failed for %s" ENDLINE), name);
}
}
static void sqlite_wt_channel_user(const UTF8 *channel_name, struct comuser *user)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.SyncChannelUser(channel_name, user->who,
user->bUserIsOn, user->ComTitleStatus,
user->bGagJoinLeave,
reinterpret_cast<const UTF8 *>(user->title.c_str())))
{
Log.tinyprintf(T("comsys sqlite_wt_channel_user failed for %s/#%d" ENDLINE),
channel_name, user->who);
}
}
static void sqlite_wt_delete_channel_user(const UTF8 *channel_name, int who)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.DeleteChannelUser(channel_name, who))
{
Log.tinyprintf(T("comsys sqlite_wt_delete_channel_user failed for %s/#%d" ENDLINE),
channel_name, who);
}
}
static void sqlite_wt_player_channel(int who, const UTF8 *alias, const UTF8 *channel_name)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.SyncPlayerChannel(who, alias, channel_name))
{
Log.tinyprintf(T("comsys sqlite_wt_player_channel failed for #%d/%s" ENDLINE),
who, alias);
}
}
static void sqlite_wt_delete_player_channel(int who, const UTF8 *alias)
{
if (!SQLITE_COMSYS_WRITABLE()) return;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.DeletePlayerChannel(who, alias))
{
Log.tinyprintf(T("comsys sqlite_wt_delete_player_channel failed for #%d/%s" ENDLINE),
who, alias);
}
}
// #1199: Drop every player's aliases that point at a channel being destroyed.
// SQLite DeleteChannel CASCADE clears player_channels rows, but the in-memory
// comsys_table must be swept or stale aliases survive until restart.
//
static void purge_aliases_for_channel(const UTF8 *channel_name)
{
if ( nullptr == channel_name
|| '\0' == channel_name[0])
{
return;
}
for (auto &kv : comsys_table)
{
comsys_t &c = kv.second;
for (auto it = c.aliases.begin(); it != c.aliases.end(); )
{
if (0 == mux_stricmp(
reinterpret_cast<const UTF8 *>(it->channel.c_str()),
channel_name))
{
sqlite_wt_delete_player_channel(c.who,
reinterpret_cast<const UTF8 *>(it->alias.c_str()));
it = c.aliases.erase(it);
}
else
{
++it;
}
}
}
}
// Return value is a static buffer.
//
static UTF8* RestrictTitleValue(const UTF8* pTitleRequest)
{
// Remove all '\r\n\t' from the string.
// Terminate any ANSI in the string.
//
thread_local UTF8 NewTitle[MAX_TITLE_LEN + 1];
StripTabsAndTruncate(pTitleRequest, NewTitle, MAX_TITLE_LEN, MAX_TITLE_LEN);
return NewTitle;
}
static void do_setcomtitlestatus(const dbref player, struct channel* ch, const bool status)
{
struct comuser* user = select_user(ch, player);
if (ch && user)
{
user->ComTitleStatus = status;
sqlite_wt_channel_user(ch->name, user);
}
}
static void do_setgagjoinleavestatus(const dbref player, struct channel* ch, const bool status)
{
struct comuser* user = select_user(ch, player);
if (ch && user)
{
user->bGagJoinLeave = status;
sqlite_wt_channel_user(ch->name, user);
}
}
static void do_setnewtitle(const dbref player, struct channel* ch, const UTF8* pValidatedTitle)
{
struct comuser* user = select_user(ch, player);
if (ch && user)
{
user->title.assign(reinterpret_cast<const char *>(pValidatedTitle));
sqlite_wt_channel_user(ch->name, user);
}
}
// Save communication system data to disk.
//
void save_comsys(UTF8* filename)
{
UTF8 buffer[500];
mux_sprintf(buffer, sizeof(buffer), T("%s.#"), filename);
FILE* fp;
if (!mux_fopen(&fp, buffer, T("wb")))
{
Log.tinyprintf(T("Unable to open %s for writing." ENDLINE), buffer);
return;
}
mux_fprintf(fp, T("+V5\n"));
mux_fprintf(fp, T("*** Begin CHANNELS ***\n"));
save_channels(fp);
mux_fprintf(fp, T("*** Begin COMSYS ***\n"));
save_comsystem(fp);
mux_fclose(fp);
ReplaceFile(buffer, filename);
}
// Aliases must be between 1 and MAX_ALIAS_LEN characters. No spaces. No ANSI.
//
UTF8* MakeCanonicalComAlias
(
const UTF8* pAlias,
size_t* nValidAlias,
bool* bValidAlias
)
{
thread_local UTF8 Buffer[MAX_ALIAS_LEN + 1];
*nValidAlias = 0;
*bValidAlias = false;
if (!pAlias)
{
return nullptr;
}
size_t n = 0;
while (pAlias[n])
{
if (!mux_isprint_ascii(pAlias[n])
|| ' ' == pAlias[n])
{
return nullptr;
}
n++;
}
if (n < 1)
{
return nullptr;
}
if (MAX_ALIAS_LEN < n)
{
n = MAX_ALIAS_LEN;
}
memcpy(Buffer, pAlias, n);
Buffer[n] = '\0';
*nValidAlias = n;
*bValidAlias = true;
return Buffer;
}
static bool ParseChannelLine(UTF8* pBuffer, UTF8* pAlias5, UTF8** ppChannelName)
{
// Fetch alias portion. We need to find the first space.
//
auto p = reinterpret_cast<UTF8*>(strchr(reinterpret_cast<char*>(pBuffer), ' '));
if (!p)
{
return false;
}
*p = '\0';
bool bValidAlias;
size_t nValidAlias;
const UTF8* pValidAlias = MakeCanonicalComAlias(pBuffer, &nValidAlias, &bValidAlias);
if (!bValidAlias)
{
return false;
}
mux_strncpy(pAlias5, pValidAlias, MAX_ALIAS_LEN);
// Skip any leading space before the channel name.
//
p++;
while (mux_isspace(*p))
{
p++;
}
if (*p == '\0')
{
return false;
}
// The rest of the line is the channel name.
//
*ppChannelName = StringClone(p);
return true;
}
static bool ReadListOfNumbers(FILE* fp, const int cnt, int anum[])
{
UTF8 buffer[200];
if (fgets(reinterpret_cast<char*>(buffer), sizeof(buffer), fp))
{
UTF8* p = buffer;
for (int i = 0; i < cnt; i++)
{
if (mux_isdigit(p[0])
|| ('-' == p[0]
&& mux_isdigit(p[1])))
{
anum[i] = mux_atoi64(p);
do
{
p++;
}
while (mux_isdigit(*p));
if (' ' == *p)
{
p++;
}
}
else
{
return false;
}
}
if ('\n' == *p)
{
return true;
}
}
return false;
}
// Perform cleanup of comsystem data for players with 0 channels or player
// objects that were destroyed.
//
void purge_comsystem(void)
{
#ifdef ABORT_PURGE_COMSYS
return;
#endif // ABORT_PURGE_COMSYS
for (auto it = comsys_table.begin(); it != comsys_table.end(); )
{
const comsys_t &c = it->second;
if (c.aliases.empty())
{
it = comsys_table.erase(it);
continue;
}
if (isPlayer(c.who))
{
++it;
continue;
}
if (God(Owner(c.who))
&& Going(c.who))
{
it = comsys_table.erase(it);
}
else
{
++it;
}
}
}
// Save Comsys channel data to the indicated file.
//
void save_channels(FILE* fp)
{
purge_comsystem();
int np = static_cast<int>(comsys_table.size());
mux_fprintf(fp, T("%d\n"), np);
for (auto &kv : comsys_table)
{
const comsys_t &c = kv.second;
// Write user dbref and # of channels.
//
mux_fprintf(fp, T("%d %d\n"), c.who, static_cast<int>(c.aliases.size()));
for (size_t j = 0; j < c.aliases.size(); j++)
{
// Write channel alias and channel name.
//
mux_fprintf(fp, T("%s %s\n"),
reinterpret_cast<const UTF8 *>(c.aliases[j].alias.c_str()),
reinterpret_cast<const UTF8 *>(c.aliases[j].channel.c_str()));
}
}
}
static comsys_t* get_comsys(const dbref which)
{
ensure_comsys_softcode_sync();
if (which < 0)
{
return nullptr;
}
comsys_t &c = comsys_table[which];
c.who = which;
return &c;
}
void del_comsys(const dbref who)
{
comsys_table.erase(who);
}
// Sort aliases.
//
void sort_com_aliases(comsys_t &c)
{
std::sort(c.aliases.begin(), c.aliases.end(),
[](const com_alias &a, const com_alias &b) { return a.alias < b.alias; });
}
// Lookup player's comsys data and find the channel associated with
// the given alias.
//
static const UTF8* get_channel_from_alias(dbref player, UTF8* alias)
{
auto it = comsys_table.find(player);
if (it == comsys_table.end())
{
return T("");
}
const comsys_t &c = it->second;
for (const auto &ca : c.aliases)
{
if (ca.alias == reinterpret_cast<const char *>(alias))
{
return reinterpret_cast<const UTF8 *>(ca.channel.c_str());
}
}
return T("");
}
// Version 5 start on 2026-MAR-04
//
// -- Adds bGagJoinLeave as a 4th number on each user line.
//
void load_comsystem_V5(FILE* fp)
{
LBuf temp = LBuf_Src("load_comsystem_V5");
int nc = 0;
if (nullptr == fgets(reinterpret_cast<char*>(temp.get()), LBUF_SIZE, fp))
{
return;
}
nc = mux_atoi64(temp);
for (int i = 0; i < nc; i++)
{
int anum[10];
auto ch = new channel();
size_t nChannel = GetLineTrunc(temp, LBUF_SIZE, fp);
if (nChannel > MAX_CHANNEL_LEN)
{
nChannel = MAX_CHANNEL_LEN;
}
if (temp[nChannel - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nChannel--;
}
memcpy(ch->name, temp, nChannel);
ch->name[nChannel] = '\0';
size_t nHeader = GetLineTrunc(temp, LBUF_SIZE, fp);
if (nHeader > MAX_HEADER_LEN)
{
nHeader = MAX_HEADER_LEN;
}
if (temp[nHeader - 1] == '\n')
{
nHeader--;
}
memcpy(ch->header, temp, nHeader);
ch->header[nHeader] = '\0';
vector<UTF8> channel_name_vector(ch->name, ch->name+nChannel);
mudstate.channel_names.insert(make_pair(channel_name_vector, ch));
ch->type = 127;
ch->temp1 = 0;
ch->temp2 = 0;
ch->charge = 0;
ch->charge_who = NOTHING;
ch->amount_col = 0;
ch->num_messages = 0;
ch->chan_obj = NOTHING;
mux_assert(ReadListOfNumbers(fp, 8, anum));
ch->type = anum[0];
ch->temp1 = anum[1];
ch->temp2 = anum[2];
ch->charge = anum[3];
ch->charge_who = anum[4];
ch->amount_col = anum[5];
ch->num_messages = anum[6];
ch->chan_obj = anum[7];
int num_users_to_read = 0;
mux_assert(ReadListOfNumbers(fp, 1, &num_users_to_read));
if (num_users_to_read > 0)
{
for (int j = 0; j < num_users_to_read; j++)
{
comuser t_user;
t_user.who = NOTHING;
t_user.bUserIsOn = false;
t_user.ComTitleStatus = false;
mux_assert(ReadListOfNumbers(fp, 4, anum));
t_user.who = anum[0];
t_user.bUserIsOn = (anum[1] ? true : false);
t_user.ComTitleStatus = (anum[2] ? true : false);
t_user.bGagJoinLeave = (anum[3] ? true : false);
// Read Comtitle.
//
size_t nTitle = GetLineTrunc(temp, LBUF_SIZE, fp);
const UTF8* pTitle = temp;
if (!Good_dbref(t_user.who))
{
Log.tinyprintf(
T("load_comsystem: dbref %d out of range [0, %d)." ENDLINE), t_user.who, mudstate.db_top);
}
else if (isGarbage(t_user.who))
{
Log.tinyprintf(T("load_comsystem: dbref is GARBAGE." ENDLINE), t_user.who);
}
else
{
// Validate comtitle.
//
if (3 < nTitle && temp[0] == 't' && temp[1] == ':')
{
pTitle = temp + 2;
nTitle -= 2;
if (pTitle[nTitle - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nTitle--;
}
if (nTitle <= 0 || MAX_TITLE_LEN < nTitle)
{
nTitle = 0;
pTitle = temp;
}
}
else
{
nTitle = 0;
}
t_user.title.assign(reinterpret_cast<const char *>(pTitle), nTitle);
auto result = ch->users.emplace(t_user.who, std::move(t_user));
comuser &user = result.first->second;
if (!(isPlayer(user.who))
&& !(Going(user.who)
&& (God(Owner(user.who)))))
{
do_joinchannel(user.who, ch);
}
user.bConnected = true;
}
}
}
}
}
// Version 4 start on 2007-MAR-17
//
// -- Supports UTF-8 and ANSI as code-points.
// -- Relies on a version number at the top of the file instead of within
// this section.
//
void load_comsystem_V4(FILE* fp)
{
LBuf temp = LBuf_Src("load_comsystem_V4");
int nc = 0;
if (nullptr == fgets(reinterpret_cast<char*>(temp.get()), LBUF_SIZE, fp))
{
return;
}
nc = mux_atoi64(temp);
for (int i = 0; i < nc; i++)
{
int anum[10];
auto ch = new channel();
size_t nChannel = GetLineTrunc(temp, LBUF_SIZE, fp);
if (nChannel > MAX_CHANNEL_LEN)
{
nChannel = MAX_CHANNEL_LEN;
}
if (temp[nChannel - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nChannel--;
}
memcpy(ch->name, temp, nChannel);
ch->name[nChannel] = '\0';
size_t nHeader = GetLineTrunc(temp, LBUF_SIZE, fp);
if (nHeader > MAX_HEADER_LEN)
{
nHeader = MAX_HEADER_LEN;
}
if (temp[nHeader - 1] == '\n')
{
nHeader--;
}
memcpy(ch->header, temp, nHeader);
ch->header[nHeader] = '\0';
vector<UTF8> channel_name_vector(ch->name, ch->name+nChannel);
mudstate.channel_names.insert(make_pair(channel_name_vector, ch));
ch->type = 127;
ch->temp1 = 0;
ch->temp2 = 0;
ch->charge = 0;
ch->charge_who = NOTHING;
ch->amount_col = 0;
ch->num_messages = 0;
ch->chan_obj = NOTHING;
mux_assert(ReadListOfNumbers(fp, 8, anum));
ch->type = anum[0];
ch->temp1 = anum[1];
ch->temp2 = anum[2];
ch->charge = anum[3];
ch->charge_who = anum[4];
ch->amount_col = anum[5];
ch->num_messages = anum[6];
ch->chan_obj = anum[7];
int num_users_to_read = 0;
mux_assert(ReadListOfNumbers(fp, 1, &num_users_to_read));
if (num_users_to_read > 0)
{
for (int j = 0; j < num_users_to_read; j++)
{
comuser t_user;
t_user.who = NOTHING;
t_user.bUserIsOn = false;
t_user.ComTitleStatus = false;
mux_assert(ReadListOfNumbers(fp, 3, anum));
t_user.who = anum[0];
t_user.bUserIsOn = (anum[1] ? true : false);
t_user.ComTitleStatus = (anum[2] ? true : false);
// Read Comtitle.
//
size_t nTitle = GetLineTrunc(temp, LBUF_SIZE, fp);
const UTF8* pTitle = temp;
if (!Good_dbref(t_user.who))
{
Log.tinyprintf(
T("load_comsystem: dbref %d out of range [0, %d)." ENDLINE), t_user.who, mudstate.db_top);
}
else if (isGarbage(t_user.who))
{
Log.tinyprintf(T("load_comsystem: dbref is GARBAGE." ENDLINE), t_user.who);
}
else
{
// Validate comtitle.
//
if (3 < nTitle && temp[0] == 't' && temp[1] == ':')
{
pTitle = temp + 2;
nTitle -= 2;
if (pTitle[nTitle - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nTitle--;
}
if (nTitle <= 0 || MAX_TITLE_LEN < nTitle)
{
nTitle = 0;
pTitle = temp;
}
}
else
{
nTitle = 0;
}
t_user.title.assign(reinterpret_cast<const char *>(pTitle), nTitle);
auto result = ch->users.emplace(t_user.who, std::move(t_user));
comuser &user = result.first->second;
if (!(isPlayer(user.who))
&& !(Going(user.who)
&& (God(Owner(user.who)))))
{
do_joinchannel(user.who, ch);
}
user.bConnected = true;
}
}
}
}
}
// Load comsys database types 0, 1, 2 or 3.
// Used Prior to Version 4 (2007-MAR-17)
//
void load_comsystem_V0123(FILE* fp)
{
int ver = 0;
LBuf temp = LBuf_Src("load_comsystem_V0123");
int nc = 0;
if (nullptr == fgets(reinterpret_cast<char *>(temp.get()), LBUF_SIZE, fp))
{
return;
}
if (!strncmp(reinterpret_cast<char *>(temp.get()), "+V", 2))
{
// +V2 has colored headers.
//
ver = mux_atoi64(temp + 2);
if (ver < 1 || 3 < ver)
{
return;
}
mux_assert(ReadListOfNumbers(fp, 1, &nc));
}
else
{
nc = mux_atoi64(temp);
}
for (int i = 0; i < nc; i++)
{
int anum[10];
auto ch = new channel();
size_t nChannel = GetLineTrunc(temp, LBUF_SIZE, fp);
if (temp[nChannel - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nChannel--;
temp[nChannel] = '\0';
}
// Convert entire line to UTF-8 including ANSI escapes.
//
const UTF8* pBufferUnicode = ConvertToUTF8(reinterpret_cast<char*>(temp.get()), &nChannel);
if (MAX_CHANNEL_LEN < nChannel)
{
nChannel = MAX_CHANNEL_LEN;
// Back off to a whole-character boundary in the CONVERTED buffer
// (pBufferUnicode), the data actually copied below — not temp, the
// pre-conversion line (the header case just below gets this right).
//
while (0 < nChannel
&& UTF8_CONTINUE <= utf8_FirstByte[static_cast<unsigned char>(pBufferUnicode[nChannel - 1])])
{
nChannel--;
}
}
memcpy(ch->name, pBufferUnicode, nChannel);
ch->name[nChannel] = '\0';
if (ver >= 2)
{
size_t nHeader = GetLineTrunc(temp, LBUF_SIZE, fp);
if (temp[nHeader - 1] == '\n')
{
nHeader--;
temp[nHeader] = '\0';
}
// Convert entire line to UTF-8 including ANSI escapes.
//
pBufferUnicode = ConvertToUTF8(reinterpret_cast<char*>(temp.get()), &nHeader);
if (MAX_HEADER_LEN < nHeader)
{
nHeader = MAX_HEADER_LEN;
while (0 < nHeader
&& UTF8_CONTINUE <= utf8_FirstByte[static_cast<unsigned char>(pBufferUnicode[nHeader - 1])])
{
nHeader--;
}
}
memcpy(ch->header, pBufferUnicode, nHeader);
ch->header[nHeader] = '\0';
}
vector<UTF8> channel_name_vector(ch->name, ch->name + nChannel);
mudstate.channel_names.insert(make_pair(channel_name_vector, ch));
ch->type = 127;
ch->temp1 = 0;
ch->temp2 = 0;
ch->charge = 0;
ch->charge_who = NOTHING;
ch->amount_col = 0;
ch->num_messages = 0;
ch->chan_obj = NOTHING;
if (ver >= 1)
{
mux_assert(ReadListOfNumbers(fp, 8, anum));
ch->type = anum[0];
ch->temp1 = anum[1];
ch->temp2 = anum[2];
ch->charge = anum[3];
ch->charge_who = anum[4];
ch->amount_col = anum[5];
ch->num_messages = anum[6];
ch->chan_obj = anum[7];
}
else
{
mux_assert(ReadListOfNumbers(fp, 10, anum));
ch->type = anum[0];
// anum[1] is not used.
ch->temp1 = anum[2];
ch->temp2 = anum[3];
// anum[4] is not used.
ch->charge = anum[5];
ch->charge_who = anum[6];
ch->amount_col = anum[7];
ch->num_messages = anum[8];
ch->chan_obj = anum[9];
}
if (ver <= 1)
{
// Build colored header if not +V2 or later db.
//
if (ch->type & CHANNEL_PUBLIC)
{
mux_sprintf(temp, LBUF_SIZE, T("%s[%s%s%s%s%s]%s"), COLOR_FG_CYAN, COLOR_INTENSE,
COLOR_FG_BLUE, ch->name, COLOR_RESET, COLOR_FG_CYAN, COLOR_RESET);
}
else
{
mux_sprintf(temp, LBUF_SIZE, T("%s[%s%s%s%s%s]%s"), COLOR_FG_MAGENTA, COLOR_INTENSE,
COLOR_FG_RED, ch->name, COLOR_RESET, COLOR_FG_MAGENTA,
COLOR_RESET);
}
StripTabsAndTruncate(temp, ch->header, MAX_HEADER_LEN, MAX_HEADER_LEN);
}
int num_users_to_read = 0;
mux_assert(ReadListOfNumbers(fp, 1, &num_users_to_read));
if (num_users_to_read > 0)
{
for (int j = 0; j < num_users_to_read; j++)
{
comuser t_user;
t_user.who = NOTHING;
t_user.bUserIsOn = false;
t_user.ComTitleStatus = false;
if (ver == 3)
{
mux_assert(ReadListOfNumbers(fp, 3, anum));
t_user.who = anum[0];
t_user.bUserIsOn = (anum[1] ? true : false);
t_user.ComTitleStatus = (anum[2] ? true : false);
}
else
{
t_user.ComTitleStatus = true;
if (ver)
{
mux_assert(ReadListOfNumbers(fp, 2, anum));
t_user.who = anum[0];
t_user.bUserIsOn = (anum[1] ? true : false);
}
else
{
mux_assert(ReadListOfNumbers(fp, 4, anum));
t_user.who = anum[0];
// anum[1] is not used.
// anum[2] is not used.
t_user.bUserIsOn = (anum[3] ? true : false);
}
}
// Read Comtitle.
//
size_t nTitle = GetLineTrunc(temp, LBUF_SIZE, fp);
// Convert entire line to UTF-8 including ANSI escapes.
//
UTF8* pTitle = ConvertToUTF8(reinterpret_cast<char*>(temp.get()), &nTitle);
if (!Good_dbref(t_user.who))
{
Log.tinyprintf(
T("load_comsystem: dbref %d out of range [0, %d)." ENDLINE), t_user.who, mudstate.db_top);
}
else if (isGarbage(t_user.who))
{
Log.tinyprintf(T("load_comsystem: dbref is GARBAGE." ENDLINE), t_user.who);
}
else
{
// Validate comtitle.
//
if (3 < nTitle && pTitle[0] == 't' && pTitle[1] == ':')
{
pTitle = pTitle + 2;
nTitle -= 2;
if (pTitle[nTitle - 1] == '\n')
{
// Get rid of trailing '\n'.
//
nTitle--;
}
if (nTitle <= 0
|| MAX_TITLE_LEN < nTitle)
{
nTitle = 0;
pTitle = temp;
}
}
else
{
nTitle = 0;
}
t_user.title.assign(reinterpret_cast<const char *>(pTitle), nTitle);
auto result = ch->users.emplace(t_user.who, std::move(t_user));
comuser &user = result.first->second;
if (!(isPlayer(user.who))
&& !(Going(user.who)
&& (God(Owner(user.who)))))
{
do_joinchannel(user.who, ch);
}
user.bConnected = true;
}
}
}
}
}
void load_channels_V4(FILE* fp)
{
LBuf buffer = LBuf_Src("load_channels_V4");
int np = 0;
mux_assert(ReadListOfNumbers(fp, 1, &np));
for (int i = 0; i < np; i++)
{
int anum[2];
int who = 0;
int numchannels = 0;
mux_assert(ReadListOfNumbers(fp, 2, anum));
who = anum[0];
numchannels = anum[1];
comsys_t c;
c.who = who;
if (numchannels > 0)
{
for (int j = 0; j < numchannels; j++)
{
size_t n = GetLineTrunc(buffer, LBUF_SIZE, fp);
if (buffer[n - 1] == '\n')
{
// Get rid of trailing '\n'.
//
n--;
buffer[n] = '\0';
}
UTF8 aliasBuf[MAX_ALIAS_LEN + 1];
UTF8 *channelName = nullptr;
if (ParseChannelLine(buffer, aliasBuf, &channelName))
{
com_alias ca;
ca.alias.assign(reinterpret_cast<const char *>(aliasBuf));
ca.channel.assign(reinterpret_cast<const char *>(channelName));
c.aliases.push_back(std::move(ca));
MEMFREE(channelName);
}
}
sort_com_aliases(c);
}
if (Good_obj(c.who))
{
comsys_table[c.who] = std::move(c);
}
else
{
Log.tinyprintf(T("Invalid dbref %d." ENDLINE), who);
}
purge_comsystem();
}
}
void load_channels_V0123(FILE* fp)
{
char buffer[LBUF_SIZE];
int np = 0;
mux_assert(ReadListOfNumbers(fp, 1, &np));
for (int i = 0; i < np; i++)
{
int anum[2];
int who = 0;
int numchannels = 0;
mux_assert(ReadListOfNumbers(fp, 2, anum));
who = anum[0];
numchannels = anum[1];
comsys_t c;
c.who = who;
if (numchannels > 0)
{
for (int j = 0; j < numchannels; j++)
{
size_t n = GetLineTrunc(reinterpret_cast<UTF8*>(buffer), sizeof(buffer), fp);
if (buffer[n - 1] == '\n')
{
// Get rid of trailing '\n'.
//
n--;
buffer[n] = '\0';
}
// Convert the entire line to UTF8 before parsing the line
// into fields. The first field no ANSI in it anyway.
//
size_t nBufferUnicode;
UTF8* pBufferUnicode = ConvertToUTF8(buffer, &nBufferUnicode);
UTF8 aliasBuf[MAX_ALIAS_LEN + 1];
UTF8 *channelName = nullptr;
if (ParseChannelLine(pBufferUnicode, aliasBuf, &channelName))
{
com_alias ca;
ca.alias.assign(reinterpret_cast<const char *>(aliasBuf));
ca.channel.assign(reinterpret_cast<const char *>(channelName));
c.aliases.push_back(std::move(ca));
MEMFREE(channelName);
}
}
sort_com_aliases(c);
}
if (Good_obj(c.who))
{
comsys_table[c.who] = std::move(c);
}
else
{
Log.tinyprintf(T("Invalid dbref %d." ENDLINE), who);
}
purge_comsystem();
}
}
void load_comsys_V5(FILE* fp)
{
char buffer[200];
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin CHANNELS ***\n") == 0)
{
load_channels_V4(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin CHANNELS." ENDLINE));
return;
}
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin COMSYS ***\n") == 0)
{
load_comsystem_V5(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin COMSYS." ENDLINE));
}
}
void load_comsys_V4(FILE* fp)
{
char buffer[200];
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin CHANNELS ***\n") == 0)
{
load_channels_V4(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin CHANNELS." ENDLINE));
return;
}
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin COMSYS ***\n") == 0)
{
load_comsystem_V4(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin COMSYS." ENDLINE));
}
}
void load_comsys_V0123(FILE* fp)
{
char buffer[200];
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin CHANNELS ***\n") == 0)
{
load_channels_V0123(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin CHANNELS." ENDLINE));
return;
}
if (fgets(buffer, sizeof(buffer), fp)
&& strcmp(buffer, "*** Begin COMSYS ***\n") == 0)
{
load_comsystem_V0123(fp);
}
else
{
Log.tinyprintf(T("Error: Couldnt find Begin COMSYS." ENDLINE));
}
}
// Open the given filename, check version, and attempt to read comsystem
// data as indicated by the version number at the head of the file.
//
void load_comsys(UTF8* filename)
{
comsys_table.clear();
FILE* fp;
if (!mux_fopen(&fp, filename, T("rb")))
{
Log.tinyprintf(T("Error: Couldnt find %s." ENDLINE), filename);
}
else
{
Log.tinyprintf(T("LOADING: %s" ENDLINE), filename);
const int ch = getc(fp);
if (EOF == ch)
{
Log.tinyprintf(T("Error: Couldnt read first byte."));
}
else
{
ungetc(ch, fp);
if ('+' == ch)
{
// Version 4 or later.
//
UTF8 nbuf1[8];
// Read the version number.
//
if (fgets(reinterpret_cast<char*>(nbuf1), sizeof(nbuf1), fp))
{
if (strncmp(reinterpret_cast<char*>(nbuf1), "+V5", 3) == 0)
{
// Started v5 on 2026-MAR-04.
//
load_comsys_V5(fp);
}
else if (strncmp(reinterpret_cast<char*>(nbuf1), "+V4", 3) == 0)
{
// Started v4 on 2007-MAR-13.
//
load_comsys_V4(fp);
}
}
}
else
{
load_comsys_V0123(fp);
}
}
mux_fclose(fp);
Log.tinyprintf(T("LOADING: %s (done)" ENDLINE), filename);
}
}
// Save channel data and some user state info on a per-channel basis.
//
void save_comsystem(FILE* fp)
{
// Number of channels.
//
mux_fprintf(fp, T("%d\n"), static_cast<int>(mudstate.channel_names.size()));
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
const auto ch = it->second;
// Channel name.
//
mux_fprintf(fp, T("%s\n"), ch->name);
// Channel header.
//
mux_fprintf(fp, T("%s\n"), ch->header);
mux_fprintf(fp, T("%d %d %d %d %d %d %d %d\n"), ch->type, ch->temp1,
ch->temp2, ch->charge, ch->charge_who, ch->amount_col,
ch->num_messages, ch->chan_obj);
// Count the number of 'valid' users to dump.
//
int number_of_valid_users = 0;
for (auto &kv : ch->users)
{
const comuser &user = kv.second;
if (user.who >= 0 && user.who < mudstate.db_top)
{
number_of_valid_users++;
}
}
// Number of users on this channel.
//
mux_fprintf(fp, T("%d\n"), number_of_valid_users);
for (auto &kv : ch->users)
{
const comuser &user = kv.second;
if (user.who >= 0 && user.who < mudstate.db_top)
{
// Write user state: dbref, on flag, comtitle status, and gag status.
//
mux_fprintf(fp, T("%d %d %d %d\n"), user.who, user.bUserIsOn, user.ComTitleStatus, user.bGagJoinLeave);
// Write user title data.
//
if (!user.title.empty())
{
mux_fprintf(fp, T("t:%s\n"), reinterpret_cast<const UTF8 *>(user.title.c_str()));
}
else
{
mux_fprintf(fp, T("t:\n"));
}
}
}
}
}
// ---------------------------------------------------------------------------
// call_mogrifier: Evaluate a MOGRIFY`<suffix> attribute on the channel
// object. Returns an alloc_lbuf'd result (caller must free_lbuf) or
// nullptr if the attribute doesn't exist or is empty.
//
// Args passed as %0..%N-1.
//
static UTF8 *call_mogrifier
(
const dbref chan_obj,
const dbref executor,
const UTF8 *suffix,
const UTF8 *args[],
int nargs
)
{
if (!Good_obj(chan_obj))
{
return nullptr;
}
// Build attribute name: MOGRIFY`<suffix>
//
UTF8 aname[SBUF_SIZE];
mux_sprintf(aname, sizeof(aname), T("MOGRIFY`%s"), suffix);
ATTR *pattr = atr_str(aname);
if (!pattr)
{
return nullptr;
}
dbref aowner;
int aflags;
LBuf atext = LBuf_Adopt(atr_pget(chan_obj, pattr->number, &aowner, &aflags));
if (!*atext.get())
{
return nullptr;
}
LBufPtr result = LBufPtr_Src("call_mogrifier");
UTF8 *bp = result;
mux_exec(atext, LBUF_SIZE-1, result, &bp, chan_obj, chan_obj, executor,
AttrTrace(aflags, EV_FCHECK|EV_EVAL|EV_TOP),
args, nargs);
*bp = '\0';
return result.release();
}
// Speaker portion of a channel line: the comtitle (evaluated as code when
// eval_comtitle is set) and the speaker's name, with SPOOF replacing the
// name by the title. Extracted from BuildChannelMessage so the join/leave
// announcements can compose a whole translatable sentence around the same
// speaker text (#1723) instead of appending a pose fragment to it.
//
// Appends into the caller's buffers via safe_* pointers. pncptr may be
// null (SPOOF has no no-comtitle variant).
//
static void BuildChannelSpeaker
(
const bool bSpoof,
const struct comuser* user,
UTF8* spkNormal,
UTF8** pnptr,
UTF8* spkNoComtitle,
UTF8** pncptr
)
{
const bool hasComTitle = !user->title.empty();
if (hasComTitle && (user->ComTitleStatus || bSpoof))
{
if (mudconf.eval_comtitle)
{
// Evaluate the comtitle as code.
//
LBuf TempToEval = LBuf_Src("eval_comtitle");
mux_strncpy(TempToEval, reinterpret_cast<const UTF8 *>(user->title.c_str()), LBUF_SIZE - 1);
mux_exec(TempToEval, LBUF_SIZE - 1, spkNormal, pnptr, user->who, user->who, user->who,
EV_FCHECK | EV_EVAL | EV_TOP, nullptr, 0);
}
else
{
safe_str(reinterpret_cast<const UTF8 *>(user->title.c_str()), spkNormal, pnptr);
}
if (!bSpoof)
{
safe_chr(' ', spkNormal, pnptr);
safe_str(Moniker(user->who), spkNormal, pnptr);
safe_str(Moniker(user->who), spkNoComtitle, pncptr);
}
}
else
{
safe_str(Moniker(user->who), spkNormal, pnptr);
if (!bSpoof)
{
safe_str(Moniker(user->who), spkNoComtitle, pncptr);
}
}
}
// A whole-sentence channel announcement (#1723). Join and leave are the
// server speaking about an event -- unlike speech, which is scaffolding
// around a player's own words -- so they are translatable sentences:
// pChannelAnnounceFmt is an M_()-resolved format with one %s, which receives the
// comtitle-decorated speaker. These are the same msgids the comsys module
// has always used, so the two implementations converge (#1614) and the
// existing catalogue entries serve both.
//
// SPEECHMOD is still honoured, as the old pose path did -- but %0 is now
// the whole announcement ("Jess has joined this channel.") rather than the
// bare verb phrase, because the verb phrase no longer exists as a unit.
// Run per variant, so an evaluated attribute now evaluates once per
// audience rather than once in total.
//
static void BuildChannelAnnounce
(
const bool bSpoof,
const UTF8* pHeader,
const struct comuser* user,
const dbref ch_obj,
const UTF8* pChannelAnnounceFmt,
UTF8** messNormal,
UTF8** messNoComtitle
)
{
UTF8 *spkNormal = alloc_lbuf("BCA.spkN");
UTF8 *spkNoComtitle = nullptr;
UTF8 *snp = spkNormal;
UTF8 *sncp = nullptr;
if (!bSpoof)
{
spkNoComtitle = alloc_lbuf("BCA.spkNC");
sncp = spkNoComtitle;
}
BuildChannelSpeaker(bSpoof, user, spkNormal, &snp, spkNoComtitle, &sncp);
*snp = '\0';
if (nullptr != sncp)
{
*sncp = '\0';
}
bool bChannelSpeechMod = false;
if (Good_obj(ch_obj))
{
dbref aowner;
int aflags;
LBuf test_attr = LBuf_Adopt(atr_get("BuildChannelAnnounce.1", ch_obj,
A_SPEECHMOD, &aowner, &aflags));
if ('\0' != test_attr[0])
{
bChannelSpeechMod = true;
}
}
struct
{
const UTF8 *pSpeaker;
UTF8 **ppMess;
const char *pTag;
} variants[2] = {
{ spkNormal, messNormal, "BCA.messNormal" },
{ spkNoComtitle, messNoComtitle, "BCA.messNoComtitle" },
};
for (int i = 0; i < 2; i++)
{
if (nullptr == variants[i].pSpeaker)
{
*variants[i].ppMess = nullptr;
continue;
}
UTF8 *sentence = alloc_lbuf("BCA.sentence");
UTF8 *sp = sentence;
safe_tprintf_str(sentence, &sp, pChannelAnnounceFmt, variants[i].pSpeaker);
*sp = '\0';
UTF8 *modded = modSpeech(bChannelSpeechMod ? ch_obj : user->who,
sentence, true, T("channel/pose"));
*variants[i].ppMess = alloc_lbuf(variants[i].pTag);
UTF8 *mp = *variants[i].ppMess;
safe_str(pHeader, *variants[i].ppMess, &mp);
safe_chr(' ', *variants[i].ppMess, &mp);
safe_str(nullptr != modded ? modded : sentence,
*variants[i].ppMess, &mp);
*mp = '\0';
if (nullptr != modded)
{
free_lbuf(modded);
}
free_lbuf(sentence);
}
free_lbuf(spkNormal);
if (nullptr != spkNoComtitle)
{
free_lbuf(spkNoComtitle);
}
}
static void BuildChannelMessage
(
const bool bSpoof,
const UTF8* pHeader,
const struct comuser* user,
const dbref ch_obj,
const UTF8* pPose,
UTF8** messNormal,
UTF8** messNoComtitle
)
{
// Allocate necessary buffers.
//
*messNormal = alloc_lbuf("BCM.messNormal");
*messNoComtitle = nullptr;
if (!bSpoof)
{
*messNoComtitle = alloc_lbuf("BCM.messNoComtitle");
}
UTF8* mnptr = *messNormal; // Message without comtitle removal.
UTF8* mncptr = *messNoComtitle; // Message with comtitle removal.
safe_str(pHeader, *messNormal, &mnptr);
safe_chr(' ', *messNormal, &mnptr);
if (!bSpoof)
{
safe_str(pHeader, *messNoComtitle, &mncptr);
safe_chr(' ', *messNoComtitle, &mncptr);
}
BuildChannelSpeaker(bSpoof, user, *messNormal, &mnptr,
*messNoComtitle, &mncptr);
bool bChannelSayString = false;
bool bChannelSpeechMod = false;
if (Good_obj(ch_obj))
{
dbref aowner;
int aflags;
{
LBuf test_attr = LBuf_Adopt(atr_get("BuildChannelMessage.1304", ch_obj,
A_SAYSTRING, &aowner, &aflags));
if ('\0' != test_attr[0])
{
bChannelSayString = true;
}
}
{
LBuf test_attr = LBuf_Adopt(atr_get("BuildChannelMessage.1312", ch_obj,
A_SPEECHMOD, &aowner, &aflags));
if ('\0' != test_attr[0])
{
bChannelSpeechMod = true;
}
}
}
UTF8* saystring = nullptr;
UTF8* newPose = nullptr;
switch (pPose[0])
{
case ':':
pPose++;
if (' ' == *pPose)
{
pPose++;
}
else
{
safe_chr(' ', *messNormal, &mnptr);
if (!bSpoof)
{
safe_chr(' ', *messNoComtitle, &mncptr);
}
}
newPose = modSpeech(bChannelSpeechMod ? ch_obj : user->who, pPose, true, T("channel/pose"));
if (newPose)
{
pPose = newPose;
}
safe_str(pPose, *messNormal, &mnptr);
if (!bSpoof)
{
safe_str(pPose, *messNoComtitle, &mncptr);
}
break;
case ';':
pPose++;
newPose = modSpeech(bChannelSpeechMod ? ch_obj : user->who, pPose, true, T("channel/pose"));
if (newPose)
{
pPose = newPose;
}
safe_str(pPose, *messNormal, &mnptr);
if (!bSpoof)
{
safe_str(pPose, *messNoComtitle, &mncptr);
}
break;
default:
newPose = modSpeech(bChannelSpeechMod ? ch_obj : user->who, pPose, true, T("channel"));
if (newPose)
{
pPose = newPose;
}
saystring = modSpeech(bChannelSayString ? ch_obj : user->who, pPose, false, T("channel"));
if (saystring)
{
safe_chr(' ', *messNormal, &mnptr);
safe_str(saystring, *messNormal, &mnptr);
safe_str(T(""), *messNormal, &mnptr);
}
else
{
safe_str(T(" says, “"), *messNormal, &mnptr);
}
safe_str(pPose, *messNormal, &mnptr);
safe_str(T(""), *messNormal, &mnptr);
if (!bSpoof)
{
if (saystring)
{
safe_chr(' ', *messNoComtitle, &mncptr);
safe_str(saystring, *messNoComtitle, &mncptr);
safe_str(T(""), *messNoComtitle, &mncptr);
}
else
{
safe_str(T(" says, “"), *messNoComtitle, &mncptr);
}
safe_str(pPose, *messNoComtitle, &mncptr);
safe_str(T(""), *messNoComtitle, &mncptr);
}
break;
}
*mnptr = '\0';
if (!bSpoof)
{
*mncptr = '\0';
}
if (newPose)
{
free_lbuf(newPose);
}
if (saystring)
{
free_lbuf(saystring);
}
}
static void do_processcom(dbref player, UTF8* arg1, UTF8* arg2)
{
if (!*arg2)
{
raw_notify(player, M_("No message."));
return;
}
if (3500 < strlen(reinterpret_cast<const char*>(arg2)))
{
arg2[3500] = '\0';
}
struct channel* ch = select_channel(arg1);
if (!ch)
{
raw_notify(player, tprintf(M_("Unknown channel %s."), arg1));
return;
}
struct comuser* user = select_user(ch, player);
if (!user)
{
raw_notify(player, M_("You are not listed as on that channel. Delete this alias and readd."));
return;
}
if (Gagged(player)
&& !Wizard(player))
{
raw_notify(player, M_("GAGGED players may not speak on channels."));
return;
}
if (!strcmp(reinterpret_cast<const char*>(arg2), "on"))
{
do_joinchannel(player, ch);
}
else if (!strcmp(reinterpret_cast<const char*>(arg2), "off"))
{
do_leavechannel(player, ch);
}
else if (!user->bUserIsOn)
{
raw_notify(player, tprintf(M_("You must be on %s to do that."), arg1));
}
else if (!strcmp(reinterpret_cast<const char*>(arg2), "who"))
{
do_comwho(player, ch);
}
else if (!strncmp(reinterpret_cast<const char*>(arg2), "last", 4)
&& (arg2[4] == '\0'
|| (arg2[4] == ' '
&& is_integer(arg2 + 5, nullptr))))
{
// Parse optional number after the 'last' command.
//
int nRecall = DFLT_RECALL_REQUEST;
if (arg2[4] == ' ')
{
nRecall = mux_atoi64(arg2 + 5);
}
do_comlast(player, ch, nRecall);
}
else if (!test_transmit_access(player, ch))
{
raw_notify(player, M_("That channel type cannot be transmitted on."));
}
else
{
if (!payfor(player, Guest(player) ? 0 : ch->charge))
{
raw_notify(player, tprintf(M_("You dont have enough %s."), mudconf.many_coins));
return;
}
ch->amount_col += ch->charge;
sqlite_wt_channel(ch);
giveto(ch->charge_who, ch->charge);
// Check MOGRIFY`BLOCK on the channel object. If it returns a
// non-empty string, send that string to the speaker and suppress
// the message entirely.
//
if (Good_obj(ch->chan_obj))
{
UTF8 chattype[2];
chattype[0] = (arg2[0] == ':' || arg2[0] == ';') ? arg2[0] : '"';
chattype[1] = '\0';
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"), player);
const UTF8 *block_args[5] = {
chattype, ch->name, arg2, (const UTF8 *)Name(player), sdrBuf
};
UTF8 *block_result = call_mogrifier(ch->chan_obj, player,
T("BLOCK"), block_args, 5);
if (block_result)
{
if (*block_result)
{
raw_notify(player, block_result);
free_lbuf(block_result);
return;
}
free_lbuf(block_result);
}
}
// BuildChannelMessage allocates messNormal and messNoComtitle,
// SendChannelMessage frees them.
//
UTF8* messNormal;
UTF8* messNoComtitle;
BuildChannelMessage((ch->type & CHANNEL_SPOOF) != 0, ch->header, user,
ch->chan_obj, arg2, &messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle);
}
}
inline void notify_comsys(const dbref target, const dbref sender, const UTF8 *msg)
{
notify_with_cause_ooc(target, sender, msg, MSG_SRC_COMSYS);
}
// Transmit the given message as appropriate to all listening parties.
// Perform channel message logging, if configured, for the channel.
//
void SendChannelMessage
(
const dbref executor,
struct channel* ch,
UTF8* msgNormal,
UTF8* msgNoComtitle,
const bool bJoinLeaveMsg
)
{
// Transmit messages.
//
const bool bSpoof = ((ch->type & CHANNEL_SPOOF) != 0);
ch->num_messages++;
sqlite_wt_channel(ch);
// Look up the CHATFORMAT attribute number, caching only SUCCESS (#1572).
//
// This used to latch on the first attempt whatever the outcome.
// CHATFORMAT is a vattr, created the first time any player sets one, so
// on a game where no player had one before the first channel message the
// number cached as 0 and CHATFORMAT was ignored for the entire life of
// the process -- including for every player who set one afterwards.
// Restarting fixed it, which is exactly the shape that never gets
// reported as a bug.
//
// Measured: with CHATFORMAT set before any channel traffic both
// implementations honoured it; set after, the engine ignored it and the
// module did not, which is how this surfaced.
//
// Caching only a hit keeps the fast path -- one lookup ever, once some
// player has a CHATFORMAT -- and costs a failed atr_str per message only
// on games where nobody uses the feature at all.
//
static int s_chatformat_atr = 0;
if (0 == s_chatformat_atr)
{
ATTR *pattr = atr_str(T("CHATFORMAT"));
if (nullptr != pattr)
{
s_chatformat_atr = pattr->number;
}
}
// Evaluate mogrifier attributes on the channel object, if present.
// These run once per message, not per listener.
//
// MOGRIFY`MESSAGE — transform the composed message text.
// MOGRIFY`FORMAT — channel-wide format (like per-player CHATFORMAT).
// MOGRIFY`OVERRIDE — if true, skip per-player CHATFORMAT.
// MOGRIFY`NOBUFFER — if true, skip recall buffer logging.
//
UTF8 *mogMsg = nullptr;
UTF8 *mogFmt = nullptr;
bool bMogOverride = false;
bool bMogNobuffer = false;
if ( Good_obj(ch->chan_obj)
&& !bJoinLeaveMsg)
{
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"), executor);
const UTF8 *mog_args[3] = { ch->name, msgNormal, sdrBuf };
// MOGRIFY`MESSAGE: if non-empty, replaces the message text.
//
mogMsg = call_mogrifier(ch->chan_obj, executor,
T("MESSAGE"), mog_args, 3);
// MOGRIFY`OVERRIDE: if true (non-zero), skip per-player CHATFORMAT.
//
UTF8 *mogOvr = call_mogrifier(ch->chan_obj, executor,
T("OVERRIDE"), mog_args, 3);
if (mogOvr)
{
bMogOverride = xlate(mogOvr);
free_lbuf(mogOvr);
}
// MOGRIFY`NOBUFFER: if true (non-zero), skip recall buffer.
//
UTF8 *mogNB = call_mogrifier(ch->chan_obj, executor,
T("NOBUFFER"), mog_args, 3);
if (mogNB)
{
bMogNobuffer = xlate(mogNB);
free_lbuf(mogNB);
}
// Update message args if MOGRIFY`MESSAGE replaced the text.
//
const UTF8 *fmt_msg = (mogMsg && *mogMsg) ? mogMsg : msgNormal;
const UTF8 *fmt_args[3] = { ch->name, fmt_msg, sdrBuf };
// MOGRIFY`FORMAT: channel-wide format applied to the
// (possibly mogrified) message.
//
mogFmt = call_mogrifier(ch->chan_obj, executor,
T("FORMAT"), fmt_args, 3);
}
// Determine effective message after mogrification.
//
const UTF8 *effMsgNormal = (mogMsg && *mogMsg) ? mogMsg : msgNormal;
const UTF8 *effMsgNoComtitle = (mogMsg && *mogMsg) ? mogMsg : msgNoComtitle;
for (auto &kv : ch->users)
{
comuser &user = kv.second;
if (bJoinLeaveMsg && user.bGagJoinLeave)
{
continue;
}
if (user.bConnected
&& user.bUserIsOn
&& test_receive_access(user.who, ch))
{
// If MOGRIFY`FORMAT produced output, use it (channel-wide
// formatting takes priority over per-player CHATFORMAT,
// unless the player has their own CHATFORMAT and OVERRIDE
// is not set).
//
if (mogFmt && *mogFmt && bMogOverride)
{
notify_comsys(user.who, executor, mogFmt);
continue;
}
const UTF8 *pMsg;
if (user.ComTitleStatus
|| bSpoof
|| effMsgNoComtitle == nullptr)
{
pMsg = effMsgNormal;
}
else
{
pMsg = effMsgNoComtitle;
}
// Check if the receiver has a CHATFORMAT attribute
// (unless MOGRIFY`OVERRIDE suppresses it).
//
if ( 0 < s_chatformat_atr
&& !bMogOverride)
{
dbref aowner;
int aflags;
LBuf chatfmt = LBuf_Adopt(atr_pget(user.who, s_chatformat_atr,
&aowner, &aflags));
if ('\0' != chatfmt[0])
{
LBuf fmtbuf = LBuf_Src("chatformat");
UTF8 *bp = fmtbuf;
UTF8 sdrBuf[32];
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"), executor);
const UTF8 *cfa[3] = { ch->name, pMsg, sdrBuf };
mux_exec(chatfmt, LBUF_SIZE-1, fmtbuf, &bp,
user.who, user.who, executor,
AttrTrace(aflags, EV_FCHECK|EV_EVAL|EV_TOP),
cfa, 3);
*bp = '\0';
notify_comsys(user.who, executor, fmtbuf);
continue;
}
}
// Use MOGRIFY`FORMAT if available, otherwise the raw message.
//
if (mogFmt && *mogFmt)
{
notify_comsys(user.who, executor, mogFmt);
}
else
{
notify_comsys(user.who, executor, pMsg);
}
}
}
// Handle logging (skip if MOGRIFY`NOBUFFER).
//
const dbref obj = ch->chan_obj;
if ( Good_obj(obj)
&& !bMogNobuffer)
{
dbref aowner;
int aflags;
int logmax = DFLT_MAX_LOG;
ATTR* pattr = atr_str(T("MAX_LOG"));
if (pattr
&& pattr->number)
{
LBuf maxbuf = LBuf_Adopt(atr_get("SendChannelMessage.1141", obj, pattr->number, &aowner, &aflags));
logmax = mux_atoi64(maxbuf);
}
if (0 < logmax)
{
if (logmax > MAX_RECALL_REQUEST)
{
logmax = MAX_RECALL_REQUEST;
atr_add(ch->chan_obj, pattr->number, mux_ltoa_t(logmax), GOD,
AF_CONST | AF_NOPROG | AF_NOPARSE);
}
const UTF8* p = tprintf(T("HISTORY_%d"), iMod(ch->num_messages, logmax));
const int atr = mkattr(GOD, p);
if (0 < atr)
{
// Read the VALUE, not merely presence (#1585).
//
// The engine writes "1" to enable and atr_clr()s to disable, so
// for engine-written data presence and a non-empty value say the
// same thing. The comsys *module* cannot clear: the only lever
// mux_IAttributeAccess gives it is SetAttribute, so it disables
// by writing "" (comsys_mod.cpp:2742). A presence test reads
// that as ON, so a channel whose timestamps were turned off
// under the module came back on under the engine.
//
// Non-empty is also what the module uses to read it
// (comsys_mod.cpp:1126), so both implementations now agree:
// "1" is on, empty or absent is off.
//
pattr = atr_str(T("LOG_TIMESTAMPS"));
bool bTimestamps = false;
if (pattr)
{
LBuf tsbuf = LBuf_Adopt(atr_get("SendChannelMessage.LOG_TIMESTAMPS",
obj, pattr->number, &aowner, &aflags));
bTimestamps = ('\0' != *tsbuf.get());
}
if (bTimestamps)
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
// Save message in history with timestamp.
//
LBuf temp = LBuf_Src("chan_history");
mux_sprintf(temp, LBUF_SIZE, T("[%s] %s"), ltaNow.ReturnDateString(0), msgNormal);
atr_add(ch->chan_obj, atr, temp, GOD, AF_CONST | AF_NOPROG | AF_NOPARSE);
}
else
{
// Save message in history without timestamp.
//
atr_add(ch->chan_obj, atr, msgNormal, GOD,
AF_CONST | AF_NOPROG | AF_NOPARSE);
}
}
}
}
else if (ch->chan_obj != NOTHING)
{
ch->chan_obj = NOTHING;
}
// Free mogrifier results.
//
if (mogMsg)
{
free_lbuf(mogMsg);
}
if (mogFmt)
{
free_lbuf(mogFmt);
}
// Since msgNormal and msgNoComTitle are no longer needed, free them here.
//
if (msgNormal)
{
free_lbuf(msgNormal);
}
if (msgNoComtitle
&& msgNoComtitle != msgNormal)
{
free_lbuf(msgNoComtitle);
}
}
static void ChannelMOTD(dbref executor, dbref enactor, int attr)
{
if (Good_obj(executor))
{
dbref aowner;
int aflags;
LBuf q = LBuf_Adopt(atr_get("ChannelMOTD.1186", executor, attr, &aowner, &aflags));
if ('\0' != q[0])
{
LBuf buf = LBuf_Src("chanmotd");
UTF8* bp = buf;
mux_exec(q, LBUF_SIZE - 1, buf, &bp, executor, executor, enactor,
AttrTrace(aflags, EV_FCHECK|EV_EVAL|EV_TOP), nullptr, 0);
*bp = '\0';
notify_comsys(enactor, executor, buf);
}
}
}
// Add player to the given channel. Transmit join messages as appropriate.
//
void do_joinchannel(const dbref player, struct channel* ch)
{
int attr;
struct comuser* user = select_user(ch, player);
if (!user)
{
if (static_cast<int>(ch->users.size()) >= MAX_USERS_PER_CHANNEL)
{
raw_notify(player, tprintf(M_("Too many people on channel %s already."),
ch->name));
return;
}
comuser cu;
cu.who = player;
cu.bUserIsOn = true;
cu.ComTitleStatus = true;
// if (Connected(player))&&(isPlayer(player))
//
if (UNDEAD(player))
{
cu.bConnected = true;
}
auto result = ch->users.emplace(player, std::move(cu));
user = &result.first->second;
sqlite_wt_channel_user(ch->name, user);
attr = A_COMJOIN;
}
else if (!user->bUserIsOn)
{
user->bUserIsOn = true;
sqlite_wt_channel_user(ch->name, user);
attr = A_COMON;
}
else
{
raw_notify(player, tprintf(M_("You are already on channel %s."), ch->name));
return;
}
if (!Hidden(player))
{
UTF8 *messNormal, *messNoComtitle;
BuildChannelAnnounce((ch->type & CHANNEL_SPOOF) != 0, ch->header, user,
ch->chan_obj, M_("%s has joined this channel."),
&messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle, true);
}
ChannelMOTD(ch->chan_obj, user->who, attr);
}
// Process leave channnel request.
//
void do_leavechannel(dbref player, struct channel* ch)
{
struct comuser* user = select_user(ch, player);
raw_notify(player, tprintf(M_("You have left channel %s."), ch->name));
if (user->bUserIsOn)
{
if (!Hidden(player))
{
UTF8 *messNormal, *messNoComtitle;
BuildChannelAnnounce((ch->type & CHANNEL_SPOOF) != 0, ch->header, user,
ch->chan_obj, M_("%s has left this channel."),
&messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle, true);
}
ChannelMOTD(ch->chan_obj, user->who, A_COMOFF);
user->bUserIsOn = false;
sqlite_wt_channel_user(ch->name, user);
}
}
static void do_comwho_line
(
const dbref player,
struct channel* ch,
const struct comuser* user
)
{
UTF8* msg;
UTF8* buff = nullptr;
if (!user->title.empty())
{
// There is a comtitle.
//
if (Staff(player))
{
buff = unparse_object(player, user->who, false);
if (ch->type & CHANNEL_SPOOF)
{
msg = tprintf(T("%s as %s"), buff, reinterpret_cast<const UTF8 *>(user->title.c_str()));
}
else
{
msg = tprintf(T("%s as %s %s"), buff, reinterpret_cast<const UTF8 *>(user->title.c_str()), buff);
}
}
else
{
if (ch->type & CHANNEL_SPOOF)
{
msg = const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(user->title.c_str()));
}
else
{
buff = unparse_object(player, user->who, false);
msg = tprintf(T("%s %s"), reinterpret_cast<const UTF8 *>(user->title.c_str()), buff);
}
}
}
else
{
buff = unparse_object(player, user->who, false);
msg = buff;
}
raw_notify(player, msg);
if (buff)
{
free_lbuf(buff);
}
}
void do_comwho(dbref player, struct channel* ch)
{
raw_notify(player, M_("-- Players --"));
for (auto &kv : ch->users)
{
comuser &user = kv.second;
if (isPlayer(user.who))
{
if (Connected(user.who)
&& (!Hidden(user.who)
|| Wizard_Who(player)
|| See_Hidden(player)))
{
if (user.bUserIsOn)
{
do_comwho_line(player, ch, &user);
}
}
else if (!Hidden(user.who))
{
do_comdisconnectchannel(user.who, ch->name);
}
}
}
raw_notify(player, M_("-- Objects --"));
for (auto &kv : ch->users)
{
comuser &user = kv.second;
if (!isPlayer(user.who))
{
if (Going(user.who)
&& God(Owner(user.who)))
{
do_comdisconnectchannel(user.who, ch->name);
}
else if (user.bUserIsOn)
{
do_comwho_line(player, ch, &user);
}
}
}
raw_notify(player, tprintf(M_("-- %s --"), ch->name));
}
void do_comlast(dbref player, struct channel* ch, int arg)
{
// Validate the channel object.
//
if (!Good_obj(ch->chan_obj))
{
raw_notify(player, M_("Channel does not have an object."));
return;
}
dbref aowner;
int aflags;
const dbref obj = ch->chan_obj;
int logmax = MAX_RECALL_REQUEST;
// Lookup depth of logging.
//
ATTR* pattr = atr_str(T("MAX_LOG"));
if (pattr
&& (atr_get_info(obj, pattr->number, &aowner, &aflags)))
{
LBuf maxbuf = LBuf_Adopt(atr_get("do_comlast.1408", obj, pattr->number, &aowner, &aflags));
logmax = mux_atoi64(maxbuf);
}
if (logmax < 1)
{
raw_notify(player, M_("Channel does not log."));
return;
}
if (arg < MIN_RECALL_REQUEST)
{
arg = MIN_RECALL_REQUEST;
}
if (arg > logmax)
{
arg = logmax;
}
int histnum = ch->num_messages - arg;
raw_notify(player, tprintf(M_("%s -- Begin Comsys Recall --"), ch->header));
for (int count = 0; count < arg; count++)
{
histnum++;
pattr = atr_str(tprintf(T("HISTORY_%d"), iMod(histnum, logmax)));
if (pattr)
{
LBuf message = LBuf_Adopt(atr_get("do_comlast.1436", obj, pattr->number,
&aowner, &aflags));
raw_notify(player, message);
}
}
raw_notify(player, tprintf(M_("%s -- End Comsys Recall --"), ch->header));
}
// Turn channel history timestamping on or off for the given channel.
//
// Why these report a reason rather than a bool (#1555):
//
// Both helpers can fail for several unrelated causes, and the caller used
// to render one message for all of them -- the one blaming the argument.
// So `@cset/log <chan>=20` on a channel with no channel object answered
// "Maximum history must be a number less than or equal to 200" for a value
// that plainly satisfies that condition, and the actual prerequisite went
// unmentioned. The downstream symptom does not point back either: without
// the buffer, cbuffer() is 0 and crecall() returns empty on a channel with
// obvious traffic, so it reads as a broken crecall.
//
enum ChanLogResult
{
CHANLOG_OK = 0,
CHANLOG_BAD_VALUE, // empty, non-integer, or out of range
CHANLOG_NO_OBJECT, // channel has no channel object (@cset/object)
CHANLOG_NOT_LOGGING, // history buffer never established (@cset/log)
CHANLOG_INTERNAL // mkattr failed
};
static ChanLogResult do_chanlog_timestamps(dbref player, UTF8* channel, UTF8* arg)
{
UNUSED_PARAMETER(player);
// Validate arg.
//
int value = 0;
if (nullptr == arg
|| !is_integer(arg, nullptr)
|| ((value = mux_atoi64(arg)) != 0
&& value != 1))
{
// arg is not "0" and not "1".
//
return CHANLOG_BAD_VALUE;
}
struct channel* ch = select_channel(channel);
if (!Good_obj(ch->chan_obj))
{
// No channel object has been set.
//
return CHANLOG_NO_OBJECT;
}
dbref aowner;
int aflags;
ATTR* pattr = atr_str(T("MAX_LOG"));
if (nullptr == pattr
|| !atr_get_info(ch->chan_obj, pattr->number, &aowner, &aflags))
{
// Logging isn't enabled.
//
return CHANLOG_NOT_LOGGING;
}
const int atr = mkattr(GOD, T("LOG_TIMESTAMPS"));
if (atr <= 0)
{
return CHANLOG_INTERNAL;
}
if (value)
{
atr_add(ch->chan_obj, atr, mux_ltoa_t(value), GOD,
AF_CONST | AF_NOPROG | AF_NOPARSE);
}
else
{
atr_clr(ch->chan_obj, atr);
}
return CHANLOG_OK;
}
// Set number of entries for channel logging.
//
static ChanLogResult do_chanlog(dbref player, UTF8* channel, UTF8* arg)
{
UNUSED_PARAMETER(player);
// int64_t, not int (#1402): MAX_LOG is an attribute integer.
//
int64_t value;
if (!*arg
|| !is_integer(arg, nullptr)
|| (value = mux_atoi64(arg)) > MAX_RECALL_REQUEST)
{
return CHANLOG_BAD_VALUE;
}
if (value < 0)
{
value = 0;
}
const struct channel* ch = select_channel(channel);
if (!Good_obj(ch->chan_obj))
{
// No channel object has been set. The history buffer lives on the
// channel object, so there is nowhere to record it -- which is a
// missing prerequisite, not a bad argument (#1555).
//
return CHANLOG_NO_OBJECT;
}
const int atr = mkattr(GOD, T("MAX_LOG"));
if (atr <= 0)
{
return CHANLOG_INTERNAL;
}
dbref aowner;
int aflags;
LBuf oldvalue = LBuf_Adopt(atr_get("do_chanlog.1477", ch->chan_obj, atr, &aowner, &aflags));
const int64_t oldnum = mux_atoi64(oldvalue);
if (value < oldnum)
{
for (int64_t count = 0; count <= oldnum; count++)
{
ATTR* hist = atr_str(tprintf(T("HISTORY_%lld"),
static_cast<long long>(count)));
if (hist)
{
atr_clr(ch->chan_obj, hist->number);
}
}
}
atr_add(ch->chan_obj, atr, mux_i64toa_t(value), GOD,
AF_CONST | AF_NOPROG | AF_NOPARSE);
return CHANLOG_OK;
}
// #1191: When the comsys module owns live channel state, softcode still
// reads engine maps (select_channel, fun_*). Re-load those maps from
// SQLite when the module reports a newer revision so softcode and
// commands stay coherent.
//
static void ensure_comsys_softcode_sync(void)
{
if (nullptr == mudstate.pIComsysControl)
{
return;
}
// Re-entrancy guard: sqlite_load_comsys() itself calls select_channel()
// for every channel_users row, and select_channel() calls us. Without
// this the nested call sees the same stale s_seen_rev (it is only
// assigned after the load returns) and starts the load again.
//
if (mudstate.bSQLiteLoading)
{
return;
}
static int s_seen_rev = -1;
int rev = 0;
MUX_RESULT mr = mudstate.pIComsysControl->GetRevision(&rev);
if (MUX_FAILED(mr))
{
return;
}
if (rev == s_seen_rev)
{
return;
}
// Reload engine softcode maps from SQLite (module already write-through).
//
(void)sqlite_load_comsys();
s_seen_rev = rev;
}
// Find struct channel entry by name with the channel_name hash table.
//
struct channel* select_channel(const UTF8* channel_name)
{
ensure_comsys_softcode_sync();
// Try exact match first.
//
const auto channel_name_length = strlen(reinterpret_cast<const char*>(channel_name));
const vector<UTF8> channel_vector(channel_name, channel_name + channel_name_length);
const auto it = mudstate.channel_names.find(channel_vector);
if (it != mudstate.channel_names.end())
{
return it->second;
}
// Fall back to case-insensitive search.
//
for (const auto& entry : mudstate.channel_names)
{
if (0 == mux_stricmp(channel_name, entry.second->name))
{
return entry.second;
}
}
return nullptr;
}
// Locate player in the user's list for the given channel.
//
struct comuser* select_user(struct channel* ch, const dbref player)
{
if (!ch)
{
return nullptr;
}
auto it = ch->users.find(player);
if (it != ch->users.end())
{
return &it->second;
}
return nullptr;
}
#define MAX_ALIASES_PER_PLAYER 100
void do_addcom
(
const dbref executor,
const dbref caller,
dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8* arg1,
UTF8* channel,
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 (nullptr != mudstate.pIComsysControl)
{
bool bValidAlias;
size_t nValidAlias;
UTF8* pValid = MakeCanonicalComAlias(arg1, &nValidAlias, &bValidAlias);
if (bValidAlias)
{
mudstate.pIComsysControl->AddAlias(executor, pValid, channel);
}
else
{
raw_notify(executor, M_("You need to specify a valid alias."));
}
return;
}
bool bValidAlias;
size_t nValidAlias;
UTF8* pValidAlias = MakeCanonicalComAlias(arg1, &nValidAlias, &bValidAlias);
if (!bValidAlias)
{
raw_notify(executor, M_("You need to specify a valid alias."));
return;
}
if ('\0' == channel[0])
{
raw_notify(executor, M_("You need to specify a channel."));
return;
}
struct channel* ch = select_channel(channel);
if (!ch)
{
UTF8 Buffer[MAX_CHANNEL_LEN + 1];
StripTabsAndTruncate(channel, Buffer, MAX_CHANNEL_LEN, MAX_CHANNEL_LEN);
raw_notify(executor, tprintf(M_("Channel %s does not exist yet."), Buffer));
return;
}
if (!test_join_access(executor, ch))
{
raw_notify(executor, M_("Sorry, this channel type does not allow you to join."));
return;
}
comsys_t* c = get_comsys(executor);
if (static_cast<int>(c->aliases.size()) >= MAX_ALIASES_PER_PLAYER)
{
raw_notify(executor, M_("Sorry, but you have reached the maximum number of aliases allowed."));
return;
}
// Check if alias already exists.
//
string sAlias(reinterpret_cast<const char *>(pValidAlias));
for (const auto &ca : c->aliases)
{
if (ca.alias == sAlias)
{
const UTF8* p = tprintf(M_("That alias is already in use for channel %s."),
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
raw_notify(executor, p);
return;
}
}
com_alias newAlias;
newAlias.alias = sAlias;
newAlias.channel.assign(reinterpret_cast<const char *>(channel));
c->aliases.push_back(std::move(newAlias));
sort_com_aliases(*c);
sqlite_wt_player_channel(executor, pValidAlias, channel);
if (!select_user(ch, executor))
{
do_joinchannel(executor, ch);
}
raw_notify(executor, tprintf(M_("Channel %s added with alias %s."), channel, pValidAlias));
}
void do_delcom(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8* arg1, const UTF8* cargs[],
int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->DelAlias(executor, arg1);
return;
}
if (!arg1)
{
raw_notify(executor, M_("Need an alias to delete."));
return;
}
comsys_t* c = get_comsys(executor);
string sArg(reinterpret_cast<const char *>(arg1));
for (auto it = c->aliases.begin(); it != c->aliases.end(); ++it)
{
if (it->alias == sArg)
{
// Count how many aliases map to the same channel.
//
int found = 0;
for (const auto &ca : c->aliases)
{
if (ca.channel == it->channel)
{
found++;
}
}
// If we found no other channels, delete it.
//
if (found <= 1)
{
do_delcomchannel(executor,
const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(it->channel.c_str())),
false);
raw_notify(executor, tprintf(M_("Alias %s for channel %s deleted."),
arg1,
reinterpret_cast<const UTF8 *>(it->channel.c_str())));
}
else
{
raw_notify(executor, tprintf(M_("Alias %s for channel %s deleted."),
arg1,
reinterpret_cast<const UTF8 *>(it->channel.c_str())));
}
sqlite_wt_delete_player_channel(executor, arg1);
c->aliases.erase(it);
return;
}
}
raw_notify(executor, M_("Unable to find that alias."));
}
// Process a complete unsubscribe for a player from a particular channel.
//
void do_delcomchannel(dbref player, UTF8* channel, bool bQuiet)
{
struct channel* ch = select_channel(channel);
if (!ch)
{
raw_notify(player, tprintf(M_("Unknown channel %s."), channel));
}
else
{
auto it = ch->users.find(player);
if (it != ch->users.end())
{
comuser &user = it->second;
do_comdisconnectchannel(player, channel);
if (!bQuiet)
{
if (user.bUserIsOn
&& !Hidden(player))
{
UTF8 *messNormal, *messNoComtitle;
BuildChannelAnnounce((ch->type & CHANNEL_SPOOF) != 0,
ch->header, &user, ch->chan_obj,
M_("%s has left this channel."),
&messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle, true);
}
raw_notify(player, tprintf(M_("You have left channel %s."),
channel));
}
ChannelMOTD(ch->chan_obj, user.who, A_COMLEAVE);
sqlite_wt_delete_channel_user(ch->name, player);
ch->users.erase(it);
}
}
}
void do_createchannel(const dbref executor, const dbref caller, dbref enactor, const int eval, const int key, UTF8* channel,
const UTF8* cargs[], const int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->CreateChannel(executor, channel);
return;
}
if ('\0' == channel[0])
{
raw_notify(executor, M_("You must specify a channel to create."));
return;
}
if (!Comm_All(executor))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
auto newchannel = new ::channel();
size_t nNameNoANSI;
UTF8* pNameNoANSI;
UTF8 Buffer[MAX_HEADER_LEN + 1];
mux_field fldChannel = StripTabsAndTruncate(channel, Buffer, MAX_HEADER_LEN,
MAX_HEADER_LEN);
if (fldChannel.m_byte == fldChannel.m_column)
{
// The channel name does not contain ANSI, so first, we add some to
// get the header.
//
const size_t nMax = MAX_HEADER_LEN - (sizeof(COLOR_INTENSE) - 1)
- (sizeof(COLOR_RESET) - 1) - 2;
size_t nChannel = fldChannel.m_byte;
if (nMax < nChannel)
{
nChannel = nMax;
}
Buffer[nChannel] = '\0';
mux_sprintf(newchannel->header, sizeof(newchannel->header),
T("%s[%s]%s"), COLOR_INTENSE, Buffer, COLOR_RESET);
// Then, we use the non-ANSI part for the name.
//
nNameNoANSI = nChannel;
pNameNoANSI = Buffer;
}
else
{
// The given channel name does contain color.
//
memcpy(newchannel->header, Buffer, fldChannel.m_byte + 1);
pNameNoANSI = strip_color(Buffer, &nNameNoANSI);
}
if (nNameNoANSI > MAX_CHANNEL_LEN)
{
raw_notify(executor, M_("Channel name is too long."));
delete newchannel;
return;
}
memcpy(newchannel->name, pNameNoANSI, nNameNoANSI);
newchannel->name[nNameNoANSI] = '\0';
if (select_channel(newchannel->name))
{
raw_notify(executor, tprintf(M_("Channel %s already exists."), newchannel->name));
delete newchannel;
return;
}
newchannel->type = 127;
newchannel->temp1 = 0;
newchannel->temp2 = 0;
newchannel->charge = 0;
newchannel->charge_who = executor;
newchannel->amount_col = 0;
newchannel->chan_obj = NOTHING;
newchannel->num_messages = 0;
const vector<UTF8> channel_name(newchannel->name, newchannel->name + nNameNoANSI);
mudstate.channel_names.insert(make_pair(channel_name, newchannel));
sqlite_wt_channel(newchannel);
// Report the channel creation using non-ANSI name.
//
raw_notify(executor, tprintf(M_("Channel %s created."), newchannel->name));
}
void do_destroychannel
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
const int key,
UTF8* channel_name,
const UTF8* cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->DestroyChannel(executor, channel_name);
return;
}
const auto channel_name_length = strlen(reinterpret_cast<const char*>(channel_name));
const vector<UTF8> channel_name_vector(channel_name, channel_name + channel_name_length);
const auto it = mudstate.channel_names.find(channel_name_vector);
// #1189: miss check was inverted (!= meant "found" reported not found;
// missing names fell through with it == end() and dereferenced).
//
if (it == mudstate.channel_names.end())
{
raw_notify(executor, tprintf(M_("Could not find channel %s."), channel_name));
return;
}
auto ch = it->second;
if ( !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
// #1199: sweep player aliases before the channel row (and CASCADE) go away.
//
purge_aliases_for_channel(ch->name);
sqlite_wt_delete_channel(ch->name);
delete ch;
ch = nullptr;
mudstate.channel_names.erase(it);
raw_notify(executor, tprintf(M_("Channel %s destroyed."), channel_name));
}
static void do_listchannels(dbref player, UTF8* pattern)
{
const bool perm = Comm_All(player);
if (!perm)
{
raw_notify(player, M_("Warning: Only public channels and your channels will be shown."));
}
bool bWild;
if (nullptr != pattern
&& '\0' != *pattern)
{
bWild = true;
}
else
{
bWild = false;
}
// @clist/full column schema (#1667 Phase 4 A1) — shared with module
// ChanListFull: "*** "/PLS (4) + name 13 + header 15 + owner 15 +
// Access field 6 (JXR + pad) + freeform users/msgs. Header labels are
// separate msgids; both paths use mux_table_* so they cannot drift.
//
static const size_t kFullNameCols = 13;
static const size_t kFullHeaderCols = 15;
static const size_t kFullOwnerCols = 15;
static const size_t kFullAccessCols = 6;
{
UTF8 header[LBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_bytes(header, sizeof(header), pos, "*** ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Channel"), kFullNameCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Header"), kFullHeaderCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Owner"), kFullOwnerCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Access"), kFullAccessCols);
// Freeform count labels (historical "Access Users Msgs" spacing).
//
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Users")));
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Msgs")));
raw_notify(player, header);
}
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
const auto ch = it->second;
if ( perm
|| (ch->type & CHANNEL_PUBLIC)
|| Controls(player, ch->charge_who)
|| nullptr != select_user(ch, player))
{
if ( !bWild
|| quick_wild(pattern, ch->name))
{
bool bCanJoin = test_join_access(player, ch);
bool bCanXmit = test_transmit_access(player, ch);
bool bCanRecv = test_receive_access(player, ch);
const UTF8 *pHeader = ch->header;
if ('\0' == pHeader[0])
{
pHeader = T("-");
}
UTF8 line[LBUF_SIZE];
size_t pos = 0;
line[pos++] = (ch->type & CHANNEL_PUBLIC) ? 'P' : '-';
line[pos++] = (ch->type & CHANNEL_LOUD) ? 'L' : '-';
line[pos++] = (ch->type & CHANNEL_SPOOF) ? 'S' : '-';
line[pos++] = ' ';
line[pos] = '\0';
pos = mux_table_append_ljust(line, sizeof(line), pos,
ch->name, kFullNameCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
pos = mux_table_append_ljust(line, sizeof(line), pos,
pHeader, kFullHeaderCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
pos = mux_table_append_ljust(line, sizeof(line), pos,
Moniker(ch->charge_who), kFullOwnerCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
UTF8 access[4];
access[0] = bCanJoin ? 'J' : '-';
access[1] = bCanXmit ? 'X' : '-';
access[2] = bCanRecv ? 'R' : '-';
access[3] = '\0';
// Access is 6 display cols (JXR + pad); counts follow with no
// extra separator — same as historical PadField-to-56 stop.
//
pos = mux_table_append_ljust(line, sizeof(line), pos,
access, kFullAccessCols);
if (pos < sizeof(line))
{
mux_sprintf(line + pos, sizeof(line) - pos,
T("%5d %4d"),
static_cast<int>(ch->users.size()),
ch->num_messages);
}
raw_notify(player, line);
}
}
}
raw_notify(player, M_("-- End of list of Channels --"));
}
void do_comtitle
(
const dbref executor,
const dbref caller,
const dbref enactor,
int eval,
const int key,
const int nargs,
UTF8* arg1,
UTF8* arg2,
const UTF8* cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->ComTitle(executor, arg1, arg2, key);
return;
}
if (!*arg1)
{
raw_notify(executor, M_("Need an alias to do comtitle."));
return;
}
UTF8 channel[MAX_CHANNEL_LEN + 1];
mux_strncpy(channel, get_channel_from_alias(executor, arg1), MAX_CHANNEL_LEN);
if (channel[0] == '\0')
{
raw_notify(executor, M_("Unknown alias."));
return;
}
struct channel* ch = select_channel(channel);
if (ch)
{
if (select_user(ch, executor))
{
if (key == COMTITLE_OFF)
{
if ((ch->type & CHANNEL_SPOOF) == 0)
{
raw_notify(executor, tprintf(M_("Comtitles are now off for channel %s"), channel));
do_setcomtitlestatus(executor, ch, false);
}
else
{
raw_notify(executor, M_("You can not turn off comtitles on that channel."));
}
}
else if (key == COMTITLE_ON)
{
raw_notify(executor, tprintf(M_("Comtitles are now on for channel %s"), channel));
do_setcomtitlestatus(executor, ch, true);
}
else if (key == COMTITLE_GAG)
{
raw_notify(executor, tprintf(M_("Join/leave messages are now gagged for channel %s"), channel));
do_setgagjoinleavestatus(executor, ch, true);
}
else if (key == COMTITLE_UNGAG)
{
raw_notify(executor, tprintf(M_("Join/leave messages are now ungagged for channel %s"), channel));
do_setgagjoinleavestatus(executor, ch, false);
}
else
{
UTF8* pValidatedTitleValue = RestrictTitleValue(arg2);
do_setnewtitle(executor, ch, pValidatedTitleValue);
raw_notify(executor, tprintf(M_("Title set to %s on channel %s."),
pValidatedTitleValue, channel));
}
}
}
else
{
raw_notify(executor, M_("Illegal comsys alias, please delete."));
}
}
void do_comlist
(
const dbref executor,
dbref caller,
const dbref enactor,
int eval,
const int key,
UTF8* pattern,
const UTF8* cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->ComList(executor, pattern);
return;
}
bool bWild;
if (nullptr != pattern
&& '\0' != *pattern)
{
bWild = true;
}
else
{
bWild = false;
}
// comlist schema (#1667 Phase 4 A3) — shared with module ComList:
// alias 15 + channel 18 + freeform status/title. Header labels are
// separate msgids via mux_table_*.
//
static const size_t kComlistAliasCols = 15;
static const size_t kComlistChannelCols = 18;
{
UTF8 header[LBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Alias"), kComlistAliasCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Channel"), kComlistChannelCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Status")));
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_bytes(header, sizeof(header), pos,
reinterpret_cast<const char *>(M_("Title")));
raw_notify(executor, header);
}
const comsys_t* c = get_comsys(executor);
for (size_t i = 0; i < c->aliases.size(); i++)
{
const UTF8 *chanName = reinterpret_cast<const UTF8 *>(c->aliases[i].channel.c_str());
struct comuser* user = select_user(select_channel(chanName), executor);
if (user)
{
if (!bWild
|| quick_wild(pattern, chanName))
{
UTF8 line[LBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(line, sizeof(line), pos,
reinterpret_cast<const UTF8 *>(c->aliases[i].alias.c_str()),
kComlistAliasCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
pos = mux_table_append_ljust(line, sizeof(line), pos,
chanName, kComlistChannelCols);
pos = mux_table_append_bytes(line, sizeof(line), pos, " ");
if (pos < sizeof(line))
{
mux_sprintf(line + pos, sizeof(line) - pos,
T("%s %s%s %s"),
(user->bUserIsOn ? T("on ") : T("off")),
(user->ComTitleStatus ? T("con ") : T("coff")),
(user->bGagJoinLeave ? T(" gag") : T("")),
reinterpret_cast<const UTF8 *>(user->title.c_str()));
}
raw_notify(executor, line);
}
}
else
{
raw_notify(executor, tprintf(
M_("Bad Comsys Alias: %s for Channel: %s"),
reinterpret_cast<const UTF8 *>(c->aliases[i].alias.c_str()),
chanName));
}
}
raw_notify(executor, M_("-- End of comlist --"));
}
// Cleanup channels owned by the player.
//
void do_channelnuke(const dbref player)
{
if (nullptr != mudstate.pIComsysControl)
{
return; // Module handles via IServerEventsSink::data_free.
}
bool found = true;
while (found)
{
found = false;
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
auto ch = it->second;
if (player == ch->charge_who)
{
// #1199: same alias sweep as do_destroychannel.
//
purge_aliases_for_channel(ch->name);
sqlite_wt_delete_channel(ch->name);
delete ch;
ch = nullptr;
// Removing an element invalidates the iterator, so the search much be restarted.
//
mudstate.channel_names.erase(it);
found = true;
break;
}
}
}
}
void do_clearcom(const dbref executor, const dbref caller, const dbref enactor, int eval, const int key)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->ClearAliases(executor);
return;
}
const comsys_t* c = get_comsys(executor);
for (int i = static_cast<int>(c->aliases.size()) - 1; i > -1; --i)
{
do_delcom(executor, caller, enactor, 0, 0,
const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(c->aliases[i].alias.c_str())),
nullptr, 0);
}
}
void do_allcom(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8* arg1, const UTF8* cargs[],
int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->AllCom(executor, arg1);
return;
}
if (strcmp(reinterpret_cast<char*>(arg1), "who") != 0
&& strcmp(reinterpret_cast<char*>(arg1), "on") != 0
&& strcmp(reinterpret_cast<char*>(arg1), "off") != 0)
{
raw_notify(executor, M_("Only options available are: on, off and who."));
return;
}
const comsys_t* c = get_comsys(executor);
for (size_t i = 0; i < c->aliases.size(); i++)
{
do_processcom(executor,
const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(c->aliases[i].channel.c_str())),
arg1);
if (strcmp(reinterpret_cast<char*>(arg1), "who") == 0)
{
raw_notify(executor, T(""));
}
}
}
void do_channelwho(const dbref executor, const dbref caller, dbref enactor, const int eval, const int key, UTF8* arg1, const UTF8* cargs[],
const int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->ChanWho(executor, arg1);
return;
}
UTF8 channel[MAX_CHANNEL_LEN + 1];
size_t i = 0;
while ('\0' != arg1[i]
&& '/' != arg1[i]
&& i < MAX_CHANNEL_LEN)
{
channel[i] = arg1[i];
i++;
}
channel[i] = '\0';
bool bAll = false;
if ('/' == arg1[i]
&& 'a' == arg1[i + 1])
{
bAll = true;
}
struct channel* ch = nullptr;
if (i <= MAX_CHANNEL_LEN)
{
ch = select_channel(channel);
}
if (nullptr == ch)
{
raw_notify(executor, tprintf(M_("Unknown channel %s."), channel));
return;
}
if (!(Comm_All(executor)
|| Controls(executor, ch->charge_who)))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
// @cwho schema (#1667 Phase 4 A4) — shared with module ChanWho:
// name 29 / status 6 / player 6 via mux_table_* (display columns, not
// printf codepoint width). Labels are separate msgids.
//
static const size_t kCwhoNameCols = 29;
static const size_t kCwhoStatusCols = 6;
static const size_t kCwhoPlayerCols = 6;
raw_notify(executor, tprintf(M_("-- %s --"), ch->name));
{
UTF8 header[SBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Name"), kCwhoNameCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Status"), kCwhoStatusCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Player"), kCwhoPlayerCols);
raw_notify(executor, header);
}
for (auto &kv : ch->users)
{
const comuser &user = kv.second;
if ((bAll
|| UNDEAD(user.who))
&& (!Hidden(user.who)
|| Wizard_Who(executor)
|| See_Hidden(executor)))
{
UTF8 temp[SBUF_SIZE];
UTF8* buff = unparse_object(executor, user.who, false);
size_t pos = 0;
pos = mux_table_append_ljust(temp, sizeof(temp), pos,
strip_color(buff), kCwhoNameCols);
pos = mux_table_append_bytes(temp, sizeof(temp), pos, " ");
pos = mux_table_append_ljust(temp, sizeof(temp), pos,
user.bUserIsOn ? T("on ") : T("off"), kCwhoStatusCols);
pos = mux_table_append_bytes(temp, sizeof(temp), pos, " ");
pos = mux_table_append_ljust(temp, sizeof(temp), pos,
isPlayer(user.who) ? T("yes") : T("no "), kCwhoPlayerCols);
raw_notify(executor, temp);
free_lbuf(buff);
}
}
raw_notify(executor, tprintf(M_("-- %s --"), ch->name));
}
// Assemble and transmit player disconnection messages to the player's active
// set of channels.
//
static void do_comdisconnectraw_notify(const dbref player, UTF8* chan)
{
struct channel* ch = select_channel(chan);
if (!ch) return;
struct comuser* cu = select_user(ch, player);
if (!cu) return;
if ((ch->type & CHANNEL_LOUD)
&& cu->bUserIsOn
&& !Hidden(player))
{
UTF8 *messNormal, *messNoComtitle;
BuildChannelAnnounce((ch->type & CHANNEL_SPOOF) != 0, ch->header, cu,
ch->chan_obj, M_("%s has disconnected."),
&messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle, true);
}
}
// Assemble and transmit player connection messages to the player's active
// set of channels.
//
static void do_comconnectraw_notify(const dbref player, UTF8* chan)
{
struct channel* ch = select_channel(chan);
if (!ch) return;
struct comuser* cu = select_user(ch, player);
if (!cu) return;
if ((ch->type & CHANNEL_LOUD)
&& cu->bUserIsOn
&& !Hidden(player))
{
UTF8 *messNormal, *messNoComtitle;
BuildChannelAnnounce((ch->type & CHANNEL_SPOOF) != 0, ch->header, cu,
ch->chan_obj, M_("%s has connected."),
&messNormal, &messNoComtitle);
SendChannelMessage(player, ch, messNormal, messNoComtitle, true);
}
}
// Set player as connected on the channel.
//
static void do_comconnectchannel(dbref player, UTF8* channel, const string &alias)
{
struct channel* ch = select_channel(channel);
if (ch)
{
struct comuser* user = select_user(ch, player);
if (user)
{
user->bConnected = true;
}
else
{
raw_notify(player,
tprintf(M_("Bad Comsys Alias: %s for Channel: %s"),
reinterpret_cast<const UTF8 *>(alias.c_str()), channel));
}
}
else
{
raw_notify(player, tprintf(M_("Bad Comsys Alias: %s for Channel: %s"),
reinterpret_cast<const UTF8 *>(alias.c_str()), channel));
}
}
// Check player for any active channels. If found, mark the player as
// disconnected. Transmit disconnection messages as needed.
//
void do_comdisconnect(const dbref player)
{
if (nullptr != mudstate.pIComsysControl)
{
return; // Module handles via IServerEventsSink::disconnect.
}
auto it = comsys_table.find(player);
if (it == comsys_table.end())
{
return;
}
const comsys_t &c = it->second;
vector<string> seen;
for (const auto &ca : c.aliases)
{
bool bFound = false;
for (const auto &s : seen)
{
if (s == ca.channel)
{
bFound = true;
break;
}
}
if (!bFound)
{
seen.push_back(ca.channel);
UTF8 *chanName = const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
// Process channel removals.
//
do_comdisconnectchannel(player, chanName);
// Send disconnection messages if necessary.
//
do_comdisconnectraw_notify(player, chanName);
}
}
}
// Locate all active channels for the given player; mark the player as
// connected and send connect notifications as appropriate.
//
void do_comconnect(const dbref player)
{
if (nullptr != mudstate.pIComsysControl)
{
return; // Module handles via IServerEventsSink::connect.
}
auto it = comsys_table.find(player);
if (it == comsys_table.end())
{
return;
}
const comsys_t &c = it->second;
vector<string> seen;
for (const auto &ca : c.aliases)
{
bool bFound = false;
for (const auto &s : seen)
{
if (s == ca.channel)
{
bFound = true;
break;
}
}
if (!bFound)
{
seen.push_back(ca.channel);
UTF8 *chanName = const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
do_comconnectchannel(player, chanName, ca.alias);
do_comconnectraw_notify(player, chanName);
}
}
}
// Mark the given player as disconnected on the channel.
//
void do_comdisconnectchannel(const dbref player, UTF8* channel)
{
struct channel* ch = select_channel(channel);
if (!ch)
{
return;
}
struct comuser* user = select_user(ch, player);
if (user)
{
user->bConnected = false;
}
}
void do_editchannel
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int flag,
int nargs,
UTF8* arg1,
UTF8* arg2,
const UTF8* cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->EditChannel(executor, arg1, arg2, flag);
return;
}
struct channel* ch = select_channel(arg1);
if (!ch)
{
raw_notify(executor, tprintf(M_("Unknown channel %s."), arg1));
return;
}
if (!(Comm_All(executor)
|| Controls(executor, ch->charge_who)))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
bool add_remove = true;
UTF8* s = arg2;
if (*s == '!')
{
add_remove = false;
s++;
}
switch (flag)
{
case EDIT_CHANNEL_CCHOWN:
{
init_match(executor, arg2, NOTYPE);
match_everything(0);
const dbref who = match_result();
if (Good_obj(who))
{
ch->charge_who = who;
raw_notify(executor, M_("Set."));
}
else
{
raw_notify(executor, M_("Invalid player."));
}
}
break;
case EDIT_CHANNEL_CCHARGE:
{
const int64_t c_charge = mux_atoi64(arg2);
if (0 <= c_charge
&& c_charge <= MAX_COST)
{
ch->charge = static_cast<int>(c_charge);
raw_notify(executor, M_("Set."));
}
else
{
raw_notify(executor, M_("That is not a reasonable cost."));
}
}
break;
case EDIT_CHANNEL_CPFLAGS:
{
int access = 0;
if (strcmp(reinterpret_cast<char*>(s), "join") == 0)
{
access = CHANNEL_PLAYER_JOIN;
}
else if (strcmp(reinterpret_cast<char*>(s), "receive") == 0)
{
access = CHANNEL_PLAYER_RECEIVE;
}
else if (strcmp(reinterpret_cast<char*>(s), "transmit") == 0)
{
access = CHANNEL_PLAYER_TRANSMIT;
}
else
{
raw_notify(executor, M_("@cpflags: Unknown Flag."));
}
if (access)
{
if (add_remove)
{
ch->type |= access;
raw_notify(executor, M_("@cpflags: Set."));
}
else
{
ch->type &= ~access;
raw_notify(executor, M_("@cpflags: Cleared."));
}
}
}
break;
case EDIT_CHANNEL_COFLAGS:
{
int access = 0;
if (strcmp(reinterpret_cast<char*>(s), "join") == 0)
{
access = CHANNEL_OBJECT_JOIN;
}
else if (strcmp(reinterpret_cast<char*>(s), "receive") == 0)
{
access = CHANNEL_OBJECT_RECEIVE;
}
else if (strcmp(reinterpret_cast<char*>(s), "transmit") == 0)
{
access = CHANNEL_OBJECT_TRANSMIT;
}
else
{
raw_notify(executor, M_("@coflags: Unknown Flag."));
}
if (access)
{
if (add_remove)
{
ch->type |= access;
raw_notify(executor, M_("@coflags: Set."));
}
else
{
ch->type &= ~access;
raw_notify(executor, M_("@coflags: Cleared."));
}
}
}
break;
}
sqlite_wt_channel(ch);
}
bool test_join_access(const dbref player, struct channel* chan)
{
if (Comm_All(player))
{
return true;
}
int access;
if (isPlayer(player))
{
access = CHANNEL_PLAYER_JOIN;
}
else
{
access = CHANNEL_OBJECT_JOIN;
}
return ((chan->type & access) != 0
|| could_doit(player, chan->chan_obj, A_LOCK));
}
bool test_transmit_access(const dbref player, struct channel* chan)
{
if (Comm_All(player))
{
return true;
}
int access;
if (isPlayer(player))
{
access = CHANNEL_PLAYER_TRANSMIT;
}
else
{
access = CHANNEL_OBJECT_TRANSMIT;
}
return ((chan->type & access) != 0
|| could_doit(player, chan->chan_obj, A_LUSE));
}
bool test_receive_access(const dbref player, struct channel* chan)
{
if (Comm_All(player))
{
return true;
}
int access;
if (isPlayer(player))
{
access = CHANNEL_PLAYER_RECEIVE;
}
else
{
access = CHANNEL_OBJECT_RECEIVE;
}
return ((chan->type & access) != 0
|| could_doit(player, chan->chan_obj, A_LENTER));
}
// true means continue, and false means stop.
//
bool do_comsystem(const dbref who, UTF8* cmd)
{
auto t = reinterpret_cast<UTF8*>(strchr(reinterpret_cast<char*>(cmd), ' '));
if (!t
|| t - cmd > MAX_ALIAS_LEN
|| t[1] == '\0')
{
// Doesn't fit the pattern of "alias message".
//
return true;
}
UTF8 alias[MAX_ALIAS_LEN + 1];
memcpy(alias, cmd, t - cmd);
alias[t - cmd] = '\0';
const UTF8* ch = get_channel_from_alias(who, alias);
if (ch[0] == '\0')
{
// Not really an alias after all.
//
return true;
}
t++;
do_processcom(who, const_cast<UTF8 *>(ch), t);
return false;
}
void do_cemit
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8* chan,
UTF8* text,
const UTF8* cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->CEmit(executor, chan, text, key);
return;
}
struct channel* ch = select_channel(chan);
if (!ch)
{
raw_notify(executor, tprintf(M_("Channel %s does not exist."), chan));
return;
}
if (!Controls(executor, ch->charge_who)
&& !Comm_All(executor))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
UTF8* text2 = alloc_lbuf("do_cemit");
if (key == CEMIT_NOHEADER)
{
mux_strncpy(text2, text, LBUF_SIZE - 1);
}
else
{
mux_strncpy(text2, tprintf(T("%s %s"), ch->header, text), LBUF_SIZE - 1);
}
SendChannelMessage(executor, ch, text2, text2);
}
void do_chopen
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
const int key,
const int nargs,
UTF8* chan,
UTF8* value,
const UTF8* cargs[],
int ncargs
)
{
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->CSet(executor, chan, value, key);
return;
}
if (key == CSET_LIST)
{
do_chanlist(executor, caller, enactor, 0, 1, nullptr, nullptr, 0);
return;
}
const UTF8* msg = nullptr;
struct channel* ch = select_channel(chan);
if (!ch)
{
msg = tprintf(M_("@cset: Channel %s does not exist."), chan);
raw_notify(executor, msg);
return;
}
if (!Controls(executor, ch->charge_who)
&& !Comm_All(executor))
{
raw_notify(executor, NOPERM_MESSAGE);
return;
}
dbref thing;
switch (key)
{
case CSET_PUBLIC:
ch->type |= CHANNEL_PUBLIC;
msg = tprintf(M_("@cset: Channel %s placed on the public listings."), chan);
break;
case CSET_PRIVATE:
ch->type &= ~CHANNEL_PUBLIC;
msg = tprintf(M_("@cset: Channel %s taken off the public listings."), chan);
break;
case CSET_LOUD:
ch->type |= CHANNEL_LOUD;
msg = tprintf(M_("@cset: Channel %s now sends connect/disconnect msgs."), chan);
break;
case CSET_QUIET:
ch->type &= ~CHANNEL_LOUD;
msg = tprintf(M_("@cset: Channel %s connect/disconnect msgs muted."), chan);
break;
case CSET_SPOOF:
ch->type |= CHANNEL_SPOOF;
msg = tprintf(M_("@cset: Channel %s set spoofable."), chan);
break;
case CSET_NOSPOOF:
ch->type &= ~CHANNEL_SPOOF;
msg = tprintf(M_("@cset: Channel %s set unspoofable."), chan);
break;
case CSET_OBJECT:
init_match(executor, value, NOTYPE);
match_everything(0);
thing = match_result();
if (thing == NOTHING)
{
ch->chan_obj = thing;
msg = tprintf(M_("Channel %s is now disassociated from any channel object."), ch->name);
}
else if (Good_obj(thing))
{
ch->chan_obj = thing;
UTF8* buff = unparse_object(executor, thing, false);
msg = tprintf(M_("Channel %s is now using %s as channel object."), ch->name, buff);
free_lbuf(buff);
}
else
{
msg = tprintf(T("%d is not a valid channel object."), thing);
}
break;
case CSET_HEADER:
do_cheader(executor, chan, value);
msg = M_("Set.");
break;
case CSET_LOG:
switch (do_chanlog(executor, chan, value))
{
case CHANLOG_OK:
msg = tprintf(M_("@cset: Channel %s maximum history set."), chan);
break;
case CHANLOG_NO_OBJECT:
msg = tprintf(M_("@cset: Channel %s has no channel object. Use @cset/object to set one before setting a history buffer."), chan);
break;
case CHANLOG_INTERNAL:
msg = tprintf(M_("@cset: Could not set maximum history for channel %s."), chan);
break;
default:
msg = tprintf(M_("@cset: Maximum history must be a number less than or equal to %d."), MAX_RECALL_REQUEST);
break;
}
break;
case CSET_LOG_TIME:
switch (do_chanlog_timestamps(executor, chan, value))
{
case CHANLOG_OK:
msg = tprintf(M_("@cset: Channel %s timestamp logging set."), chan);
break;
case CHANLOG_BAD_VALUE:
msg = tprintf(M_("@cset: Timestamp logging for channel %s must be 0 or 1."), chan);
break;
case CHANLOG_NO_OBJECT:
msg = tprintf(M_("@cset: Channel %s has no channel object. Use @cset/object to set one before enabling timestamp logging."), chan);
break;
case CHANLOG_NOT_LOGGING:
msg = tprintf(M_("@cset: Channel %s has no history buffer. Use @cset/log to set one before enabling timestamp logging."), chan);
break;
default:
msg = tprintf(M_("@cset: Could not set timestamp logging for channel %s."), chan);
break;
}
}
sqlite_wt_channel(ch);
raw_notify(executor, msg);
}
void do_chboot
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
int key,
const int nargs,
UTF8* channel,
UTF8* victim,
const UTF8* cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->CBoot(executor, channel, victim, key);
return;
}
// I sure hope it's not going to be that long.
//
struct channel* ch = select_channel(channel);
if (!ch)
{
raw_notify(executor, M_("@cboot: Unknown channel."));
return;
}
struct comuser* user = select_user(ch, executor);
if (!user)
{
raw_notify(executor, M_("@cboot: You are not on that channel."));
return;
}
if (!Controls(executor, ch->charge_who)
&& !Comm_All(executor))
{
raw_notify(executor, M_("@cboot: You cant do that!"));
return;
}
const dbref thing = match_thing(executor, victim);
if (!Good_obj(thing))
{
return;
}
struct comuser* vu = select_user(ch, thing);
if (!vu)
{
raw_notify(executor, tprintf(M_("@cboot: %s is not on the channel."),
Moniker(thing)));
return;
}
raw_notify(executor, tprintf(M_("You boot %s off channel %s."),
Moniker(thing), ch->name));
raw_notify(thing, tprintf(M_("%s boots you off channel %s."),
Moniker(executor), ch->name));
if (!(key & CBOOT_QUIET))
{
UTF8 *mess1, *mess1nct;
UTF8 *mess2, *mess2nct;
BuildChannelMessage((ch->type & CHANNEL_SPOOF) != 0, ch->header, user,
ch->chan_obj, T(":boots"), &mess1, &mess1nct);
BuildChannelMessage((ch->type & CHANNEL_SPOOF) != 0, nullptr, vu,
ch->chan_obj, T(":off the channel."), &mess2,
&mess2nct);
UTF8* messNormal = alloc_lbuf("do_chboot.messnormal");
UTF8* messNoComtitle = alloc_lbuf("do_chboot.messnocomtitle");
UTF8* mnp = messNormal;
UTF8* mnctp = messNoComtitle;
if (mess1)
{
safe_str(mess1, messNormal, &mnp);
free_lbuf(mess1);
}
if (mess2)
{
safe_str(mess2, messNormal, &mnp);
free_lbuf(mess2);
}
*mnp = '\0';
if (mess1nct)
{
safe_str(mess1nct, messNoComtitle, &mnctp);
free_lbuf(mess1nct);
}
if (mess2nct)
{
safe_str(mess2nct, messNoComtitle, &mnctp);
free_lbuf(mess2nct);
}
*mnctp = '\0';
SendChannelMessage(executor, ch, messNormal, messNoComtitle);
do_delcomchannel(thing, channel, false);
}
else
{
do_delcomchannel(thing, channel, true);
}
}
// Process a channel header set request.
//
void do_cheader(const dbref player, UTF8* channel, const UTF8* header)
{
struct channel* ch = select_channel(channel);
if (!ch)
{
raw_notify(player, M_("That channel does not exist."));
return;
}
if (!Controls(player, ch->charge_who)
&& !Comm_All(player))
{
raw_notify(player, NOPERM_MESSAGE);
return;
}
if ('\0' == header[0])
{
// Empty value resets to the default bold [ChannelName] header.
//
mux_sprintf(ch->header, sizeof(ch->header),
T("%s[%s]%s"), COLOR_INTENSE, ch->name, COLOR_RESET);
}
else
{
// Optimize/terminate any ANSI in the string.
//
UTF8 NewHeader_ANSI[MAX_HEADER_LEN + 1];
const mux_field nLen = StripTabsAndTruncate(header, NewHeader_ANSI,
MAX_HEADER_LEN, MAX_HEADER_LEN);
memcpy(ch->header, NewHeader_ANSI, nLen.m_byte + 1);
}
sqlite_wt_channel(ch);
}
struct chanlist_node
{
UTF8* name;
struct channel* ptr;
};
static int DCL_CDECL chanlist_comp(const void* a, const void* b)
{
const auto ca = reinterpret_cast<const chanlist_node *>(a);
const auto cb = reinterpret_cast<const chanlist_node *>(b);
return mux_stricmp(ca->name, cb->name);
}
void do_chanlist
(
const dbref executor,
const dbref caller,
const dbref enactor,
const int eval,
int key,
UTF8* pattern,
const UTF8* cargs[],
const int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nullptr != mudstate.pIComsysControl)
{
mudstate.pIComsysControl->ChanList(executor, pattern, key);
return;
}
if (key & CLIST_FULL)
{
do_listchannels(executor, pattern);
return;
}
// @clist / @clist/headers column schema (#1667 Phase 4 A2).
// Prefix 4 cols ("*** " or "PLS "), then name 13 / owner 15 / third 45
// with a single space between fields, pad line to 79. Header and rows
// share mux_table_* so labels are separate msgids and cannot drift from
// the data stops. Matches the module ChanList construction.
//
static const size_t kClistNameCols = 13;
static const size_t kClistOwnerCols = 15;
static const size_t kClistThirdCols = 45;
static const size_t kClistLineCols = 79;
{
UTF8 header[MBUF_SIZE];
size_t pos = 0;
pos = mux_table_append_bytes(header, sizeof(header), pos, "*** ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Channel"), kClistNameCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
M_("Owner"), kClistOwnerCols);
pos = mux_table_append_bytes(header, sizeof(header), pos, " ");
pos = mux_table_append_ljust(header, sizeof(header), pos,
(key & CLIST_HEADERS) ? M_("Header") : M_("Description"),
kClistThirdCols);
while (pos < kClistLineCols && pos + 1 < sizeof(header))
{
header[pos++] = ' ';
}
if (pos < sizeof(header))
{
header[pos] = '\0';
}
raw_notify(executor, header);
}
dbref owner;
int flags = 0;
bool bWild;
if (nullptr != pattern
&& '\0' != *pattern)
{
bWild = true;
}
else
{
bWild = false;
}
#define MAX_SUPPORTED_NUM_ENTRIES 10000
auto entries = mudstate.channel_names.size();
if (MAX_SUPPORTED_NUM_ENTRIES < entries)
{
// Nobody should have so many channels.
//
entries = MAX_SUPPORTED_NUM_ENTRIES;
}
if (0 < entries)
{
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
const auto ch = it->second;
if (!bWild != quick_wild(pattern, ch->name))
{
if (Comm_All(executor)
|| (ch->type & CHANNEL_PUBLIC)
|| Controls(executor, ch->charge_who)
|| nullptr != select_user(ch, executor))
{
const UTF8* pBuffer = nullptr;
UTF8* atrstr = nullptr;
if (key & CLIST_HEADERS)
{
pBuffer = ch->header;
}
else
{
if (NOTHING != ch->chan_obj)
{
atrstr = atr_pget(ch->chan_obj, A_DESC, &owner, &flags);
}
if (nullptr != atrstr && '\0' != atrstr[0])
{
pBuffer = atrstr;
}
else
{
pBuffer = T("No description.");
}
}
UTF8* temp = alloc_mbuf("do_chanlist_temp");
size_t pos = 0;
temp[pos++] = (ch->type & CHANNEL_PUBLIC) ? 'P' : '-';
temp[pos++] = (ch->type & CHANNEL_LOUD) ? 'L' : '-';
temp[pos++] = (ch->type & CHANNEL_SPOOF) ? 'S' : '-';
temp[pos++] = ' ';
temp[pos] = '\0';
pos = mux_table_append_ljust(temp, MBUF_SIZE, pos,
ch->name, kClistNameCols);
pos = mux_table_append_bytes(temp, MBUF_SIZE, pos, " ");
pos = mux_table_append_ljust(temp, MBUF_SIZE, pos,
Moniker(ch->charge_who), kClistOwnerCols);
pos = mux_table_append_bytes(temp, MBUF_SIZE, pos, " ");
pos = mux_table_append_ljust(temp, MBUF_SIZE, pos,
pBuffer, kClistThirdCols);
while (pos < kClistLineCols && pos + 1 < MBUF_SIZE)
{
temp[pos++] = ' ';
}
if (pos < MBUF_SIZE)
{
temp[pos] = '\0';
}
raw_notify(executor, temp);
free_mbuf(temp);
if (nullptr != atrstr)
{
free_lbuf(atrstr);
}
}
}
}
}
raw_notify(executor, M_("-- End of list of Channels --"));
}
// Returns a player's comtitle for a named channel.
//
FUNCTION(fun_comtitle)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref victim = lookup_player(executor, fargs[0], true);
if (!Good_obj(victim))
{
init_match(executor, fargs[0], TYPE_THING);
match_everything(0);
victim = match_result();
if (!Good_obj(victim))
{
safe_str(S_("#-1 OBJECT DOES NOT EXIST"), buff, bufc);
return;
}
}
struct channel* chn = select_channel(fargs[1]);
if (!chn)
{
safe_str(S_("#-1 CHANNEL DOES NOT EXIST"), buff, bufc);
return;
}
comsys_t* c = get_comsys(executor);
struct comuser* user;
bool onchannel = false;
if (Wizard(executor))
{
onchannel = true;
}
else
{
user = select_user(chn, executor);
if (user)
{
onchannel = true;
}
}
if (!onchannel)
{
safe_noperm(buff, bufc);
return;
}
user = select_user(chn, victim);
if (user)
{
safe_str(reinterpret_cast<const UTF8 *>(user->title.c_str()), buff, bufc);
return;
}
safe_str(S_("#-1 OBJECT NOT ON THAT CHANNEL"), buff, bufc);
}
// Returns a player's comsys alias for a named channel.
//
FUNCTION(fun_comalias)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref victim = lookup_player(executor, fargs[0], true);
if (!Good_obj(victim))
{
init_match(executor, fargs[0], TYPE_THING);
match_everything(0);
victim = match_result();
if (!Good_obj(victim))
{
safe_str(S_("#-1 OBJECT DOES NOT EXIST"), buff, bufc);
return;
}
}
struct channel* chn = select_channel(fargs[1]);
if (!chn)
{
safe_str(S_("#-1 CHANNEL DOES NOT EXIST"), buff, bufc);
return;
}
// Wizards can get the comalias for anyone. Players and objects can check
// for themselves. Objects that Inherit can check for their owners.
//
if (!Wizard(executor)
&& executor != victim
&& (Owner(executor) != victim
|| !Inherits(executor)))
{
safe_noperm(buff, bufc);
return;
}
auto it = comsys_table.find(victim);
if (it != comsys_table.end())
{
const comsys_t &cc = it->second;
for (const auto &ca : cc.aliases)
{
if (ca.channel == reinterpret_cast<const char *>(fargs[1]))
{
safe_str(reinterpret_cast<const UTF8 *>(ca.alias.c_str()), buff, bufc);
return;
}
}
}
safe_str(S_("#-1 OBJECT NOT ON THAT CHANNEL"), buff, bufc);
}
// Returns a list of channels.
//
FUNCTION(fun_channels)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
ensure_comsys_softcode_sync();
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
dbref who = NOTHING;
if (nfargs >= 1 && fargs[0][0] != '\0')
{
who = lookup_player(executor, fargs[0], true);
if (who == NOTHING
&& mux_stricmp(fargs[0], T("all")) != 0)
{
safe_str(S_("#-1 PLAYER NOT FOUND"), buff, bufc);
return;
}
}
int pg_offset = 0;
int pg_limit = 0;
if (nfargs >= 3) pg_offset = mux_atoi64(fargs[2]);
if (nfargs >= 4) pg_limit = mux_atoi64(fargs[3]);
if (pg_offset < 0) pg_offset = 0;
if (pg_limit < 0) pg_limit = 0;
int pos = 0;
int count = 0;
bool bFirst = true;
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
const auto ch = it->second;
if ( (Comm_All(executor)
|| (ch->type & CHANNEL_PUBLIC)
|| Controls(executor, ch->charge_who)
|| nullptr != select_user(ch, executor))
&& (who == NOTHING
|| Controls(who, ch->charge_who)))
{
if (pos < pg_offset)
{
pos++;
continue;
}
if (pg_limit > 0 && count >= pg_limit)
break;
if (!bFirst)
{
print_sep(sep, buff, bufc);
}
safe_str(ch->name, buff, bufc);
bFirst = false;
count++;
pos++;
}
}
}
FUNCTION(fun_chanobj)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel* ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
const dbref obj = ch->chan_obj;
if (Good_obj(obj))
{
safe_str(tprintf(T("#%d"), obj), buff, bufc);
}
else
{
safe_str(S_("#-1"), buff, bufc);
}
}
// ---------------------------------------------------------------------------
// Channel query functions — PennMUSH-compatible.
// ---------------------------------------------------------------------------
FUNCTION(fun_cowner)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Visibility check.
//
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
safe_tprintf_str(buff, bufc, T("#%d"), ch->charge_who);
}
// cmogrifier(<channel>) — Returns the dbref of the channel object (which
// is also the mogrifier object in TinyMUX). Returns #-1 if no channel
// object is set. PennMUSH compatibility.
//
FUNCTION(fun_cmogrifier)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if (Good_obj(ch->chan_obj))
{
safe_tprintf_str(buff, bufc, T("#%d"), ch->chan_obj);
}
else
{
safe_str(S_("#-1"), buff, bufc);
}
}
FUNCTION(fun_cusers)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
safe_str(mux_ltoa_t(static_cast<int>(ch->users.size())), buff, bufc);
}
FUNCTION(fun_cmsgs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
safe_str(mux_ltoa_t(ch->num_messages), buff, bufc);
}
FUNCTION(fun_cbuffer)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Buffer size is the MAX_LOG attribute on the channel object.
//
int logmax = 0;
if (Good_obj(ch->chan_obj))
{
ATTR *pattr = atr_str(T("MAX_LOG"));
if (pattr && pattr->number)
{
dbref aowner;
int aflags;
LBuf maxbuf = LBuf_Adopt(atr_get("fun_cbuffer", ch->chan_obj,
pattr->number, &aowner, &aflags));
logmax = mux_atoi64(maxbuf);
}
}
safe_str(mux_ltoa_t(logmax), buff, bufc);
}
FUNCTION(fun_cdesc)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Description is the DESC attribute on the channel object.
//
if (Good_obj(ch->chan_obj))
{
dbref aowner;
int aflags;
LBuf desc = LBuf_Adopt(atr_pget(ch->chan_obj, A_DESC, &aowner, &aflags));
if ('\0' != desc[0])
{
safe_str(desc, buff, bufc);
}
}
}
FUNCTION(fun_cflags)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if (nfargs >= 2)
{
// cflags(channel, player) — per-user flags.
//
dbref target = match_thing(executor, fargs[1]);
if (!Good_obj(target))
{
safe_str(S_("#-1 NO MATCH"), buff, bufc);
return;
}
struct comuser *user = select_user(ch, target);
if (nullptr == user)
{
safe_str(S_("#-1 NOT ON CHANNEL"), buff, bufc);
return;
}
// Return user flags: on/off, gag, comtitle status.
//
if (!user->bUserIsOn)
{
safe_chr('O', buff, bufc);
}
if (user->bGagJoinLeave)
{
safe_chr('G', buff, bufc);
}
if (!user->ComTitleStatus)
{
safe_chr('Q', buff, bufc);
}
}
else
{
// cflags(channel) — channel flags.
//
if (ch->type & CHANNEL_PUBLIC) safe_chr('P', buff, bufc);
if (ch->type & CHANNEL_LOUD) safe_chr('L', buff, bufc);
if (ch->type & CHANNEL_SPOOF) safe_chr('S', buff, bufc);
}
}
FUNCTION(fun_cstatus)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
dbref target = match_thing(executor, fargs[1]);
if (!Good_obj(target))
{
safe_str(S_("#-1 NO MATCH"), buff, bufc);
return;
}
struct comuser *user = select_user(ch, target);
if (nullptr == user)
{
safe_str(T("Off"), buff, bufc);
return;
}
if (!user->bUserIsOn)
{
safe_str(T("Off"), buff, bufc);
}
else if (user->bGagJoinLeave)
{
safe_str(T("Gag"), buff, bufc);
}
else
{
safe_str(T("On"), buff, bufc);
}
}
FUNCTION(fun_crecall)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Must be on the channel.
//
struct comuser *user = select_user(ch, executor);
if (nullptr == user || !user->bUserIsOn)
{
safe_str(S_("#-1 NOT ON CHANNEL"), buff, bufc);
return;
}
// Get buffer depth from channel object.
//
if (!Good_obj(ch->chan_obj))
{
return;
}
ATTR *pattr = atr_str(T("MAX_LOG"));
int logmax = 0;
if (pattr && pattr->number)
{
dbref aowner;
int aflags;
LBuf maxbuf = LBuf_Adopt(atr_get("fun_crecall", ch->chan_obj,
pattr->number, &aowner, &aflags));
logmax = mux_atoi64(maxbuf);
}
if (logmax < 1)
{
return;
}
// Number of lines to recall (default 1, max logmax).
//
int nLines = 1;
if (nfargs >= 2 && fargs[1][0] != '\0')
{
nLines = mux_atoi64(fargs[1]);
if (nLines < 1) nLines = 1;
if (nLines > logmax) nLines = logmax;
}
// Output separator (default newline).
//
SEP sep;
sep.n = 1;
sep.str[0] = '\r';
sep.str[1] = '\0';
if (nfargs >= 3)
{
memcpy(sep.str, fargs[2], strlen(reinterpret_cast<const char *>(fargs[2])) + 1);
sep.n = strlen(reinterpret_cast<const char *>(fargs[2]));
}
int histnum = ch->num_messages - nLines;
bool bFirst = true;
for (int count = 0; count < nLines; count++)
{
histnum++;
const UTF8 *attrname = tprintf(T("HISTORY_%d"),
((histnum % logmax) + logmax) % logmax);
int atr = mkattr(GOD, attrname);
if (0 < atr)
{
dbref aowner;
int aflags;
LBuf msg = LBuf_Adopt(atr_get("fun_crecall", ch->chan_obj,
atr, &aowner, &aflags));
if ('\0' != msg[0])
{
if (!bFirst)
{
safe_copy_buf(sep.str, sep.n, buff, bufc);
}
safe_str(msg, buff, bufc);
bFirst = false;
}
}
}
}
// ---------------------------------------------------------------------------
// chaninfo(channel, field) — generic channel metadata accessor.
// ---------------------------------------------------------------------------
//
// Visibility: PUBLIC, or subscriber, or Comm_All, or Controls(charge_who).
// The "object" field additionally requires Wizard.
//
FUNCTION(fun_chaninfo)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who)
&& nullptr == select_user(ch, executor))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
const UTF8 *field = fargs[1];
if (0 == mux_stricmp(field, T("name")))
{
safe_str(ch->name, buff, bufc);
}
else if (0 == mux_stricmp(field, T("header")))
{
safe_str(ch->header, buff, bufc);
}
else if (0 == mux_stricmp(field, T("owner")))
{
safe_tprintf_str(buff, bufc, T("#%d"), ch->charge_who);
}
else if (0 == mux_stricmp(field, T("object")))
{
if (!Wizard(executor))
{
safe_noperm(buff, bufc);
return;
}
if (Good_obj(ch->chan_obj))
{
safe_tprintf_str(buff, bufc, T("#%d"), ch->chan_obj);
}
else
{
safe_str(S_("#-1"), buff, bufc);
}
}
else if (0 == mux_stricmp(field, T("type")))
{
safe_str(mux_ltoa_t(ch->type), buff, bufc);
}
else if (0 == mux_stricmp(field, T("flags")))
{
if (ch->type & CHANNEL_PUBLIC) safe_chr('P', buff, bufc);
if (ch->type & CHANNEL_LOUD) safe_chr('L', buff, bufc);
if (ch->type & CHANNEL_SPOOF) safe_chr('S', buff, bufc);
}
else if (0 == mux_stricmp(field, T("charge")))
{
safe_str(mux_ltoa_t(ch->charge), buff, bufc);
}
else if (0 == mux_stricmp(field, T("users")))
{
safe_str(mux_ltoa_t(static_cast<int>(ch->users.size())), buff, bufc);
}
else if (0 == mux_stricmp(field, T("msgs")))
{
safe_str(mux_ltoa_t(ch->num_messages), buff, bufc);
}
else if (0 == mux_stricmp(field, T("desc")))
{
if (Good_obj(ch->chan_obj))
{
dbref aowner;
int aflags;
LBuf desc = LBuf_Adopt(atr_pget(ch->chan_obj, A_DESC, &aowner, &aflags));
if ('\0' != desc[0])
{
safe_str(desc, buff, bufc);
}
}
}
else if (0 == mux_stricmp(field, T("buffer")))
{
int logmax = 0;
if (Good_obj(ch->chan_obj))
{
ATTR *pattr = atr_str(T("MAX_LOG"));
if (pattr && pattr->number)
{
dbref aowner;
int aflags;
LBuf maxbuf = LBuf_Adopt(atr_get("fun_chaninfo", ch->chan_obj,
pattr->number, &aowner, &aflags));
logmax = mux_atoi64(maxbuf);
}
}
safe_str(mux_ltoa_t(logmax), buff, bufc);
}
else if (0 == mux_stricmp(field, T("canjoin")))
{
safe_chr(test_join_access(executor, ch) ? '1' : '0', buff, bufc);
}
else if (0 == mux_stricmp(field, T("cantransmit")))
{
safe_chr(test_transmit_access(executor, ch) ? '1' : '0', buff, bufc);
}
else if (0 == mux_stricmp(field, T("canreceive")))
{
safe_chr(test_receive_access(executor, ch) ? '1' : '0', buff, bufc);
}
else
{
safe_str(S_("#-1 INVALID FIELD"), buff, bufc);
}
}
// ---------------------------------------------------------------------------
// chanfind(name-or-header) — resolve a channel name from name or header.
// ---------------------------------------------------------------------------
//
// Matches the canonical channel name or its display header (case-insensitive).
// Name matching covers the common "channel exists under this name" case
// (#1204); header matching keeps the reverse-lookup documented for
// @cset/header values players see in chat (e.g. "<PublicServices>" → PubServ).
//
// Visibility: same subscriber-aware model as chaninfo.
//
FUNCTION(fun_chanfind)
{
ensure_comsys_softcode_sync();
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *target = fargs[0];
for (auto it = mudstate.channel_names.begin();
it != mudstate.channel_names.end(); ++it)
{
const auto ch = it->second;
// #1204: name is the baseline; header remains supported for reverse
// lookup of custom display headers.
//
if ( 0 == mux_stricmp(target, ch->name)
|| 0 == mux_stricmp(target, ch->header))
{
// Visibility check.
//
if ( (ch->type & CHANNEL_PUBLIC)
|| Comm_All(executor)
|| Controls(executor, ch->charge_who)
|| nullptr != select_user(ch, executor))
{
safe_str(ch->name, buff, bufc);
return;
}
}
}
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
}
// ---------------------------------------------------------------------------
// chanusers(channel[, separator[, field]]) — list subscriber data.
// ---------------------------------------------------------------------------
//
// With no field arg (or empty), returns delimited dbrefs.
// With a field arg, returns that field for each subscriber:
// title, status, flags, gagjoin, comtitles, alias, name.
//
// "alias" requires Wizard (exposes other players' aliases).
// Other fields follow the co-member rule (executor must be visible).
//
// Visibility: PUBLIC, or subscriber, or Comm_All, or Controls(charge_who).
//
FUNCTION(fun_chanusers)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
SEP sep;
if (!OPTIONAL_DELIM(2, sep, DELIM_DFLT|DELIM_STRING))
{
return;
}
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who)
&& nullptr == select_user(ch, executor))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Determine output field (default: dbref).
//
enum FieldType { FT_DBREF, FT_TITLE, FT_STATUS, FT_FLAGS,
FT_GAGJOIN, FT_COMTITLES, FT_ALIAS, FT_NAME };
FieldType ft = FT_DBREF;
if (nfargs >= 3 && fargs[2][0] != '\0')
{
const UTF8 *field = fargs[2];
if (0 == mux_stricmp(field, T("title")))
{
ft = FT_TITLE;
}
else if (0 == mux_stricmp(field, T("status")))
{
ft = FT_STATUS;
}
else if (0 == mux_stricmp(field, T("flags")))
{
ft = FT_FLAGS;
}
else if (0 == mux_stricmp(field, T("gagjoin")))
{
ft = FT_GAGJOIN;
}
else if (0 == mux_stricmp(field, T("comtitles")))
{
ft = FT_COMTITLES;
}
else if (0 == mux_stricmp(field, T("alias")))
{
if (!Wizard(executor))
{
safe_noperm(buff, bufc);
return;
}
ft = FT_ALIAS;
}
else if (0 == mux_stricmp(field, T("name")))
{
ft = FT_NAME;
}
else
{
safe_str(S_("#-1 INVALID FIELD"), buff, bufc);
return;
}
}
int pg_offset = 0;
int pg_limit = 0;
if (nfargs >= 4) pg_offset = mux_atoi64(fargs[3]);
if (nfargs >= 5) pg_limit = mux_atoi64(fargs[4]);
if (pg_offset < 0) pg_offset = 0;
if (pg_limit < 0) pg_limit = 0;
int pos = 0;
int count = 0;
bool bFirst = true;
for (const auto &kv : ch->users)
{
if (pos < pg_offset)
{
pos++;
continue;
}
if (pg_limit > 0 && count >= pg_limit)
break;
if (!bFirst)
{
print_sep(sep, buff, bufc);
}
bFirst = false;
const comuser &user = kv.second;
switch (ft)
{
case FT_DBREF:
safe_tprintf_str(buff, bufc, T("#%d"), kv.first);
break;
case FT_NAME:
safe_str(Moniker(kv.first), buff, bufc);
break;
case FT_TITLE:
safe_str(reinterpret_cast<const UTF8 *>(user.title.c_str()),
buff, bufc);
break;
case FT_STATUS:
safe_str(user.bUserIsOn ? T("On") : T("Off"), buff, bufc);
break;
case FT_FLAGS:
if (!user.bUserIsOn) safe_chr('O', buff, bufc);
if (user.bGagJoinLeave) safe_chr('G', buff, bufc);
if (!user.ComTitleStatus) safe_chr('Q', buff, bufc);
break;
case FT_GAGJOIN:
safe_chr(user.bGagJoinLeave ? '1' : '0', buff, bufc);
break;
case FT_COMTITLES:
safe_chr(user.ComTitleStatus ? '1' : '0', buff, bufc);
break;
case FT_ALIAS:
{
auto it = comsys_table.find(kv.first);
if (it != comsys_table.end())
{
const comsys_t &cc = it->second;
for (const auto &ca : cc.aliases)
{
if (0 == mux_stricmp(
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
ch->name))
{
safe_str(reinterpret_cast<const UTF8 *>(
ca.alias.c_str()), buff, bufc);
break;
}
}
}
}
break;
}
count++;
pos++;
}
}
// ---------------------------------------------------------------------------
// chanuser(channel, player, field) — per-user channel data.
// ---------------------------------------------------------------------------
//
// Per-field permissions:
// alias: self, or Owner(executor)==victim && Inherits(executor), or Wizard
// title, status, flags, gagjoin, comtitles:
// self, or both executor and victim on channel, or Wizard
//
FUNCTION(fun_chanuser)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
struct channel *ch = select_channel(fargs[0]);
if (nullptr == ch)
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Channel visibility: subscriber-aware.
//
if ( !(ch->type & CHANNEL_PUBLIC)
&& !Comm_All(executor)
&& !Controls(executor, ch->charge_who)
&& nullptr == select_user(ch, executor))
{
safe_str(S_("#-1 CHANNEL NOT FOUND"), buff, bufc);
return;
}
// Resolve target player/object.
//
dbref victim = lookup_player(executor, fargs[1], true);
if (!Good_obj(victim))
{
init_match(executor, fargs[1], TYPE_THING);
match_everything(0);
victim = match_result();
if (!Good_obj(victim))
{
safe_str(S_("#-1 PLAYER NOT FOUND"), buff, bufc);
return;
}
}
const UTF8 *field = fargs[2];
// The "alias" field has its own permission rule.
//
if (0 == mux_stricmp(field, T("alias")))
{
if ( !Wizard(executor)
&& executor != victim
&& (Owner(executor) != victim
|| !Inherits(executor)))
{
safe_noperm(buff, bufc);
return;
}
auto it = comsys_table.find(victim);
if (it != comsys_table.end())
{
const comsys_t &cc = it->second;
for (const auto &ca : cc.aliases)
{
if (0 == mux_stricmp(
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
fargs[0]))
{
safe_str(reinterpret_cast<const UTF8 *>(ca.alias.c_str()),
buff, bufc);
return;
}
}
}
safe_str(S_("#-1 NOT ON CHANNEL"), buff, bufc);
return;
}
// All other fields: self, or co-member, or Wizard.
//
if ( !Wizard(executor)
&& executor != victim)
{
struct comuser *executor_user = select_user(ch, executor);
if (nullptr == executor_user)
{
safe_noperm(buff, bufc);
return;
}
}
// Look up the target's user record on this channel.
//
struct comuser *user = select_user(ch, victim);
if (nullptr == user)
{
safe_str(S_("#-1 NOT ON CHANNEL"), buff, bufc);
return;
}
if (0 == mux_stricmp(field, T("title")))
{
safe_str(reinterpret_cast<const UTF8 *>(user->title.c_str()),
buff, bufc);
}
else if (0 == mux_stricmp(field, T("status")))
{
if (user->bUserIsOn)
{
safe_str(T("On"), buff, bufc);
}
else
{
safe_str(T("Off"), buff, bufc);
}
}
else if (0 == mux_stricmp(field, T("flags")))
{
if (!user->bUserIsOn)
{
safe_chr('O', buff, bufc);
}
if (user->bGagJoinLeave)
{
safe_chr('G', buff, bufc);
}
if (!user->ComTitleStatus)
{
safe_chr('Q', buff, bufc);
}
}
else if (0 == mux_stricmp(field, T("gagjoin")))
{
safe_chr(user->bGagJoinLeave ? '1' : '0', buff, bufc);
}
else if (0 == mux_stricmp(field, T("comtitles")))
{
safe_chr(user->ComTitleStatus ? '1' : '0', buff, bufc);
}
else
{
safe_str(S_("#-1 INVALID FIELD"), buff, bufc);
}
}
// ---------------------------------------------------------------------------
// SQLite comsys bulk sync and load.
// ---------------------------------------------------------------------------
#include "sqlite_backend.h"
static void clear_runtime_comsys_data(void)
{
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
channel *ch = it->second;
delete ch;
}
mudstate.channel_names.clear();
comsys_table.clear();
}
bool sqlite_sync_comsys(void)
{
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.Begin())
{
return false;
}
if (!sqldb.ClearComsysTables())
{
sqldb.Rollback();
return false;
}
// Sync channels and their users.
//
for (auto it = mudstate.channel_names.begin(); it != mudstate.channel_names.end(); ++it)
{
struct channel *ch = it->second;
if (!sqldb.SyncChannel(ch->name, ch->header,
ch->type, ch->temp1, ch->temp2,
ch->charge, ch->charge_who, ch->amount_col,
ch->num_messages, ch->chan_obj))
{
sqldb.Rollback();
return false;
}
for (auto &kv : ch->users)
{
comuser &user = kv.second;
if (user.who >= 0 && user.who < mudstate.db_top)
{
if (!sqldb.SyncChannelUser(ch->name, user.who,
user.bUserIsOn, user.ComTitleStatus,
user.bGagJoinLeave,
reinterpret_cast<const UTF8 *>(user.title.c_str())))
{
sqldb.Rollback();
return false;
}
}
}
}
// Sync player channel aliases.
//
for (auto &kv : comsys_table)
{
const comsys_t &c = kv.second;
for (const auto &ca : c.aliases)
{
// Skip orphaned aliases for channels that no longer exist.
//
if (nullptr == select_channel(
const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(ca.channel.c_str()))))
{
continue;
}
if (!sqldb.SyncPlayerChannel(c.who,
reinterpret_cast<const UTF8 *>(ca.alias.c_str()),
reinterpret_cast<const UTF8 *>(ca.channel.c_str())))
{
sqldb.Rollback();
return false;
}
}
}
if (!sqldb.PutMeta("has_comsys", 1))
{
sqldb.Rollback();
return false;
}
if (!sqldb.Commit())
{
sqldb.Rollback();
return false;
}
return true;
}
int sqlite_load_comsys(void)
{
mudstate.bSQLiteLoading = true;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
int has_comsys = 0;
CSQLiteDB::MetaGetResult has_comsys_meta = sqldb.GetMetaEx("has_comsys", &has_comsys);
if (CSQLiteDB::MetaGetResult::Error == has_comsys_meta)
{
mudstate.bSQLiteLoading = false;
return -1;
}
if (CSQLiteDB::MetaGetResult::Found != has_comsys_meta || 0 == has_comsys)
{
mudstate.bSQLiteLoading = false;
return 0;
}
// Reset in-memory structures before repopulating.
//
clear_runtime_comsys_data();
// Load channels.
//
if (!sqldb.LoadAllChannels([](const UTF8 *name, const UTF8 *header,
int type, int temp1, int temp2, int charge, int charge_who,
int amount_col, int num_messages, int chan_obj)
{
auto ch = new channel();
mux_strncpy(ch->name, name, MAX_CHANNEL_LEN);
mux_strncpy(ch->header, header, MAX_HEADER_LEN);
ch->type = type;
ch->temp1 = temp1;
ch->temp2 = temp2;
ch->charge = charge;
ch->charge_who = charge_who;
ch->amount_col = amount_col;
ch->num_messages = num_messages;
ch->chan_obj = chan_obj;
size_t nName = strlen(reinterpret_cast<const char *>(ch->name));
vector<UTF8> channel_name_vector(ch->name, ch->name + nName);
mudstate.channel_names.insert(make_pair(channel_name_vector, ch));
}))
{
clear_runtime_comsys_data();
mudstate.bSQLiteLoading = false;
return -1;
}
// Load channel users.
//
if (!sqldb.LoadAllChannelUsers([](const UTF8 *channel_name, int who,
bool is_on, bool comtitle_status, bool gag_join_leave,
const UTF8 *title)
{
auto ch_name = const_cast<UTF8 *>(channel_name);
struct channel *ch = select_channel(ch_name);
if (!ch)
{
return;
}
if (!Good_dbref(who) || isGarbage(who))
{
return;
}
comuser cu;
cu.who = who;
cu.bUserIsOn = is_on;
cu.ComTitleStatus = comtitle_status;
cu.bGagJoinLeave = gag_join_leave;
cu.title.assign(reinterpret_cast<const char *>(title));
auto result = ch->users.emplace(who, std::move(cu));
comuser &user = result.first->second;
if (!(isPlayer(user.who))
&& !(Going(user.who)
&& (God(Owner(user.who)))))
{
do_joinchannel(user.who, ch);
}
user.bConnected = true;
}))
{
clear_runtime_comsys_data();
mudstate.bSQLiteLoading = false;
return -1;
}
// Load player channel aliases.
//
// We need to group by player. Use a map to collect aliases per player,
// then create comsys_t entries.
//
struct PlayerAliasEntry
{
string alias;
string channel_name;
};
std::map<int, std::vector<PlayerAliasEntry>> player_aliases;
if (!sqldb.LoadAllPlayerChannels([&player_aliases](int who, const UTF8 *alias,
const UTF8 *channel_name)
{
PlayerAliasEntry entry;
entry.alias.assign(reinterpret_cast<const char *>(alias));
entry.channel_name.assign(reinterpret_cast<const char *>(channel_name));
player_aliases[who].push_back(std::move(entry));
}))
{
player_aliases.clear();
clear_runtime_comsys_data();
mudstate.bSQLiteLoading = false;
return -1;
}
for (auto &pa : player_aliases)
{
int who = pa.first;
auto &entries = pa.second;
if (!Good_obj(who))
{
continue;
}
comsys_t c;
c.who = who;
for (auto &e : entries)
{
com_alias ca;
ca.alias = std::move(e.alias);
ca.channel = std::move(e.channel_name);
c.aliases.push_back(std::move(ca));
}
sort_com_aliases(c);
comsys_table[who] = std::move(c);
}
mudstate.bSQLiteLoading = false;
return 1;
}