mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
CResultsSet constructor and NextField require length headers and payloads to fit the result blob. CreateChannel rejects names longer than MAX_CHANNEL_LEN before duplicate checks. Mail aliases cap at 50 per owner and 100 members, including on load.
4554 lines
132 KiB
C++
4554 lines
132 KiB
C++
/*! \file comsys_mod.cpp
|
||
* \brief Comsys Module — Channel system as a loadable module
|
||
*
|
||
* This module implements the MUX channel system as a dynamically loaded
|
||
* module. It hooks into server events for player connect/disconnect
|
||
* and provides mux_IComsysControl for command dispatch.
|
||
*
|
||
* Core dependencies are accessed exclusively through COM interfaces:
|
||
* mux_INotify — player notification
|
||
* mux_IObjectInfo — object property queries
|
||
* mux_IAttributeAccess — attribute read/write
|
||
* mux_IEvaluator — softcode evaluation
|
||
* mux_IPermissions — permission checks
|
||
* mux_ILog — logging
|
||
*
|
||
* Channel data is loaded from and persisted to the game's SQLite database
|
||
* via the module's own sqlite3 connection.
|
||
*/
|
||
|
||
#include "copyright.h"
|
||
#include "autoconf.h"
|
||
#include "config.h"
|
||
#include "libmux.h"
|
||
#include "modules.h"
|
||
#include "timeutil.h"
|
||
// Column layout: co_copy_field (#1649 / #1653 / #1656). cheader storage
|
||
// still uses StripTabsAndTruncate via core.h so ANSI collapse matches the
|
||
// engine's do_cheader. Plain assembly uses mux_sprintf (mux_format.h).
|
||
//
|
||
#include "core.h"
|
||
#include "mux_format.h"
|
||
#include "mathutil.h"
|
||
#include "color_ops.h"
|
||
#include "mux_table.h"
|
||
#include "comsys_mod.h"
|
||
|
||
#include <atomic>
|
||
#include <cstdarg>
|
||
#include <cstring>
|
||
|
||
// Windows compatibility for POSIX functions.
|
||
//
|
||
#ifdef _MSC_VER
|
||
#define strcasecmp _stricmp
|
||
#endif
|
||
|
||
// Column layout: libmux mux_table_* on co_copy_field (#1667 Phase 3).
|
||
//
|
||
#define append_ljust_field mux_table_append_ljust
|
||
#define append_bytes mux_table_append_bytes
|
||
|
||
// Module bookkeeping.
|
||
//
|
||
static std::atomic<uint32_t> g_cComponents{0};
|
||
static std::atomic<uint32_t> g_cServerLocks{0};
|
||
|
||
// Module entry points.
|
||
//
|
||
static MUX_CLASS_INFO comsys_classes[] =
|
||
{
|
||
{ CID_ComsysMod }
|
||
};
|
||
#define NUM_CLASSES (sizeof(comsys_classes)/sizeof(comsys_classes[0]))
|
||
|
||
// Required by libmux ModuleLoad — without this, bLoaded stays false and
|
||
// CreateInstance never finds the class (#1191 root cause).
|
||
//
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_CanUnloadNow(void)
|
||
{
|
||
if ( 0 == g_cComponents
|
||
&& 0 == g_cServerLocks)
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
return MUX_S_FALSE;
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Register(void)
|
||
{
|
||
MUX_RESULT mr = MUX_E_UNEXPECTED;
|
||
|
||
if ( 0 == g_cComponents
|
||
&& 0 == g_cServerLocks)
|
||
{
|
||
mr = mux_RegisterClassObjects(NUM_CLASSES, comsys_classes, nullptr);
|
||
}
|
||
return mr;
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Unregister(void)
|
||
{
|
||
return mux_RevokeClassObjects(NUM_CLASSES, comsys_classes);
|
||
}
|
||
|
||
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_GetClassObject(MUX_CID cid, MUX_IID iid, void **ppv)
|
||
{
|
||
MUX_RESULT mr = MUX_E_CLASSNOTAVAILABLE;
|
||
|
||
if (CID_ComsysMod == cid)
|
||
{
|
||
CComsysModFactory *pFactory = nullptr;
|
||
try
|
||
{
|
||
pFactory = new CComsysModFactory;
|
||
}
|
||
catch (...)
|
||
{
|
||
; // Nothing.
|
||
}
|
||
|
||
if (nullptr == pFactory)
|
||
{
|
||
return MUX_E_OUTOFMEMORY;
|
||
}
|
||
|
||
mr = pFactory->QueryInterface(iid, ppv);
|
||
pFactory->Release();
|
||
}
|
||
return mr;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CComsysMod — main module class.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
CComsysMod::CComsysMod(void) : m_cRef(1),
|
||
m_pILog(nullptr),
|
||
m_pIServerEventsControl(nullptr),
|
||
m_pINotify(nullptr),
|
||
m_pIObjectInfo(nullptr),
|
||
m_pIAttributeAccess(nullptr),
|
||
m_pIEvaluator(nullptr),
|
||
m_pIPermissions(nullptr),
|
||
m_pIGameConfig(nullptr),
|
||
m_pIStorage(nullptr),
|
||
m_revision(0)
|
||
{
|
||
g_cComponents++;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::FinalConstruct(void)
|
||
{
|
||
MUX_RESULT mr;
|
||
|
||
// Acquire logging interface.
|
||
//
|
||
mr = mux_CreateInstance(CID_Log, nullptr, UseSameProcess,
|
||
IID_ILog,
|
||
reinterpret_cast<void **>(&m_pILog));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
// Register for server events.
|
||
//
|
||
mux_IServerEventsSink *pIServerEventsSink = nullptr;
|
||
mr = QueryInterface(IID_IServerEventsSink,
|
||
reinterpret_cast<void **>(&pIServerEventsSink));
|
||
if (MUX_SUCCEEDED(mr))
|
||
{
|
||
mr = mux_CreateInstance(CID_ServerEventsSource, nullptr,
|
||
UseSameProcess, IID_IServerEventsControl,
|
||
reinterpret_cast<void **>(&m_pIServerEventsControl));
|
||
if (MUX_SUCCEEDED(mr))
|
||
{
|
||
m_pIServerEventsControl->Advise(pIServerEventsSink);
|
||
}
|
||
pIServerEventsSink->Release();
|
||
}
|
||
|
||
// Acquire core interfaces.
|
||
//
|
||
mr = mux_CreateInstance(CID_Notify, nullptr, UseSameProcess,
|
||
IID_INotify,
|
||
reinterpret_cast<void **>(&m_pINotify));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess,
|
||
IID_IObjectInfo,
|
||
reinterpret_cast<void **>(&m_pIObjectInfo));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess,
|
||
IID_IAttributeAccess,
|
||
reinterpret_cast<void **>(&m_pIAttributeAccess));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_Evaluator, nullptr, UseSameProcess,
|
||
IID_IEvaluator,
|
||
reinterpret_cast<void **>(&m_pIEvaluator));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
mr = mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess,
|
||
IID_IPermissions,
|
||
reinterpret_cast<void **>(&m_pIPermissions));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
return mr;
|
||
}
|
||
|
||
// Optional (#1654): an engine older than CID_GameConfig simply lacks the
|
||
// class, and every consumer defaults to the engine's compiled-in
|
||
// behaviour. Hard-failing here would make the module unloadable against
|
||
// a binary it otherwise works with.
|
||
//
|
||
mr = mux_CreateInstance(CID_GameConfig, nullptr, UseSameProcess,
|
||
IID_IGameConfig,
|
||
reinterpret_cast<void **>(&m_pIGameConfig));
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
m_pIGameConfig = nullptr;
|
||
}
|
||
|
||
// Log that we are alive.
|
||
//
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr2 = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr2) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module loaded."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
return mr;
|
||
}
|
||
|
||
CComsysMod::~CComsysMod()
|
||
{
|
||
// Release the storage interface.
|
||
//
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
m_pIStorage->Release();
|
||
m_pIStorage = nullptr;
|
||
}
|
||
|
||
// STL containers clean up automatically.
|
||
//
|
||
m_channels.clear();
|
||
m_comsys.clear();
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module unloading."));
|
||
m_pILog->end_log();
|
||
}
|
||
m_pILog->Release();
|
||
m_pILog = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIServerEventsControl)
|
||
{
|
||
m_pIServerEventsControl->Release();
|
||
m_pIServerEventsControl = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->Release();
|
||
m_pINotify = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->Release();
|
||
m_pIObjectInfo = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIAttributeAccess)
|
||
{
|
||
m_pIAttributeAccess->Release();
|
||
m_pIAttributeAccess = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIEvaluator)
|
||
{
|
||
m_pIEvaluator->Release();
|
||
m_pIEvaluator = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->Release();
|
||
m_pIPermissions = nullptr;
|
||
}
|
||
if (nullptr != m_pIGameConfig)
|
||
{
|
||
m_pIGameConfig->Release();
|
||
}
|
||
|
||
g_cComponents--;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::QueryInterface(MUX_IID iid, void **ppv)
|
||
{
|
||
if (mux_IID_IUnknown == iid)
|
||
{
|
||
*ppv = static_cast<mux_IComsysControl *>(this);
|
||
}
|
||
else if (IID_IComsysControl == iid)
|
||
{
|
||
*ppv = static_cast<mux_IComsysControl *>(this);
|
||
}
|
||
else if (IID_IServerEventsSink == iid)
|
||
{
|
||
*ppv = static_cast<mux_IServerEventsSink *>(this);
|
||
}
|
||
else
|
||
{
|
||
*ppv = nullptr;
|
||
return MUX_E_NOINTERFACE;
|
||
}
|
||
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
uint32_t CComsysMod::AddRef(void)
|
||
{
|
||
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
|
||
}
|
||
|
||
uint32_t CComsysMod::Release(void)
|
||
{
|
||
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
|
||
if (1 == prev)
|
||
{
|
||
delete this;
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channel data loading via engine storage interface.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CComsysMod::LoadChannels(void)
|
||
{
|
||
if (nullptr == m_pIStorage)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllChannels(
|
||
[](void *ctx, 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)
|
||
{
|
||
CComsysMod *self = static_cast<CComsysMod *>(ctx);
|
||
|
||
auto ch = std::make_unique<channel>();
|
||
|
||
if (nullptr != name)
|
||
{
|
||
strncpy(reinterpret_cast<char *>(ch->name),
|
||
reinterpret_cast<const char *>(name), MAX_CHANNEL_LEN);
|
||
ch->name[MAX_CHANNEL_LEN] = '\0';
|
||
}
|
||
if (nullptr != header)
|
||
{
|
||
strncpy(reinterpret_cast<char *>(ch->header),
|
||
reinterpret_cast<const char *>(header), MAX_HEADER_LEN);
|
||
ch->header[MAX_HEADER_LEN] = '\0';
|
||
}
|
||
|
||
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;
|
||
|
||
std::vector<UTF8> key(ch->name,
|
||
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
|
||
self->m_channels[key] = std::move(ch);
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
bool CComsysMod::LoadChannelUsers(void)
|
||
{
|
||
if (nullptr == m_pIStorage)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllChannelUsers(
|
||
[](void *ctx, const UTF8 *channel_name, int who,
|
||
bool is_on, bool comtitle_status, bool gag_join_leave,
|
||
const UTF8 *title)
|
||
{
|
||
CComsysMod *self = static_cast<CComsysMod *>(ctx);
|
||
|
||
if (nullptr == channel_name) return;
|
||
|
||
struct channel *ch = self->select_channel(channel_name);
|
||
if (nullptr == ch) return;
|
||
|
||
// #1195: skip invalid dbrefs; object listeners are allowed.
|
||
//
|
||
if (nullptr != self->m_pIObjectInfo)
|
||
{
|
||
bool bValid = false;
|
||
self->m_pIObjectInfo->IsValid(who, &bValid);
|
||
if (!bValid)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
comuser cu;
|
||
cu.who = who;
|
||
cu.bUserIsOn = is_on;
|
||
cu.ComTitleStatus = comtitle_status;
|
||
cu.bGagJoinLeave = gag_join_leave;
|
||
cu.bConnected = false;
|
||
|
||
if (nullptr != title && title[0] != '\0')
|
||
{
|
||
cu.title = reinterpret_cast<const char *>(title);
|
||
}
|
||
|
||
ch->users[who] = std::move(cu);
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
bool CComsysMod::LoadPlayerChannels(void)
|
||
{
|
||
if (nullptr == m_pIStorage)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
MUX_RESULT mr = m_pIStorage->LoadAllPlayerChannels(
|
||
[](void *ctx, int who, const UTF8 *alias,
|
||
const UTF8 *channel_name)
|
||
{
|
||
CComsysMod *self = static_cast<CComsysMod *>(ctx);
|
||
|
||
if (nullptr == alias || nullptr == channel_name) return;
|
||
|
||
comsys_t &c = self->m_comsys[who];
|
||
c.who = who;
|
||
|
||
com_alias ca;
|
||
ca.alias = reinterpret_cast<const char *>(alias);
|
||
ca.channel = reinterpret_cast<const char *>(channel_name);
|
||
c.aliases.push_back(std::move(ca));
|
||
}, this);
|
||
|
||
return MUX_SUCCEEDED(mr);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Internal data structure helpers.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
struct channel *CComsysMod::select_channel(const UTF8 *name)
|
||
{
|
||
std::vector<UTF8> key(name,
|
||
name + strlen(reinterpret_cast<const char *>(name)) + 1);
|
||
auto it = m_channels.find(key);
|
||
if (it != m_channels.end())
|
||
{
|
||
return it->second.get();
|
||
}
|
||
|
||
// Case-insensitive fallback.
|
||
//
|
||
for (auto &kv : m_channels)
|
||
{
|
||
if (0 == strcasecmp(reinterpret_cast<const char *>(name),
|
||
reinterpret_cast<const char *>(kv.second->name)))
|
||
{
|
||
return kv.second.get();
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
struct comuser *CComsysMod::select_user(struct channel *ch, dbref player)
|
||
{
|
||
if (nullptr == ch)
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
auto it = ch->users.find(player);
|
||
if (it != ch->users.end())
|
||
{
|
||
return &it->second;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
comsys_t &CComsysMod::get_comsys(dbref who)
|
||
{
|
||
comsys_t &c = m_comsys[who];
|
||
c.who = who;
|
||
return c;
|
||
}
|
||
|
||
const UTF8 *CComsysMod::get_channel_from_alias(dbref player, const UTF8 *alias)
|
||
{
|
||
static const UTF8 empty[] = { '\0' };
|
||
|
||
auto it = m_comsys.find(player);
|
||
if (it == m_comsys.end())
|
||
{
|
||
return empty;
|
||
}
|
||
|
||
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 empty;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Connect/disconnect helpers.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_comconnectraw_notify(dbref player, const UTF8 *chan)
|
||
{
|
||
struct channel *ch = select_channel(chan);
|
||
if (nullptr == ch)
|
||
{
|
||
return;
|
||
}
|
||
struct comuser *cu = select_user(ch, player);
|
||
if (nullptr == cu)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Only send if channel is LOUD, user is on, and player is not hidden.
|
||
//
|
||
if ( (ch->type & CHANNEL_LOUD)
|
||
&& cu->bUserIsOn)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
const UTF8 *pName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetName(player, &pName);
|
||
}
|
||
if (nullptr == pName)
|
||
{
|
||
pName = T("???");
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s %s has connected."),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(pName));
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
for (auto &kv : ch->users)
|
||
{
|
||
comuser &u = kv.second;
|
||
if (u.bConnected && u.bUserIsOn && !u.bGagJoinLeave)
|
||
{
|
||
m_pINotify->RawNotify(u.who, msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void CComsysMod::do_comdisconnectraw_notify(dbref player, const UTF8 *chan)
|
||
{
|
||
struct channel *ch = select_channel(chan);
|
||
if (nullptr == ch)
|
||
{
|
||
return;
|
||
}
|
||
struct comuser *cu = select_user(ch, player);
|
||
if (nullptr == cu)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ( (ch->type & CHANNEL_LOUD)
|
||
&& cu->bUserIsOn)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
const UTF8 *pName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetName(player, &pName);
|
||
}
|
||
if (nullptr == pName)
|
||
{
|
||
pName = T("???");
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s %s has disconnected."),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(pName));
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
for (auto &kv : ch->users)
|
||
{
|
||
comuser &u = kv.second;
|
||
if (u.bConnected && u.bUserIsOn && !u.bGagJoinLeave)
|
||
{
|
||
m_pINotify->RawNotify(u.who, msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void CComsysMod::do_comconnectchannel(dbref player, const UTF8 *channel,
|
||
const std::string &alias)
|
||
{
|
||
struct channel *ch = select_channel(channel);
|
||
if (nullptr != ch)
|
||
{
|
||
struct comuser *user = select_user(ch, player);
|
||
if (nullptr != user)
|
||
{
|
||
user->bConnected = true;
|
||
}
|
||
else if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Bad Comsys Alias: %s for Channel: %s"),
|
||
alias.c_str(),
|
||
reinterpret_cast<const char *>(channel));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
else if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Bad Comsys Alias: %s for Channel: %s"),
|
||
alias.c_str(),
|
||
reinterpret_cast<const char *>(channel));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
void CComsysMod::do_comdisconnectchannel(dbref player, const UTF8 *channel)
|
||
{
|
||
struct channel *ch = select_channel(channel);
|
||
if (nullptr == ch)
|
||
{
|
||
return;
|
||
}
|
||
|
||
struct comuser *user = select_user(ch, player);
|
||
if (nullptr != user)
|
||
{
|
||
user->bConnected = false;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Write-through helpers — delegate to engine storage interface.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::bump_revision(void)
|
||
{
|
||
// Wrap-safe for softcode gen-sync; softcode treats any change as dirty.
|
||
//
|
||
m_revision++;
|
||
if (m_revision < 0)
|
||
{
|
||
m_revision = 1;
|
||
}
|
||
}
|
||
|
||
// Report a refused storage write (#1630). Silent on success, so it can wrap
|
||
// a call directly: log_storage_failure(m_pIStorage->Foo(k), "Foo(k=%d)", k);
|
||
//
|
||
// The message carries the operation, the key that identifies the row, and the
|
||
// result code. #1620's history-write log is the same idea spelled out at one
|
||
// call site; this is that shape made cheap enough to use at all of them.
|
||
//
|
||
void CComsysMod::log_storage_failure(MUX_RESULT mr, const char *fmt, ...)
|
||
{
|
||
if ( MUX_SUCCEEDED(mr)
|
||
|| nullptr == m_pILog)
|
||
{
|
||
return;
|
||
}
|
||
|
||
char detail[192];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
mux_vsnprintf(reinterpret_cast<UTF8 *>(detail), sizeof(detail), reinterpret_cast<const UTF8 *>(fmt), ap);
|
||
va_end(ap);
|
||
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("DB"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 buf[256];
|
||
mux_sprintf(buf, sizeof(buf),
|
||
T("Comsys module: storage write refused: %s; result %d."),
|
||
detail, mr);
|
||
m_pILog->log_text(buf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
void CComsysMod::sqlite_wt_channel_user(const UTF8 *channel_name,
|
||
const comuser &user)
|
||
{
|
||
if (nullptr == m_pIStorage) return;
|
||
log_storage_failure(m_pIStorage->SyncChannelUser(channel_name, user.who,
|
||
user.bUserIsOn, user.ComTitleStatus, user.bGagJoinLeave,
|
||
reinterpret_cast<const UTF8 *>(user.title.c_str())),
|
||
"SyncChannelUser(channel=%s, who=#%d)",
|
||
reinterpret_cast<const char *>(channel_name),
|
||
static_cast<int>(user.who));
|
||
bump_revision();
|
||
}
|
||
|
||
void CComsysMod::sqlite_wt_channel(struct channel *ch)
|
||
{
|
||
if (nullptr == m_pIStorage) return;
|
||
log_storage_failure(m_pIStorage->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),
|
||
"SyncChannel(channel=%s)",
|
||
reinterpret_cast<const char *>(ch->name));
|
||
bump_revision();
|
||
}
|
||
|
||
void CComsysMod::sqlite_wt_player_channel(dbref who, const UTF8 *alias,
|
||
const UTF8 *channel_name)
|
||
{
|
||
if (nullptr == m_pIStorage) return;
|
||
log_storage_failure(
|
||
m_pIStorage->SyncPlayerChannel(who, alias, channel_name),
|
||
"SyncPlayerChannel(who=#%d, alias=%s, channel=%s)",
|
||
static_cast<int>(who), reinterpret_cast<const char *>(alias),
|
||
reinterpret_cast<const char *>(channel_name));
|
||
bump_revision();
|
||
}
|
||
|
||
void CComsysMod::sqlite_wt_delete_player_channel(dbref who, const UTF8 *alias)
|
||
{
|
||
if (nullptr == m_pIStorage) return;
|
||
log_storage_failure(m_pIStorage->DeletePlayerChannel(who, alias),
|
||
"DeletePlayerChannel(who=#%d, alias=%s)",
|
||
static_cast<int>(who), reinterpret_cast<const char *>(alias));
|
||
bump_revision();
|
||
}
|
||
|
||
void CComsysMod::sqlite_wt_delete_channel_user(const UTF8 *channel_name,
|
||
dbref who)
|
||
{
|
||
if (nullptr == m_pIStorage) return;
|
||
log_storage_failure(m_pIStorage->DeleteChannelUser(channel_name, who),
|
||
"DeleteChannelUser(channel=%s, who=#%d)",
|
||
reinterpret_cast<const char *>(channel_name), static_cast<int>(who));
|
||
bump_revision();
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channel access checks.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// Attribute numbers for channel-object locks (attrs.h A_LOCK/A_LUSE/A_LENTER).
|
||
// Duplicated here so the module does not include engine attrs.h.
|
||
//
|
||
static constexpr int kA_LOCK = 42;
|
||
static constexpr int kA_LENTER = 59;
|
||
static constexpr int kA_LUSE = 62;
|
||
|
||
// #1084: type-bit OR could_doit on channel object — engine comsys parity.
|
||
//
|
||
static bool channel_lock_ok(mux_IPermissions *pPerm, dbref player,
|
||
dbref chan_obj, int atr)
|
||
{
|
||
if (nullptr == pPerm || NOTHING == chan_obj)
|
||
{
|
||
return false;
|
||
}
|
||
bool ok = false;
|
||
pPerm->CouldDoit(player, chan_obj, atr, &ok);
|
||
return ok;
|
||
}
|
||
|
||
bool CComsysMod::test_transmit_access(dbref player, struct channel *ch)
|
||
{
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(player, &bCommAll);
|
||
if (bCommAll)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
int access;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(player, &bPlayer);
|
||
access = bPlayer ? CHANNEL_PLAYER_TRANSMIT : CHANNEL_OBJECT_TRANSMIT;
|
||
}
|
||
else
|
||
{
|
||
access = CHANNEL_PLAYER_TRANSMIT;
|
||
}
|
||
|
||
return ((ch->type & access) != 0)
|
||
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LUSE);
|
||
}
|
||
|
||
bool CComsysMod::test_receive_access(dbref player, struct channel *ch)
|
||
{
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(player, &bCommAll);
|
||
if (bCommAll)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
int access;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(player, &bPlayer);
|
||
access = bPlayer ? CHANNEL_PLAYER_RECEIVE : CHANNEL_OBJECT_RECEIVE;
|
||
}
|
||
else
|
||
{
|
||
access = CHANNEL_PLAYER_RECEIVE;
|
||
}
|
||
|
||
return ((ch->type & access) != 0)
|
||
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LENTER);
|
||
}
|
||
|
||
bool CComsysMod::test_join_access(dbref player, struct channel *ch)
|
||
{
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(player, &bCommAll);
|
||
if (bCommAll)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
int access;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(player, &bPlayer);
|
||
access = bPlayer ? CHANNEL_PLAYER_JOIN : CHANNEL_OBJECT_JOIN;
|
||
}
|
||
else
|
||
{
|
||
access = CHANNEL_PLAYER_JOIN;
|
||
}
|
||
|
||
return ((ch->type & access) != 0)
|
||
|| channel_lock_ok(m_pIPermissions, player, ch->chan_obj, kA_LOCK);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Policy helpers (#1194 / #1195).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
bool CComsysMod::flag_set(dbref obj, int word, unsigned int bit)
|
||
{
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
unsigned int flags = 0;
|
||
if (MUX_FAILED(m_pIObjectInfo->GetFlags(obj, word, &flags)))
|
||
{
|
||
return false;
|
||
}
|
||
return (flags & bit) != 0;
|
||
}
|
||
|
||
bool CComsysMod::is_wizard(dbref obj)
|
||
{
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
bool b = false;
|
||
m_pIPermissions->IsWizard(obj, &b);
|
||
return b;
|
||
}
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
bool b = false;
|
||
m_pIObjectInfo->IsWizard(obj, &b);
|
||
return b;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool CComsysMod::is_gagged(dbref obj)
|
||
{
|
||
return flag_set(obj, MOD_FLAG_WORD2, MOD_FLAG_GAGGED);
|
||
}
|
||
|
||
bool CComsysMod::is_hidden(dbref obj)
|
||
{
|
||
// Engine Hidden(x) is (Flags(x) & DARK) — FLAG_WORD1.
|
||
//
|
||
return flag_set(obj, MOD_FLAG_WORD1, MOD_FLAG_DARK);
|
||
}
|
||
|
||
bool CComsysMod::is_guest(dbref obj)
|
||
{
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
unsigned int powers = 0;
|
||
if (MUX_FAILED(m_pIObjectInfo->GetPowers(obj, &powers)))
|
||
{
|
||
return false;
|
||
}
|
||
return (powers & MOD_POW_GUEST) != 0;
|
||
}
|
||
|
||
bool CComsysMod::undead_connected(dbref obj)
|
||
{
|
||
// Engine UNDEAD(x): Good_obj && (not player || Connected).
|
||
//
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return false;
|
||
}
|
||
bool bValid = false;
|
||
m_pIObjectInfo->IsValid(obj, &bValid);
|
||
if (!bValid)
|
||
{
|
||
return false;
|
||
}
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(obj, &bPlayer);
|
||
if (!bPlayer)
|
||
{
|
||
return true;
|
||
}
|
||
bool bConnected = false;
|
||
m_pIObjectInfo->IsConnected(obj, &bConnected);
|
||
return bConnected;
|
||
}
|
||
|
||
bool CComsysMod::pay_channel_charge(dbref player, struct channel *ch)
|
||
{
|
||
if (nullptr == ch || 0 >= ch->charge)
|
||
{
|
||
return true;
|
||
}
|
||
if (nullptr == m_pIObjectInfo)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
const int cost = is_guest(player) ? 0 : ch->charge;
|
||
bool bPaid = false;
|
||
if (MUX_FAILED(m_pIObjectInfo->PayFor(player, cost, &bPaid)) || !bPaid)
|
||
{
|
||
return false;
|
||
}
|
||
ch->amount_col += ch->charge;
|
||
sqlite_wt_channel(ch);
|
||
m_pIObjectInfo->GiveTo(ch->charge_who, ch->charge);
|
||
return true;
|
||
}
|
||
|
||
bool CComsysMod::blocked_by_mogrify(dbref player, struct channel *ch,
|
||
const UTF8 *arg2)
|
||
{
|
||
// Engine call_mogrifier(MOGRIFY`BLOCK): non-empty result suppresses send.
|
||
//
|
||
if ( nullptr == ch
|
||
|| ch->chan_obj < 0
|
||
|| nullptr == m_pIAttributeAccess
|
||
|| nullptr == m_pIEvaluator
|
||
|| nullptr == arg2)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool bValid = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
UTF8 atext[MOD_LBUF_SIZE];
|
||
atext[0] = '\0';
|
||
size_t nLen = 0;
|
||
// GOD executor so AF_DARK channel-object attrs are readable.
|
||
//
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
|
||
T("MOGRIFY`BLOCK"), atext, sizeof(atext) - 1, &nLen);
|
||
if (MUX_FAILED(mr) || 0 == nLen || '\0' == atext[0])
|
||
{
|
||
return false;
|
||
}
|
||
|
||
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 *pName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetName(player, &pName);
|
||
}
|
||
if (nullptr == pName)
|
||
{
|
||
pName = T("???");
|
||
}
|
||
|
||
const UTF8 *block_args[5] = {
|
||
chattype, ch->name, arg2, pName, sdrBuf
|
||
};
|
||
|
||
UTF8 result[MOD_LBUF_SIZE];
|
||
result[0] = '\0';
|
||
size_t nResult = 0;
|
||
mr = m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj, player,
|
||
atext, block_args, 5, result, sizeof(result), &nResult);
|
||
if (MUX_FAILED(mr) || 0 == nResult || '\0' == result[0])
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, result);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Unsubscribe a player from a channel completely.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_delcomchannel(dbref player, const UTF8 *channel,
|
||
bool bQuiet)
|
||
{
|
||
struct channel *ch = select_channel(channel);
|
||
if (nullptr == ch)
|
||
{
|
||
return;
|
||
}
|
||
|
||
struct comuser *user = select_user(ch, player);
|
||
if (nullptr == user)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Order matters, and it is the engine's (comsys.cpp:2591). Clearing
|
||
// bConnected FIRST is why the leaver does not see their own departure
|
||
// broadcast -- SendChannelMessage skips disconnected users -- and is what
|
||
// makes the two implementations agree on the visible result.
|
||
//
|
||
do_comdisconnectchannel(player, channel);
|
||
|
||
if (!bQuiet)
|
||
{
|
||
if ( user->bUserIsOn
|
||
&& !is_hidden(player))
|
||
{
|
||
UTF8 pfxNormal[MOD_LBUF_SIZE];
|
||
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
|
||
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
|
||
pfxNoComtitle, sizeof(pfxNoComtitle));
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s has left this channel."),
|
||
reinterpret_cast<const char *>(pfxNormal));
|
||
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."),
|
||
reinterpret_cast<const char *>(pfxNoComtitle));
|
||
SendChannelMessage(player, ch, msg,
|
||
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
|
||
}
|
||
|
||
// The engine gives the leaver a personal confirmation (#1640). The
|
||
// module emitted only the channel broadcast, which the leaver cannot
|
||
// even see -- so `delcom` looked like it had done nothing, and any
|
||
// softcode or client trigger matching "^You have left channel"
|
||
// stopped firing under the module.
|
||
//
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You have left channel %s."),
|
||
reinterpret_cast<const char *>(channel));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
}
|
||
|
||
ch->users.erase(player);
|
||
sqlite_wt_delete_channel_user(channel, player);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sort aliases for display/lookup.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::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;
|
||
});
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channel message broadcasting.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Speaker prefix, with the comtitle the engine has always shown (#1640).
|
||
//
|
||
// The module never applied comtitles to anything: not speech, not poses, not
|
||
// join/leave, and not spoof channels -- where the comtitle is supposed to
|
||
// REPLACE the name entirely. Measured against the engine on identical
|
||
// databases, `comtitle d=Tester` then `d hello`:
|
||
//
|
||
// engine HDR Tester Wizard says, "hello"
|
||
// module HDR Wizard says, "hello"
|
||
//
|
||
// The module stored the title correctly and round-tripped it through
|
||
// SyncChannelUser; it simply never read it back out when composing a message.
|
||
//
|
||
// Two variants because the LISTENER decides, not the speaker: a player with
|
||
// comtitles off sees the plain name even when the speaker has one set. On a
|
||
// spoof channel the engine builds no second variant, so everyone sees the
|
||
// comtitle -- pNoComtitle stays empty and SendChannelMessage falls back.
|
||
//
|
||
// Follows the engine's BuildChannelMessage (comsys.cpp:1349) branch for
|
||
// branch, including that ComTitleStatus is ignored when the channel is spoof.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::BuildSpeakerPrefix(struct channel *ch,
|
||
const struct comuser *user, UTF8 *pNormal, size_t nNormal,
|
||
UTF8 *pNoComtitle, size_t nNoComtitle)
|
||
{
|
||
pNormal[0] = '\0';
|
||
pNoComtitle[0] = '\0';
|
||
|
||
const bool bSpoof = ((ch->type & CHANNEL_SPOOF) != 0);
|
||
|
||
const UTF8 *pMoniker = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetMoniker(user->who, &pMoniker);
|
||
}
|
||
if (nullptr == pMoniker)
|
||
{
|
||
pMoniker = T("???");
|
||
}
|
||
|
||
const bool bHasComTitle = !user->title.empty();
|
||
if ( !bHasComTitle
|
||
|| (!user->ComTitleStatus && !bSpoof))
|
||
{
|
||
mux_sprintf(pNormal, nNormal, T("%s %s"),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(pMoniker));
|
||
if (!bSpoof)
|
||
{
|
||
mux_sprintf(pNoComtitle, nNoComtitle, T("%s"),
|
||
reinterpret_cast<const char *>(pNormal));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// The engine evaluates the comtitle as softcode unless `eval_comtitle 0`
|
||
// (mudconf.eval_comtitle, default on). Since #1654 the flag reaches the
|
||
// module through mux_IGameConfig, queried per call rather than cached --
|
||
// a boot-time snapshot is #1613's bug. Against an engine without the
|
||
// interface, fall back to the default (evaluate), which is the old
|
||
// behaviour.
|
||
//
|
||
bool bEvalComtitle = true;
|
||
if (nullptr != m_pIGameConfig)
|
||
{
|
||
GAME_CONFIG gc;
|
||
memset(&gc, 0, sizeof(gc));
|
||
gc.cbSize = sizeof(gc);
|
||
if (MUX_SUCCEEDED(m_pIGameConfig->GetGameConfig(&gc)))
|
||
{
|
||
bEvalComtitle = gc.eval_comtitle;
|
||
}
|
||
}
|
||
|
||
UTF8 title[MOD_LBUF_SIZE];
|
||
mux_sprintf(title, sizeof(title), T("%s"),
|
||
user->title.c_str());
|
||
if ( bEvalComtitle
|
||
&& nullptr != m_pIEvaluator)
|
||
{
|
||
UTF8 evaled[MOD_LBUF_SIZE];
|
||
size_t nOut = 0;
|
||
if (MUX_SUCCEEDED(m_pIEvaluator->EvalWithArgs(user->who, user->who,
|
||
user->who, title, nullptr, 0, evaled, sizeof(evaled) - 1,
|
||
&nOut)))
|
||
{
|
||
evaled[nOut] = '\0';
|
||
mux_sprintf(title, sizeof(title), T("%s"),
|
||
reinterpret_cast<const char *>(evaled));
|
||
}
|
||
}
|
||
|
||
if (bSpoof)
|
||
{
|
||
// The comtitle stands in for the speaker.
|
||
//
|
||
mux_sprintf(pNormal, nNormal, T("%s %s"),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(title));
|
||
return;
|
||
}
|
||
|
||
mux_sprintf(pNormal, nNormal, T("%s %s %s"),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(title),
|
||
reinterpret_cast<const char *>(pMoniker));
|
||
mux_sprintf(pNoComtitle, nNoComtitle, T("%s %s"),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(pMoniker));
|
||
}
|
||
|
||
void CComsysMod::SendChannelMessage(dbref executor, struct channel *ch,
|
||
const UTF8 *msg, const UTF8 *msgNoComtitle, bool bJoinLeaveMsg)
|
||
{
|
||
ch->num_messages++;
|
||
sqlite_wt_channel(ch);
|
||
|
||
if (nullptr == m_pINotify)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// MOGRIFY`MESSAGE / `OVERRIDE / `FORMAT, and per-player CHATFORMAT
|
||
// (#1572). Only BLOCK and NOBUFFER were implemented here; the other
|
||
// three hooks and CHATFORMAT were silently ignored, so a channel
|
||
// configured with them behaved differently depending on which
|
||
// implementation happened to be live.
|
||
//
|
||
// Order and precedence follow the engine exactly (comsys.cpp:1705-1822),
|
||
// because "mostly the same" is what produced the divergence in the first
|
||
// place.
|
||
//
|
||
UTF8 sdrBuf[32];
|
||
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"),
|
||
static_cast<int>(executor));
|
||
|
||
UTF8 mogMsg[MOD_LBUF_SIZE];
|
||
UTF8 mogFmt[MOD_LBUF_SIZE];
|
||
mogMsg[0] = '\0';
|
||
mogFmt[0] = '\0';
|
||
bool bMogOverride = false;
|
||
|
||
// The engine evaluates these only for non-join/leave traffic
|
||
// (comsys.cpp:1701).
|
||
//
|
||
if (!bJoinLeaveMsg)
|
||
{
|
||
const UTF8 *mog_args[3] = { ch->name, msg, sdrBuf };
|
||
|
||
mogrify_eval(ch, executor, T("MESSAGE"), mog_args, 3,
|
||
mogMsg, sizeof(mogMsg));
|
||
|
||
UTF8 ovr[MOD_LBUF_SIZE];
|
||
if (mogrify_eval(ch, executor, T("OVERRIDE"), mog_args, 3,
|
||
ovr, sizeof(ovr)))
|
||
{
|
||
bMogOverride = mogrify_truthy(ovr);
|
||
}
|
||
|
||
// FORMAT sees the message AFTER MESSAGE may have replaced it.
|
||
//
|
||
const UTF8 *fmt_msg = ('\0' != mogMsg[0]) ? mogMsg : msg;
|
||
const UTF8 *fmt_args[3] = { ch->name, fmt_msg, sdrBuf };
|
||
mogrify_eval(ch, executor, T("FORMAT"), fmt_args, 3,
|
||
mogFmt, sizeof(mogFmt));
|
||
}
|
||
|
||
const UTF8 *effMsg = ('\0' != mogMsg[0]) ? mogMsg : msg;
|
||
|
||
// The no-comtitle variant follows the same mogrify substitution the
|
||
// normal one does (engine comsys.cpp:1766-1767): a MOGRIFY`MESSAGE
|
||
// result replaces both, because it replaced the whole line.
|
||
//
|
||
const UTF8 *effMsgNoComtitle = nullptr;
|
||
if ( nullptr != msgNoComtitle
|
||
&& '\0' != msgNoComtitle[0])
|
||
{
|
||
effMsgNoComtitle = ('\0' != mogMsg[0]) ? 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))
|
||
{
|
||
// Whether a comtitle is shown is the LISTENER's setting, not the
|
||
// speaker's. On a spoof channel there is no second variant.
|
||
//
|
||
const UTF8 *pMsg = ( user.ComTitleStatus
|
||
|| nullptr == effMsgNoComtitle)
|
||
? effMsg : effMsgNoComtitle;
|
||
|
||
// Channel-wide FORMAT wins outright when OVERRIDE is set.
|
||
//
|
||
if ('\0' != mogFmt[0] && bMogOverride)
|
||
{
|
||
m_pINotify->RawNotify(user.who, mogFmt);
|
||
continue;
|
||
}
|
||
|
||
// Per-player CHATFORMAT, unless OVERRIDE suppresses it.
|
||
//
|
||
if (!bMogOverride && nullptr != m_pIEvaluator)
|
||
{
|
||
UTF8 chatfmt[MOD_LBUF_SIZE];
|
||
size_t nFmt = 0;
|
||
if (MUX_SUCCEEDED(m_pIAttributeAccess->GetAttribute(
|
||
user.who, user.who, T("CHATFORMAT"),
|
||
chatfmt, sizeof(chatfmt) - 1, &nFmt))
|
||
&& 0 < nFmt)
|
||
{
|
||
chatfmt[nFmt] = '\0';
|
||
if ('\0' != chatfmt[0])
|
||
{
|
||
const UTF8 *cfa[3] = { ch->name, pMsg, sdrBuf };
|
||
UTF8 fmtbuf[MOD_LBUF_SIZE];
|
||
size_t nOut = 0;
|
||
if (MUX_SUCCEEDED(m_pIEvaluator->EvalWithArgs(
|
||
user.who, user.who, executor, chatfmt,
|
||
cfa, 3, fmtbuf, sizeof(fmtbuf) - 1, &nOut)))
|
||
{
|
||
fmtbuf[nOut] = '\0';
|
||
m_pINotify->RawNotify(user.who, fmtbuf);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if ('\0' != mogFmt[0])
|
||
{
|
||
m_pINotify->RawNotify(user.who, mogFmt);
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->RawNotify(user.who, pMsg);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Engine only evaluates MOGRIFY`* for non-join/leave traffic
|
||
// (comsys.cpp:1701). Pass that through so join/leave history is not
|
||
// suppressed by a sticky NOBUFFER=1 under the module alone.
|
||
//
|
||
RecordChannelHistory(executor, ch, msg, bJoinLeaveMsg);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Append a message to the channel's recall buffer (#1564).
|
||
//
|
||
// The engine's SendChannelMessage writes HISTORY_n at comsys.cpp:1851. The
|
||
// module never did, so do_crecall above read HISTORY_n attributes that
|
||
// nothing wrote: recall came back empty on a channel with obvious traffic,
|
||
// with cmsgs and cbuffer both reporting healthy values because those come
|
||
// from elsewhere. Silent, and invisible to any test that did not read the
|
||
// history back.
|
||
//
|
||
// The index must be num_messages % logmax computed AFTER the increment in
|
||
// the caller, which is exactly what do_crecall's read expects.
|
||
//
|
||
// Join and leave messages are logged too. The engine guards its logging
|
||
// block only on the channel object being valid, not on bJoinLeaveMsg, and
|
||
// its recall output does include "has joined this channel".
|
||
//
|
||
// GOD (dbref 1) is the executor, matching both the engine -- which writes
|
||
// with atr_add(..., GOD, ...) -- and this module's existing use of a GOD
|
||
// executor for channel-object attributes. The speaker is an ordinary
|
||
// member who will not usually control the channel object, so passing the
|
||
// executor through would fail bCanSetAttr for most messages.
|
||
//
|
||
// LOG_TIMESTAMPS (#1570): when the attribute is present and non-empty on the
|
||
// channel object, wrap the line as the engine does -- "[%s] %s" with
|
||
// CLinearTimeAbsolute::ReturnDateString(0). libmux exports that class, so
|
||
// the module can match the engine's format rather than inventing a second
|
||
// one with strftime. @cset/timestamp_logs sets or clears the attribute;
|
||
// without this consult it reported success while history stayed plain.
|
||
//
|
||
// MOGRIFY`NOBUFFER (#1572).
|
||
//
|
||
// The engine evaluates the hook with three arguments -- channel name, message,
|
||
// and the sender as "#<dbref>" -- and skips the recall buffer when the result
|
||
// is true by xlate() (comsys.cpp:1727). The module can do the same: it already
|
||
// holds mux_IEvaluator, whose EvalWithArgs exists for exactly this (the
|
||
// interface comment cites MOGRIFY`BLOCK, #1194).
|
||
//
|
||
// Evaluated via EvalWithArgs (same path as blocked_by_mogrify), with xlate-
|
||
// style truth on the result so "0" and empty stay off. A hook added now
|
||
// should match the engine rather than invent a second meaning.
|
||
//
|
||
// Evaluate MOGRIFY`<suffix> on the channel object (#1572).
|
||
//
|
||
// The engine's call_mogrifier (comsys.cpp:1307) reads the attribute and runs
|
||
// it with mux_exec as the channel object, with the speaker as enactor. This
|
||
// is that, through the module's evaluator interface. A GOD executor is used
|
||
// for the attribute READ so AF_DARK channel-object attributes are visible --
|
||
// the speaker is an ordinary member and usually cannot see them.
|
||
//
|
||
bool CComsysMod::mogrify_eval(struct channel *ch, dbref executor,
|
||
const UTF8 *suffix, const UTF8 *args[], int nargs,
|
||
UTF8 *out, size_t outsz)
|
||
{
|
||
if (nullptr != out && 0 < outsz)
|
||
{
|
||
out[0] = '\0';
|
||
}
|
||
if ( nullptr == ch
|
||
|| ch->chan_obj < 0
|
||
|| nullptr == out
|
||
|| nullptr == m_pIAttributeAccess
|
||
|| nullptr == m_pIEvaluator)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool bValid = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
UTF8 aname[64];
|
||
mux_sprintf(aname, sizeof(aname), T("MOGRIFY`%s"),
|
||
reinterpret_cast<const char *>(suffix));
|
||
|
||
UTF8 expr[MOD_LBUF_SIZE];
|
||
size_t nExpr = 0;
|
||
if (MUX_FAILED(m_pIAttributeAccess->GetAttribute(1, ch->chan_obj, aname,
|
||
expr, sizeof(expr) - 1, &nExpr))
|
||
|| 0 == nExpr)
|
||
{
|
||
return false;
|
||
}
|
||
expr[nExpr] = '\0';
|
||
if ('\0' == expr[0])
|
||
{
|
||
return false;
|
||
}
|
||
|
||
size_t nResult = 0;
|
||
if (MUX_FAILED(m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj,
|
||
executor, expr, args, nargs, out, outsz - 1, &nResult)))
|
||
{
|
||
out[0] = '\0';
|
||
return false;
|
||
}
|
||
out[nResult] = '\0';
|
||
return true;
|
||
}
|
||
|
||
// Approximate xlate() (functions.cpp) for the common 0/1/empty results
|
||
// without pulling engine-private xlate into the module. Leading "#-" is
|
||
// false; bare "#N" is true; "0" is false; other non-empty is true.
|
||
//
|
||
bool CComsysMod::mogrify_truthy(const UTF8 *result)
|
||
{
|
||
const char *p = reinterpret_cast<const char *>(result);
|
||
while (' ' == *p) p++;
|
||
if ('\0' == *p) return false;
|
||
if ('#' == *p) return ('-' != p[1]);
|
||
if ('0' == p[0] && '\0' == p[1]) return false;
|
||
return true;
|
||
}
|
||
|
||
bool CComsysMod::nobuffer_by_mogrify(dbref executor, struct channel *ch,
|
||
const UTF8 *msg)
|
||
{
|
||
if ( nullptr == ch
|
||
|| ch->chan_obj < 0
|
||
|| nullptr == m_pIAttributeAccess
|
||
|| nullptr == m_pIEvaluator
|
||
|| nullptr == msg)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool bValid = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
UTF8 expr[MOD_LBUF_SIZE];
|
||
size_t nExpr = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
|
||
T("MOGRIFY`NOBUFFER"), expr, sizeof(expr) - 1, &nExpr);
|
||
if (MUX_FAILED(mr) || 0 == nExpr)
|
||
{
|
||
return false;
|
||
}
|
||
expr[nExpr] = '\0';
|
||
|
||
UTF8 sdrBuf[32];
|
||
mux_sprintf(sdrBuf, sizeof(sdrBuf), T("#%d"),
|
||
static_cast<int>(executor));
|
||
const UTF8 *args[3] = { ch->name, msg, sdrBuf };
|
||
|
||
UTF8 result[MOD_LBUF_SIZE];
|
||
size_t nResult = 0;
|
||
// Match call_mogrifier / blocked_by_mogrify: evaluate as the channel
|
||
// object with the speaker as enactor (comsys.cpp:1342).
|
||
//
|
||
mr = m_pIEvaluator->EvalWithArgs(ch->chan_obj, ch->chan_obj, executor,
|
||
expr, args, 3, result, sizeof(result) - 1, &nResult);
|
||
if (MUX_FAILED(mr) || 0 == nResult)
|
||
{
|
||
return false;
|
||
}
|
||
result[nResult] = '\0';
|
||
|
||
// Approximate xlate() (functions.cpp) for the common 0/1/empty results
|
||
// without pulling engine-private xlate into the module. Leading "#-" is
|
||
// false; bare "#N" is true; "0" is false; other non-empty is true.
|
||
//
|
||
const char *p = reinterpret_cast<const char *>(result);
|
||
while (' ' == *p) p++;
|
||
if ('\0' == *p) return false;
|
||
if ('#' == *p) return ('-' != p[1]);
|
||
if ('0' == p[0] && '\0' == p[1]) return false;
|
||
return true;
|
||
}
|
||
|
||
void CComsysMod::RecordChannelHistory(dbref executor, struct channel *ch,
|
||
const UTF8 *msg, bool bJoinLeaveMsg)
|
||
{
|
||
// MOGRIFY`NOBUFFER keeps a line out of the recall buffer (#1572). The
|
||
// engine honours it; the module did not, so a channel that suppressed
|
||
// history under the engine still recorded it under the module.
|
||
// Join/leave is not evaluated by the engine either (comsys.cpp:1701).
|
||
//
|
||
if ( !bJoinLeaveMsg
|
||
&& nobuffer_by_mogrify(executor, ch, msg))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ( nullptr == m_pIAttributeAccess
|
||
|| nullptr == m_pIObjectInfo
|
||
|| nullptr == ch
|
||
|| nullptr == msg)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool bValid = false;
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
|
||
if (!bValid)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UTF8 valbuf[64];
|
||
size_t nLen = 0;
|
||
int64_t logmax = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
|
||
T("MAX_LOG"), valbuf, sizeof(valbuf) - 1, &nLen);
|
||
if (MUX_SUCCEEDED(mr) && 0 < nLen)
|
||
{
|
||
valbuf[nLen] = '\0';
|
||
logmax = mux_atoi64(valbuf);
|
||
}
|
||
|
||
if (logmax < 1)
|
||
{
|
||
// Channel does not log; do_crecall reports the same condition.
|
||
//
|
||
return;
|
||
}
|
||
|
||
UTF8 histattr[64];
|
||
mux_sprintf(histattr, sizeof(histattr),
|
||
T("HISTORY_%d"), ((ch->num_messages % logmax) + logmax) % logmax);
|
||
|
||
const UTF8 *payload = msg;
|
||
UTF8 stamped[MOD_LBUF_SIZE];
|
||
UTF8 tsattr[64];
|
||
size_t nTs = 0;
|
||
mr = m_pIAttributeAccess->GetAttribute(1, ch->chan_obj,
|
||
T("LOG_TIMESTAMPS"), tsattr, sizeof(tsattr) - 1, &nTs);
|
||
// Engine uses atr_get_info (presence); @cset clears by writing "".
|
||
// Treat non-empty as on, empty/missing as off.
|
||
//
|
||
if (MUX_SUCCEEDED(mr) && 0 < nTs)
|
||
{
|
||
CLinearTimeAbsolute ltaNow;
|
||
ltaNow.GetLocal();
|
||
// Same shape as comsys.cpp:1857-1865.
|
||
//
|
||
mux_sprintf(stamped, sizeof(stamped),
|
||
T("[%s] %s"),
|
||
reinterpret_cast<const char *>(ltaNow.ReturnDateString(0)),
|
||
reinterpret_cast<const char *>(msg));
|
||
payload = stamped;
|
||
}
|
||
|
||
// Check the write (#1620). SetAttribute is permission-checked, and the
|
||
// engine writes these same attributes as GOD with AF_CONST -- which
|
||
// bCanSetAttr denies in every branch, God included. So on a game that
|
||
// ever ran the built-in comsys, a HISTORY_n slot the engine wrote is
|
||
// refused here forever once the ring wraps onto it. num_messages is a
|
||
// relational column and keeps counting, so the counter and the history
|
||
// silently diverge: exactly #1564's symptom, from a different cause.
|
||
//
|
||
// This does not make the write land. It makes a lost message say so
|
||
// instead of vanishing, which is the difference between #1564 taking an
|
||
// investigation and taking a log grep.
|
||
//
|
||
// Through the engine (#1620). The permission-checked route is refused
|
||
// by the AF_CONST the engine puts on these, so a slot the engine wrote
|
||
// first was frozen against the module forever once the ring wrapped
|
||
// onto it -- and num_messages kept counting, which is #1564's symptom
|
||
// arriving by a second route.
|
||
//
|
||
MUX_RESULT mrHist = (nullptr != m_pIStorage)
|
||
? m_pIStorage->SetChannelAttr(ch->name, histattr, payload)
|
||
: MUX_E_FAIL;
|
||
if (MUX_FAILED(mrHist) && nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("HIST"));
|
||
if (fStarted)
|
||
{
|
||
UTF8 logbuf[256];
|
||
mux_sprintf(logbuf, sizeof(logbuf),
|
||
T("Comsys module: channel history write refused (%s on #%d, result %d); message not recorded."),
|
||
reinterpret_cast<const char *>(histattr),
|
||
static_cast<int>(ch->chan_obj), mrHist);
|
||
m_pILog->log_text(logbuf);
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Join/leave channel.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_joinchannel(dbref player, struct channel *ch)
|
||
{
|
||
struct comuser *user = select_user(ch, player);
|
||
|
||
if (nullptr == user)
|
||
{
|
||
// #1195: enforce membership cap like the engine.
|
||
//
|
||
if (static_cast<int>(ch->users.size()) >= MAX_USERS_PER_CHANNEL)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Too many people on channel %s already."),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return;
|
||
}
|
||
|
||
comuser cu;
|
||
cu.who = player;
|
||
cu.bUserIsOn = true;
|
||
cu.ComTitleStatus = true;
|
||
cu.bGagJoinLeave = false;
|
||
// #1195: UNDEAD connection, not "always true".
|
||
//
|
||
cu.bConnected = undead_connected(player);
|
||
|
||
auto result = ch->users.emplace(player, std::move(cu));
|
||
user = &result.first->second;
|
||
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
}
|
||
else if (!user->bUserIsOn)
|
||
{
|
||
user->bUserIsOn = true;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
}
|
||
else
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You are already on channel %s."),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Engine suppresses join announce for Hidden players.
|
||
//
|
||
if (is_hidden(player))
|
||
{
|
||
return;
|
||
}
|
||
|
||
UTF8 pfxNormal[MOD_LBUF_SIZE];
|
||
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
|
||
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
|
||
pfxNoComtitle, sizeof(pfxNoComtitle));
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s has joined this channel."),
|
||
reinterpret_cast<const char *>(pfxNormal));
|
||
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle),
|
||
M_("%s has joined this channel."),
|
||
reinterpret_cast<const char *>(pfxNoComtitle));
|
||
SendChannelMessage(player, ch, msg,
|
||
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
|
||
}
|
||
|
||
void CComsysMod::do_leavechannel(dbref player, struct channel *ch)
|
||
{
|
||
struct comuser *user = select_user(ch, player);
|
||
if (nullptr == user)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You have left channel %s."),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
if (user->bUserIsOn)
|
||
{
|
||
// Engine suppresses leave announce for Hidden players.
|
||
//
|
||
if (!is_hidden(player))
|
||
{
|
||
UTF8 pfxNormal[MOD_LBUF_SIZE];
|
||
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
|
||
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
|
||
pfxNoComtitle, sizeof(pfxNoComtitle));
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s has left this channel."),
|
||
reinterpret_cast<const char *>(pfxNormal));
|
||
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."),
|
||
reinterpret_cast<const char *>(pfxNoComtitle));
|
||
SendChannelMessage(player, ch, msg,
|
||
('\0' != pfxNoComtitle[0]) ? msgNoComtitle : nullptr, true);
|
||
}
|
||
|
||
user->bUserIsOn = false;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channel who list.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_comwho(dbref player, struct channel *ch)
|
||
{
|
||
if (nullptr == m_pINotify || nullptr == m_pIObjectInfo)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// #1195: mirror engine do_comwho — players vs objects; filter Hidden
|
||
// unless Wizard_Who / See_Hidden.
|
||
//
|
||
bool bSeeHidden = false;
|
||
bool bWizardWho = false;
|
||
m_pIObjectInfo->SeeHidden(player, &bSeeHidden);
|
||
m_pIObjectInfo->WizardWho(player, &bWizardWho);
|
||
const bool bMaySeeHidden = bSeeHidden || bWizardWho;
|
||
|
||
m_pINotify->RawNotify(player, M_("-- Players --"));
|
||
|
||
for (auto &kv : ch->users)
|
||
{
|
||
comuser &user = kv.second;
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
|
||
if (!bPlayer)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool bConnected = false;
|
||
m_pIObjectInfo->IsConnected(user.who, &bConnected);
|
||
const bool bHidden = is_hidden(user.who);
|
||
|
||
if (bConnected && (!bHidden || bMaySeeHidden))
|
||
{
|
||
if (user.bUserIsOn)
|
||
{
|
||
const UTF8 *pName = nullptr;
|
||
m_pIObjectInfo->GetMoniker(user.who, &pName);
|
||
if (nullptr != pName)
|
||
{
|
||
m_pINotify->RawNotify(player, pName);
|
||
}
|
||
}
|
||
}
|
||
else if (!bHidden)
|
||
{
|
||
// Stale presence — disconnect from this channel's view.
|
||
//
|
||
do_comdisconnectchannel(user.who, ch->name);
|
||
}
|
||
}
|
||
|
||
m_pINotify->RawNotify(player, M_("-- Objects --"));
|
||
|
||
for (auto &kv : ch->users)
|
||
{
|
||
const comuser &user = kv.second;
|
||
bool bPlayer = false;
|
||
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
|
||
if (bPlayer)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
bool bGoing = false;
|
||
m_pIObjectInfo->IsGoing(user.who, &bGoing);
|
||
if (bGoing)
|
||
{
|
||
continue;
|
||
}
|
||
if (user.bUserIsOn)
|
||
{
|
||
const UTF8 *pName = nullptr;
|
||
m_pIObjectInfo->GetMoniker(user.who, &pName);
|
||
if (nullptr != pName)
|
||
{
|
||
m_pINotify->RawNotify(player, pName);
|
||
}
|
||
}
|
||
}
|
||
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Recall channel message history from channel object attributes.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_comlast(dbref player, struct channel *ch, int arg)
|
||
{
|
||
if (nullptr == m_pINotify || nullptr == m_pIAttributeAccess)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Validate the channel object.
|
||
//
|
||
bool bValid = false;
|
||
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("Channel does not have an object."));
|
||
return;
|
||
}
|
||
|
||
const dbref obj = ch->chan_obj;
|
||
|
||
// Read MAX_LOG attribute to determine logging depth.
|
||
//
|
||
UTF8 valbuf[64];
|
||
size_t nLen = 0;
|
||
int64_t logmax = 0;
|
||
MUX_RESULT mr = m_pIAttributeAccess->GetAttribute(player, obj,
|
||
T("MAX_LOG"), valbuf, sizeof(valbuf) - 1, &nLen);
|
||
if (MUX_SUCCEEDED(mr) && 0 < nLen)
|
||
{
|
||
valbuf[nLen] = '\0';
|
||
logmax = mux_atoi64(valbuf);
|
||
}
|
||
|
||
if (logmax < 1)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("Channel does not log."));
|
||
return;
|
||
}
|
||
|
||
if (arg < 1)
|
||
{
|
||
arg = 1;
|
||
}
|
||
if (arg > logmax)
|
||
{
|
||
arg = logmax;
|
||
}
|
||
|
||
int histnum = ch->num_messages - arg;
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s -- Begin Comsys Recall --"),
|
||
reinterpret_cast<const char *>(ch->header));
|
||
m_pINotify->RawNotify(player, msg);
|
||
|
||
for (int count = 0; count < arg; count++)
|
||
{
|
||
histnum++;
|
||
UTF8 attrname[64];
|
||
mux_sprintf(attrname, sizeof(attrname),
|
||
T("HISTORY_%d"), ((histnum % logmax) + logmax) % logmax);
|
||
|
||
UTF8 message[MOD_LBUF_SIZE];
|
||
size_t msgLen = 0;
|
||
mr = m_pIAttributeAccess->GetAttribute(player, obj,
|
||
attrname, message, sizeof(message) - 1, &msgLen);
|
||
if (MUX_SUCCEEDED(mr) && 0 < msgLen)
|
||
{
|
||
message[msgLen] = '\0';
|
||
m_pINotify->RawNotify(player, message);
|
||
}
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s -- End Comsys Recall --"),
|
||
reinterpret_cast<const char *>(ch->header));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Process a channel message (alias dispatch target).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::do_processcom(dbref player, const UTF8 *arg1, UTF8 *arg2)
|
||
{
|
||
if (nullptr == arg2 || '\0' == *arg2)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player, M_("No message."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Truncate excessively long messages.
|
||
//
|
||
if (3500 < strlen(reinterpret_cast<const char *>(arg2)))
|
||
{
|
||
arg2[3500] = '\0';
|
||
}
|
||
|
||
struct channel *ch = select_channel(arg1);
|
||
if (nullptr == ch)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Unknown channel %s."),
|
||
reinterpret_cast<const char *>(arg1));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return;
|
||
}
|
||
|
||
struct comuser *user = select_user(ch, player);
|
||
if (nullptr == user)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("You are not listed as on that channel. "
|
||
"Delete this alias and readd."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Handle sub-commands.
|
||
//
|
||
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "on"))
|
||
{
|
||
do_joinchannel(player, ch);
|
||
return;
|
||
}
|
||
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "off"))
|
||
{
|
||
do_leavechannel(player, ch);
|
||
return;
|
||
}
|
||
|
||
if (!user->bUserIsOn)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You must be on %s to do that."),
|
||
reinterpret_cast<const char *>(arg1));
|
||
m_pINotify->RawNotify(player, msg);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (0 == strcmp(reinterpret_cast<const char *>(arg2), "who"))
|
||
{
|
||
do_comwho(player, ch);
|
||
return;
|
||
}
|
||
|
||
// Handle "last [N]" — channel history recall.
|
||
//
|
||
if (0 == strncmp(reinterpret_cast<const char *>(arg2), "last", 4)
|
||
&& ('\0' == arg2[4]
|
||
|| (' ' == arg2[4]
|
||
&& '\0' != arg2[5])))
|
||
{
|
||
int64_t nRecall = 10; // DFLT_RECALL_REQUEST
|
||
if (' ' == arg2[4])
|
||
{
|
||
nRecall = mux_atoi64(arg2 + 5);
|
||
}
|
||
int nRecallI = (nRecall > INT_MAX) ? INT_MAX
|
||
: (nRecall < 0) ? 0 : static_cast<int>(nRecall);
|
||
do_comlast(player, ch, nRecallI);
|
||
return;
|
||
}
|
||
|
||
// #1194: Gagged (non-wizard) may not speak on channels.
|
||
//
|
||
if (is_gagged(player) && !is_wizard(player))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("GAGGED players may not speak on channels."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Check transmit access.
|
||
//
|
||
if (!test_transmit_access(player, ch))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("That channel type cannot be transmitted on."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// #1194: channel charge (payfor + giveto to charge_who).
|
||
//
|
||
if (!pay_channel_charge(player, ch))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(player,
|
||
M_("You don’t have enough coins."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// #1194: MOGRIFY`BLOCK on the channel object.
|
||
//
|
||
if (blocked_by_mogrify(player, ch, arg2))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Build and send the message. The speaker prefix carries the comtitle
|
||
// (#1640); everything after it is unchanged.
|
||
//
|
||
UTF8 pfxNormal[MOD_LBUF_SIZE];
|
||
UTF8 pfxNoComtitle[MOD_LBUF_SIZE];
|
||
BuildSpeakerPrefix(ch, user, pfxNormal, sizeof(pfxNormal),
|
||
pfxNoComtitle, sizeof(pfxNoComtitle));
|
||
const bool bHaveNoComtitle = ('\0' != pfxNoComtitle[0]);
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
UTF8 msgNoComtitle[MOD_LBUF_SIZE];
|
||
msgNoComtitle[0] = '\0';
|
||
const char *pPose = reinterpret_cast<const char *>(arg2);
|
||
|
||
// One format string per shape, applied to both prefixes, so the two
|
||
// variants cannot drift apart the way the engine's and the module's did.
|
||
//
|
||
const UTF8 *pFmt;
|
||
if (':' == pPose[0])
|
||
{
|
||
// Pose: "<prefix> <action>", with the leading space already supplied
|
||
// when the pose starts with one.
|
||
//
|
||
pPose++;
|
||
if (' ' == *pPose)
|
||
{
|
||
pPose++;
|
||
pFmt = T("%s%s");
|
||
}
|
||
else
|
||
{
|
||
pFmt = T("%s %s");
|
||
}
|
||
}
|
||
else if (';' == pPose[0])
|
||
{
|
||
// Semipose: "<prefix><text>"
|
||
//
|
||
pPose++;
|
||
pFmt = T("%s%s");
|
||
}
|
||
else
|
||
{
|
||
// Say: "<prefix> says, “<text>”"
|
||
//
|
||
pFmt = M_("%s says, “%s”");
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg), pFmt,
|
||
reinterpret_cast<const char *>(pfxNormal), pPose);
|
||
if (bHaveNoComtitle)
|
||
{
|
||
mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), pFmt,
|
||
reinterpret_cast<const char *>(pfxNoComtitle), pPose);
|
||
}
|
||
|
||
SendChannelMessage(player, ch, msg,
|
||
bHaveNoComtitle ? msgNoComtitle : nullptr, false);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// mux_IComsysControl implementation.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::Initialize(mux_IComsysStorage *pStorage)
|
||
{
|
||
if (nullptr == pStorage)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
m_pIStorage = pStorage;
|
||
m_pIStorage->AddRef();
|
||
|
||
// Load all channel data from SQLite.
|
||
//
|
||
if (!LoadChannels())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module: LoadChannels failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!LoadChannelUsers())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module: LoadChannelUsers failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!LoadPlayerChannels())
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module: LoadPlayerChannels failed."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
m_pILog->start_log(&fStarted, LOG_ALWAYS, T("COM"), T("LOAD"));
|
||
if (fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module: loaded "));
|
||
m_pILog->log_number(static_cast<int>(m_channels.size()));
|
||
m_pILog->log_text(T(" channels from SQLite."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::PlayerConnect(dbref player)
|
||
{
|
||
auto it = m_comsys.find(player);
|
||
if (it == m_comsys.end())
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
comsys_t &c = it->second;
|
||
|
||
// Track which channels we have already connected to (avoid duplicates).
|
||
//
|
||
std::vector<std::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);
|
||
do_comconnectchannel(player,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
|
||
ca.alias);
|
||
do_comconnectraw_notify(player,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
|
||
}
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::PlayerDisconnect(dbref player)
|
||
{
|
||
auto it = m_comsys.find(player);
|
||
if (it == m_comsys.end())
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
comsys_t &c = it->second;
|
||
|
||
std::vector<std::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);
|
||
do_comdisconnectchannel(player,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
|
||
do_comdisconnectraw_notify(player,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
|
||
}
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::PlayerNuke(dbref player)
|
||
{
|
||
if (player < 0)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Mirror engine ReleaseAllResources comsys side (#1193):
|
||
// disconnect presence, clear aliases/membership, destroy owned
|
||
// channels, drop per-player comsys row, and purge SQLite player_channels.
|
||
//
|
||
|
||
// 1. Quiet disconnect of channel presence.
|
||
//
|
||
PlayerDisconnect(player);
|
||
|
||
// 2. Remove all aliases and channel membership for this player.
|
||
//
|
||
auto itCom = m_comsys.find(player);
|
||
if (itCom != m_comsys.end())
|
||
{
|
||
comsys_t &c = itCom->second;
|
||
std::vector<com_alias> aliases = c.aliases;
|
||
for (const auto &ca : aliases)
|
||
{
|
||
do_delcomchannel(player,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()), true);
|
||
sqlite_wt_delete_player_channel(player,
|
||
reinterpret_cast<const UTF8 *>(ca.alias.c_str()));
|
||
}
|
||
c.aliases.clear();
|
||
m_comsys.erase(itCom);
|
||
}
|
||
|
||
// 3. Strip residual membership (joined without a surviving alias).
|
||
//
|
||
for (auto &kv : m_channels)
|
||
{
|
||
struct channel *ch = kv.second.get();
|
||
if (nullptr == ch)
|
||
{
|
||
continue;
|
||
}
|
||
if (0 != ch->users.erase(player))
|
||
{
|
||
sqlite_wt_delete_channel_user(ch->name, player);
|
||
}
|
||
}
|
||
|
||
// 4. Destroy channels owned by this player. SQLite ON DELETE CASCADE
|
||
// clears channel_users / player_channels for those channel names; also
|
||
// drop other players' in-memory aliases that pointed at them.
|
||
//
|
||
std::vector<std::string> destroyedNames;
|
||
for (auto it = m_channels.begin(); it != m_channels.end(); )
|
||
{
|
||
if (player == it->second->charge_who)
|
||
{
|
||
destroyedNames.emplace_back(
|
||
reinterpret_cast<const char *>(it->second->name));
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
log_storage_failure(
|
||
m_pIStorage->DeleteChannel(it->second->name),
|
||
"DeleteChannel(channel=%s) [owner #%d destroyed]",
|
||
reinterpret_cast<const char *>(it->second->name),
|
||
static_cast<int>(player));
|
||
bump_revision();
|
||
}
|
||
it = m_channels.erase(it);
|
||
}
|
||
else
|
||
{
|
||
++it;
|
||
}
|
||
}
|
||
|
||
if (!destroyedNames.empty())
|
||
{
|
||
for (auto &kv : m_comsys)
|
||
{
|
||
comsys_t &c = kv.second;
|
||
for (auto ait = c.aliases.begin(); ait != c.aliases.end(); )
|
||
{
|
||
bool bHit = false;
|
||
for (const auto &nm : destroyedNames)
|
||
{
|
||
if (ait->channel == nm)
|
||
{
|
||
bHit = true;
|
||
break;
|
||
}
|
||
}
|
||
if (bHit)
|
||
{
|
||
sqlite_wt_delete_player_channel(c.who,
|
||
reinterpret_cast<const UTF8 *>(ait->alias.c_str()));
|
||
ait = c.aliases.erase(ait);
|
||
}
|
||
else
|
||
{
|
||
++ait;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Belt-and-suspenders: remaining player_channels rows for this who.
|
||
//
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
log_storage_failure(m_pIStorage->DeleteAllPlayerChannels(player),
|
||
"DeleteAllPlayerChannels(who=#%d)", static_cast<int>(player));
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// allcom — send command to all channels.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::AllCom(dbref executor, const UTF8 *pAction)
|
||
{
|
||
if (nullptr == pAction)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
const char *a = reinterpret_cast<const char *>(pAction);
|
||
if (0 != strcmp(a, "who") && 0 != strcmp(a, "on") && 0 != strcmp(a, "off"))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Only options available are: on, off and who."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
auto it = m_comsys.find(executor);
|
||
if (it == m_comsys.end())
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
comsys_t &c = it->second;
|
||
|
||
for (const auto &ca : c.aliases)
|
||
{
|
||
do_processcom(executor,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
|
||
const_cast<UTF8 *>(pAction));
|
||
if (0 == strcmp(a, "who") && nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor, T(""));
|
||
}
|
||
}
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// comlist — list player's channel aliases.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::ComList(dbref executor, const UTF8 *pPattern)
|
||
{
|
||
if (nullptr == m_pINotify)
|
||
{
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
// comlist schema (#1667 Phase 4 A3) — same as engine do_comlist.
|
||
//
|
||
static const size_t kComlistAliasCols = 15;
|
||
static const size_t kComlistChannelCols = 18;
|
||
|
||
{
|
||
UTF8 header[256];
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Alias"), kComlistAliasCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Channel"), kComlistChannelCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_bytes(header, sizeof(header), pos,
|
||
reinterpret_cast<const char *>(M_("Status")));
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_bytes(header, sizeof(header), pos,
|
||
reinterpret_cast<const char *>(M_("Title")));
|
||
m_pINotify->RawNotify(executor, header);
|
||
}
|
||
|
||
auto it = m_comsys.find(executor);
|
||
if (it == m_comsys.end())
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("-- End of comlist --"));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
comsys_t &c = it->second;
|
||
bool bWild = (nullptr != pPattern && '\0' != *pPattern);
|
||
|
||
for (const auto &ca : c.aliases)
|
||
{
|
||
struct channel *ch = select_channel(
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()));
|
||
struct comuser *user = (nullptr != ch)
|
||
? select_user(ch, executor) : nullptr;
|
||
|
||
if (nullptr != user)
|
||
{
|
||
if (bWild && std::string::npos == ca.channel.find(
|
||
reinterpret_cast<const char *>(pPattern)))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
UTF8 msg[256];
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(msg, sizeof(msg), pos,
|
||
reinterpret_cast<const UTF8 *>(ca.alias.c_str()),
|
||
kComlistAliasCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
pos = append_ljust_field(msg, sizeof(msg), pos,
|
||
reinterpret_cast<const UTF8 *>(ca.channel.c_str()),
|
||
kComlistChannelCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
if (pos < sizeof(msg))
|
||
{
|
||
mux_sprintf(msg + pos, sizeof(msg) - 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()));
|
||
}
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
else
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Bad Comsys Alias: %s for Channel: %s"),
|
||
ca.alias.c_str(),
|
||
ca.channel.c_str());
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
}
|
||
|
||
m_pINotify->RawNotify(executor, M_("-- End of comlist --"));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// comtitle — set/get comtitle for a channel alias.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::ComTitle(dbref executor, const UTF8 *pAlias,
|
||
const UTF8 *pTitle, int key)
|
||
{
|
||
if (nullptr == pAlias || '\0' == *pAlias)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Need an alias to do comtitle."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
const UTF8 *chName = get_channel_from_alias(executor, pAlias);
|
||
if ('\0' == chName[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("Unknown alias."));
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
struct channel *ch = select_channel(chName);
|
||
if (nullptr == ch)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Illegal comsys alias, please delete."));
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
struct comuser *user = select_user(ch, executor);
|
||
if (nullptr == user)
|
||
{
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
UTF8 msg[256];
|
||
switch (key)
|
||
{
|
||
case COMTITLE_OFF:
|
||
if (0 == (ch->type & CHANNEL_SPOOF))
|
||
{
|
||
user->ComTitleStatus = false;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Comtitles are now off for channel %s"),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You can not turn off comtitles on that channel."));
|
||
}
|
||
break;
|
||
|
||
case COMTITLE_ON:
|
||
user->ComTitleStatus = true;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Comtitles are now on for channel %s"),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
break;
|
||
|
||
case COMTITLE_GAG:
|
||
user->bGagJoinLeave = true;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Join/leave messages are now gagged for channel %s"),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
break;
|
||
|
||
case COMTITLE_UNGAG:
|
||
user->bGagJoinLeave = false;
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Join/leave messages are now ungagged for channel %s"),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
break;
|
||
|
||
default:
|
||
// Set title.
|
||
//
|
||
if (nullptr != pTitle && '\0' != *pTitle)
|
||
{
|
||
user->title.assign(reinterpret_cast<const char *>(pTitle),
|
||
0, MAX_TITLE_LEN);
|
||
}
|
||
else
|
||
{
|
||
user->title.clear();
|
||
}
|
||
sqlite_wt_channel_user(ch->name, *user);
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Title set to ‘%s’ on channel %s."),
|
||
user->title.c_str(),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
break;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @clist — list channels.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @clist/full — same schema as engine do_listchannels (#1667 Phase 4 A1).
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::ChanListFull(dbref executor, const UTF8 *pPattern)
|
||
{
|
||
bool bCommAll = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
}
|
||
|
||
if (!bCommAll)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Warning: Only public channels and your channels will be shown."));
|
||
}
|
||
|
||
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[256];
|
||
size_t pos = 0;
|
||
pos = append_bytes(header, sizeof(header), pos, "*** ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Channel"), kFullNameCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Header"), kFullHeaderCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Owner"), kFullOwnerCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Access"), kFullAccessCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_bytes(header, sizeof(header), pos,
|
||
reinterpret_cast<const char *>(M_("Users")));
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_bytes(header, sizeof(header), pos,
|
||
reinterpret_cast<const char *>(M_("Msgs")));
|
||
m_pINotify->RawNotify(executor, header);
|
||
}
|
||
|
||
const bool bWild = (nullptr != pPattern && '\0' != *pPattern);
|
||
|
||
for (auto &kv : m_channels)
|
||
{
|
||
struct channel *ch = kv.second.get();
|
||
|
||
if (bWild && nullptr == strstr(
|
||
reinterpret_cast<const char *>(ch->name),
|
||
reinterpret_cast<const char *>(pPattern)))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!bCommAll
|
||
&& 0 == (ch->type & CHANNEL_PUBLIC))
|
||
{
|
||
bool bControls = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->HasControl(executor, ch->charge_who,
|
||
&bControls);
|
||
}
|
||
if ( !bControls
|
||
&& nullptr == select_user(ch, executor))
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
|
||
const UTF8 *pOwnerName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetMoniker(ch->charge_who, &pOwnerName);
|
||
}
|
||
if (nullptr == pOwnerName)
|
||
{
|
||
pOwnerName = T("???");
|
||
}
|
||
|
||
// The engine shows "-" for an unset header here, unlike the default
|
||
// listing which shows it empty.
|
||
//
|
||
const UTF8 *pHeader = ch->header;
|
||
if ('\0' == pHeader[0])
|
||
{
|
||
pHeader = T("-");
|
||
}
|
||
|
||
UTF8 line[MOD_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 = append_ljust_field(line, sizeof(line), pos, ch->name,
|
||
kFullNameCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, pHeader,
|
||
kFullHeaderCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, pOwnerName,
|
||
kFullOwnerCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
|
||
UTF8 access[4];
|
||
access[0] = test_join_access(executor, ch) ? 'J' : '-';
|
||
access[1] = test_transmit_access(executor, ch) ? 'X' : '-';
|
||
access[2] = test_receive_access(executor, ch) ? 'R' : '-';
|
||
access[3] = '\0';
|
||
// Access is 6 cols (JXR + pad); counts follow with no extra separator.
|
||
//
|
||
pos = append_ljust_field(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);
|
||
}
|
||
m_pINotify->RawNotify(executor, line);
|
||
}
|
||
|
||
m_pINotify->RawNotify(executor, M_("-- End of list of Channels --"));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::ChanList(dbref executor, const UTF8 *pPattern,
|
||
int key)
|
||
{
|
||
if (nullptr == m_pINotify)
|
||
{
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
// @clist/full was silently answered with the DEFAULT listing (#1640):
|
||
// Header, Access, Users and Msgs -- the four columns that are the entire
|
||
// reason /full exists -- were dropped. /headers was implemented and both
|
||
// implementations agreed on it, which is why this looked like a
|
||
// formatting difference rather than an unhandled switch.
|
||
//
|
||
if (key & CLIST_FULL)
|
||
{
|
||
return ChanListFull(executor, pPattern);
|
||
}
|
||
|
||
// @clist / @clist/headers schema — same as engine do_chanlist
|
||
// (#1667 Phase 4 A2): "*** "/PLS prefix, name 13, owner 15, third 45,
|
||
// pad to 79. Labels are separate msgids via mux_table_append_ljust.
|
||
//
|
||
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[256];
|
||
size_t pos = 0;
|
||
pos = append_bytes(header, sizeof(header), pos, "*** ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Channel"), kClistNameCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(header, sizeof(header), pos,
|
||
M_("Owner"), kClistOwnerCols);
|
||
pos = append_bytes(header, sizeof(header), pos, " ");
|
||
pos = append_ljust_field(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';
|
||
}
|
||
m_pINotify->RawNotify(executor, header);
|
||
}
|
||
|
||
bool bWild = (nullptr != pPattern && '\0' != *pPattern);
|
||
|
||
for (auto &kv : m_channels)
|
||
{
|
||
struct channel *ch = kv.second.get();
|
||
|
||
// Simple wildcard filter.
|
||
//
|
||
if (bWild && nullptr == strstr(
|
||
reinterpret_cast<const char *>(ch->name),
|
||
reinterpret_cast<const char *>(pPattern)))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Visibility check: public or owner or Comm_All.
|
||
//
|
||
bool bVisible = ((ch->type & CHANNEL_PUBLIC) != 0);
|
||
if (!bVisible && nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
bVisible = bCommAll;
|
||
}
|
||
if (!bVisible)
|
||
{
|
||
bool bControls = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->HasControl(executor, ch->charge_who,
|
||
&bControls);
|
||
}
|
||
bVisible = bControls;
|
||
}
|
||
|
||
if (!bVisible)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const UTF8 *pOwnerName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetMoniker(ch->charge_who, &pOwnerName);
|
||
}
|
||
if (nullptr == pOwnerName)
|
||
{
|
||
pOwnerName = T("???");
|
||
}
|
||
|
||
const UTF8 *pDesc = (key & CLIST_HEADERS)
|
||
? ch->header
|
||
: M_("No description.");
|
||
|
||
UTF8 line[MOD_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 = append_ljust_field(line, sizeof(line), pos, ch->name,
|
||
kClistNameCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, pOwnerName,
|
||
kClistOwnerCols);
|
||
pos = append_bytes(line, sizeof(line), pos, " ");
|
||
pos = append_ljust_field(line, sizeof(line), pos, pDesc,
|
||
kClistThirdCols);
|
||
while (pos < kClistLineCols && pos + 1 < sizeof(line))
|
||
{
|
||
line[pos++] = ' ';
|
||
}
|
||
if (pos < sizeof(line))
|
||
{
|
||
line[pos] = '\0';
|
||
}
|
||
|
||
m_pINotify->RawNotify(executor, line);
|
||
}
|
||
|
||
m_pINotify->RawNotify(executor,
|
||
M_("-- End of list of Channels --"));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @cwho — who is on a channel.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::ChanWho(dbref executor, const UTF8 *pArg)
|
||
{
|
||
if (nullptr == m_pINotify)
|
||
{
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
if (nullptr == pArg || '\0' == *pArg)
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("You must specify a channel."));
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Parse "channel/all" format.
|
||
//
|
||
UTF8 chanName[MAX_CHANNEL_LEN + 1];
|
||
size_t i = 0;
|
||
while ('\0' != pArg[i] && '/' != pArg[i] && i < MAX_CHANNEL_LEN)
|
||
{
|
||
chanName[i] = pArg[i];
|
||
i++;
|
||
}
|
||
chanName[i] = '\0';
|
||
|
||
bool bAll = ('/' == pArg[i] && 'a' == pArg[i + 1]);
|
||
|
||
struct channel *ch = select_channel(chanName);
|
||
if (nullptr == ch)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Unknown channel %s."),
|
||
reinterpret_cast<const char *>(chanName));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
// Permission check.
|
||
//
|
||
bool bAllowed = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->HasCommAll(executor, &bAllowed);
|
||
if (!bAllowed)
|
||
{
|
||
m_pIPermissions->HasControl(executor, ch->charge_who, &bAllowed);
|
||
}
|
||
}
|
||
if (!bAllowed)
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("Permission denied."));
|
||
return MUX_E_PERMISSION;
|
||
}
|
||
|
||
// @cwho schema (#1667 Phase 4 A4) — same as engine do_channelwho.
|
||
//
|
||
static const size_t kCwhoNameCols = 29;
|
||
static const size_t kCwhoStatusCols = 6;
|
||
static const size_t kCwhoPlayerCols = 6;
|
||
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
|
||
{
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Name"),
|
||
kCwhoNameCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Status"),
|
||
kCwhoStatusCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
pos = append_ljust_field(msg, sizeof(msg), pos, M_("Player"),
|
||
kCwhoPlayerCols);
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
for (auto &kv : ch->users)
|
||
{
|
||
comuser &user = kv.second;
|
||
|
||
// If not showing all, only show connected users.
|
||
//
|
||
if (!bAll)
|
||
{
|
||
bool bConnected = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsConnected(user.who, &bConnected);
|
||
}
|
||
if (!bConnected)
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// UnparseObject for name+dbref+flags (#1640). strip_color so
|
||
// width arithmetic stays honest on colored monikers.
|
||
//
|
||
UTF8 unparsed[MOD_LBUF_SIZE];
|
||
unparsed[0] = '\0';
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->UnparseObject(executor, user.who, false,
|
||
unparsed, sizeof(unparsed));
|
||
}
|
||
if ('\0' == unparsed[0])
|
||
{
|
||
const UTF8 *pName = nullptr;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->GetName(user.who, &pName);
|
||
}
|
||
mux_sprintf(unparsed, sizeof(unparsed),
|
||
T("%s"), reinterpret_cast<const char *>(
|
||
(nullptr != pName) ? pName : T("???")));
|
||
}
|
||
|
||
bool bPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(user.who, &bPlayer);
|
||
}
|
||
|
||
size_t pos = 0;
|
||
pos = append_ljust_field(msg, sizeof(msg), pos,
|
||
strip_color(unparsed), kCwhoNameCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
pos = append_ljust_field(msg, sizeof(msg), pos,
|
||
user.bUserIsOn ? T("on ") : T("off"), kCwhoStatusCols);
|
||
pos = append_bytes(msg, sizeof(msg), pos, " ");
|
||
pos = append_ljust_field(msg, sizeof(msg), pos,
|
||
bPlayer ? T("yes") : T("no "), kCwhoPlayerCols);
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
T("-- %s --"), reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// @cemit — emit to a channel.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::CEmit(dbref executor, const UTF8 *pChannel,
|
||
const UTF8 *pText, int key)
|
||
{
|
||
if (nullptr == pChannel || nullptr == pText)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
struct channel *ch = select_channel(pChannel);
|
||
if (nullptr == ch)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel %s does not exist."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
// Permission check.
|
||
//
|
||
bool bAllowed = false;
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
m_pIPermissions->HasCommAll(executor, &bAllowed);
|
||
if (!bAllowed)
|
||
{
|
||
m_pIPermissions->HasControl(executor, ch->charge_who, &bAllowed);
|
||
}
|
||
}
|
||
if (!bAllowed)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("Permission denied."));
|
||
}
|
||
return MUX_E_PERMISSION;
|
||
}
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
if (key == CEMIT_NOHEADER)
|
||
{
|
||
strncpy(reinterpret_cast<char *>(msg),
|
||
reinterpret_cast<const char *>(pText), sizeof(msg) - 1);
|
||
msg[sizeof(msg) - 1] = '\0';
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msg, sizeof(msg),
|
||
T("%s %s"),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
reinterpret_cast<const char *>(pText));
|
||
}
|
||
|
||
// No speaker prefix here: @cemit is the channel talking, not a player,
|
||
// so there is no comtitle variant to choose between.
|
||
//
|
||
SendChannelMessage(executor, ch, msg, nullptr, false);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::CSet(dbref executor, const UTF8 *pChannel,
|
||
const UTF8 *pValue, int key)
|
||
{
|
||
if (CSET_LIST == key)
|
||
{
|
||
// Redirect to ChanList with CLIST_FULL.
|
||
//
|
||
return ChanList(executor, nullptr, CLIST_FULL);
|
||
}
|
||
|
||
struct channel *ch = select_channel(pChannel);
|
||
if (nullptr == ch)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("@cset: Channel %s does not exist."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
m_pINotify->Notify(executor, msg);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Permission check: must control channel owner or have Comm_All.
|
||
//
|
||
bool bControls = false;
|
||
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
if (!bControls && !bCommAll)
|
||
{
|
||
m_pINotify->Notify(executor, M_("Permission denied."));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
const UTF8 *msg = nullptr;
|
||
UTF8 msgbuf[MOD_LBUF_SIZE];
|
||
|
||
switch (key)
|
||
{
|
||
case CSET_PUBLIC:
|
||
ch->type |= CHANNEL_PUBLIC;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s placed on the public listings."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_PRIVATE:
|
||
ch->type &= ~CHANNEL_PUBLIC;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s taken off the public listings."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_LOUD:
|
||
ch->type |= CHANNEL_LOUD;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s now sends connect/disconnect msgs."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_QUIET:
|
||
ch->type &= ~CHANNEL_LOUD;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s connect/disconnect msgs muted."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_SPOOF:
|
||
ch->type |= CHANNEL_SPOOF;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s set spoofable."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_NOSPOOF:
|
||
ch->type &= ~CHANNEL_SPOOF;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s set unspoofable."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
|
||
case CSET_OBJECT:
|
||
{
|
||
dbref thing = NOTHING;
|
||
if (nullptr != pValue && '\0' != pValue[0])
|
||
{
|
||
m_pIObjectInfo->MatchThing(executor, pValue, &thing);
|
||
}
|
||
bool bValid = false;
|
||
if (NOTHING != thing)
|
||
{
|
||
m_pIObjectInfo->IsValid(thing, &bValid);
|
||
}
|
||
if (NOTHING == thing)
|
||
{
|
||
ch->chan_obj = NOTHING;
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("Channel %s is now disassociated from any channel object."),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
}
|
||
else if (bValid)
|
||
{
|
||
ch->chan_obj = thing;
|
||
const UTF8 *pName = nullptr;
|
||
m_pIObjectInfo->GetName(thing, &pName);
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("Channel %s is now using %s(#%d) as channel object."),
|
||
reinterpret_cast<const char *>(ch->name),
|
||
pName ? reinterpret_cast<const char *>(pName) : "???",
|
||
thing);
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("%d is not a valid channel object."), thing);
|
||
}
|
||
msg = msgbuf;
|
||
}
|
||
break;
|
||
|
||
case CSET_HEADER:
|
||
{
|
||
if (nullptr == pValue || '\0' == pValue[0])
|
||
{
|
||
// Reset to default bold [ChannelName] header.
|
||
// COLOR_INTENSE = \xEF\x94\x81, COLOR_RESET = \xEF\x94\x80
|
||
//
|
||
mux_sprintf(ch->header, sizeof(ch->header),
|
||
T("\xEF\x94\x81[%s]\xEF\x94\x80"),
|
||
reinterpret_cast<const char *>(ch->name));
|
||
}
|
||
else
|
||
{
|
||
// Optimize/terminate the ANSI, as do_cheader does. The raw
|
||
// strncpy that was here stored the header uncollapsed, so
|
||
// `@cset/header c=[ansi(r,RED)][ansi(b,BLU)]` persisted an
|
||
// extra reset between the two codes under the module and not
|
||
// under the engine -- a divergence in SQLite, not merely on
|
||
// screen, so it survived a handoff. It also truncated at
|
||
// MAX_HEADER_LEN *bytes*, which can split a codepoint or a
|
||
// color code in half (#1640, #1649).
|
||
//
|
||
UTF8 hdr[MAX_HEADER_LEN + 1];
|
||
const mux_field nLen = StripTabsAndTruncate(pValue, hdr,
|
||
MAX_HEADER_LEN, MAX_HEADER_LEN);
|
||
memcpy(ch->header, hdr, nLen.m_byte + 1);
|
||
}
|
||
msg = reinterpret_cast<const UTF8 *>("Set.");
|
||
}
|
||
break;
|
||
|
||
case CSET_LOG:
|
||
{
|
||
if (nullptr == pValue || '\0' == pValue[0])
|
||
{
|
||
msg = reinterpret_cast<const UTF8 *>(
|
||
"@cset: Maximum history must be a number less than "
|
||
"or equal to 200.");
|
||
break;
|
||
}
|
||
int64_t value = mux_atoi64(pValue);
|
||
if (value < 0 || value > 200)
|
||
{
|
||
msg = reinterpret_cast<const UTF8 *>(
|
||
"@cset: Maximum history must be a number less than "
|
||
"or equal to 200.");
|
||
break;
|
||
}
|
||
|
||
// Set when shrinking the ring leaves entries we could not clear
|
||
// (#1620); reported with the result rather than swallowed.
|
||
//
|
||
bool bStale = false;
|
||
|
||
bool bObjValid = false;
|
||
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bObjValid);
|
||
}
|
||
if (!bObjValid)
|
||
{
|
||
msg = reinterpret_cast<const UTF8 *>(
|
||
"@cset: Channel has no object. Use @cset/object first.");
|
||
break;
|
||
}
|
||
|
||
// Read old MAX_LOG value.
|
||
//
|
||
UTF8 oldbuf[64];
|
||
size_t nOldLen = 0;
|
||
int64_t oldnum = 0;
|
||
MUX_RESULT mr2 = m_pIAttributeAccess->GetAttribute(
|
||
executor, ch->chan_obj, T("MAX_LOG"),
|
||
oldbuf, sizeof(oldbuf) - 1, &nOldLen);
|
||
if (MUX_SUCCEEDED(mr2) && 0 < nOldLen)
|
||
{
|
||
oldbuf[nOldLen] = '\0';
|
||
oldnum = mux_atoi64(oldbuf);
|
||
}
|
||
|
||
// If reducing, clear old HISTORY_N attributes.
|
||
//
|
||
if (value < oldnum)
|
||
{
|
||
for (int64_t count = 0; count <= oldnum; count++)
|
||
{
|
||
UTF8 histattr[64];
|
||
mux_sprintf(histattr, sizeof(histattr), T("HISTORY_%lld"),
|
||
static_cast<long long>(count));
|
||
if (nullptr == m_pIStorage
|
||
|| MUX_FAILED(m_pIStorage->SetChannelAttr(ch->name,
|
||
histattr, T(""))))
|
||
{
|
||
// Engine-written slots used to refuse here (#1620),
|
||
// leaving stale history above the new maximum. Going
|
||
// through the engine removes that cause; anything
|
||
// still failing is a real storage failure and is
|
||
// still reported rather than swallowed.
|
||
//
|
||
bStale = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set MAX_LOG.
|
||
//
|
||
UTF8 vbuf[32];
|
||
mux_sprintf(vbuf, sizeof(vbuf),
|
||
T("%d"), value);
|
||
if (nullptr == m_pIStorage
|
||
|| MUX_FAILED(m_pIStorage->SetChannelAttr(ch->name,
|
||
T("MAX_LOG"), vbuf)))
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Failed. Could not set the maximum for %s (the attribute is not writable here)."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
}
|
||
else if (bStale)
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s maximum history set, but older entries could not be cleared."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s maximum history set."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
}
|
||
msg = msgbuf;
|
||
}
|
||
break;
|
||
|
||
case CSET_LOG_TIME:
|
||
{
|
||
if (nullptr == pValue || '\0' == pValue[0])
|
||
{
|
||
msg = reinterpret_cast<const UTF8 *>("@cset: Failed.");
|
||
break;
|
||
}
|
||
int64_t value = mux_atoi64(pValue);
|
||
if (value != 0 && value != 1)
|
||
{
|
||
msg = reinterpret_cast<const UTF8 *>("@cset: Failed.");
|
||
break;
|
||
}
|
||
|
||
bool bObjValid = false;
|
||
if (NOTHING != ch->chan_obj && nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(ch->chan_obj, &bObjValid);
|
||
}
|
||
if (!bObjValid)
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Failed. Is logging enabled for %s?"),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
}
|
||
|
||
// Verify logging is enabled (MAX_LOG exists).
|
||
//
|
||
UTF8 logbuf[64];
|
||
size_t nLogLen = 0;
|
||
MUX_RESULT mr2 = m_pIAttributeAccess->GetAttribute(
|
||
executor, ch->chan_obj, T("MAX_LOG"),
|
||
logbuf, sizeof(logbuf) - 1, &nLogLen);
|
||
if (MUX_FAILED(mr2) || 0 == nLogLen)
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Failed. Is logging enabled for %s?"),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
msg = msgbuf;
|
||
break;
|
||
}
|
||
|
||
// Set or clear LOG_TIMESTAMPS.
|
||
//
|
||
// #1585: this write is refused when the engine set the attribute
|
||
// (GOD + AF_CONST, which bCanSetAttr denies in every branch).
|
||
// Reporting "set." after a refusal told the player the opposite
|
||
// of what happened, and is how the divergence stayed hidden.
|
||
//
|
||
MUX_RESULT mrTs = (nullptr != m_pIStorage)
|
||
? m_pIStorage->SetChannelAttr(ch->name, T("LOG_TIMESTAMPS"),
|
||
value ? T("1") : T(""))
|
||
: MUX_E_FAIL;
|
||
if (MUX_FAILED(mrTs))
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Failed. Timestamp logging for %s could not be changed (the attribute is not writable here)."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
}
|
||
else
|
||
{
|
||
mux_sprintf(msgbuf, sizeof(msgbuf),
|
||
M_("@cset: Channel %s timestamp logging set."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
}
|
||
msg = msgbuf;
|
||
}
|
||
break;
|
||
}
|
||
|
||
sqlite_wt_channel(ch);
|
||
if (nullptr != msg)
|
||
{
|
||
m_pINotify->Notify(executor, msg);
|
||
}
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::EditChannel(dbref executor, const UTF8 *pChannel,
|
||
const UTF8 *pValue, int flag)
|
||
{
|
||
struct channel *ch = select_channel(pChannel);
|
||
if (nullptr == ch)
|
||
{
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Unknown channel %s."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
m_pINotify->Notify(executor, msg);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Permission check.
|
||
//
|
||
bool bControls = false;
|
||
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
if (!bControls && !bCommAll)
|
||
{
|
||
m_pINotify->Notify(executor, M_("Permission denied."));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
bool add_remove = true;
|
||
const UTF8 *s = pValue;
|
||
if (nullptr != s && '!' == *s)
|
||
{
|
||
add_remove = false;
|
||
s++;
|
||
}
|
||
|
||
switch (flag)
|
||
{
|
||
case EDIT_CHANNEL_CCHOWN:
|
||
{
|
||
dbref who = NOTHING;
|
||
if (nullptr != pValue)
|
||
{
|
||
m_pIObjectInfo->MatchThing(executor, pValue, &who);
|
||
}
|
||
bool bValid = false;
|
||
if (NOTHING != who)
|
||
{
|
||
m_pIObjectInfo->IsValid(who, &bValid);
|
||
}
|
||
if (bValid)
|
||
{
|
||
ch->charge_who = who;
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("Set."));
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("Invalid player."));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EDIT_CHANNEL_CCHARGE:
|
||
{
|
||
int64_t c_charge = 0;
|
||
if (nullptr != pValue)
|
||
{
|
||
c_charge = mux_atoi64(pValue);
|
||
}
|
||
if (0 <= c_charge && c_charge <= MAX_COST)
|
||
{
|
||
ch->charge = static_cast<int>(c_charge);
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("Set."));
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>(
|
||
"That is not a reasonable cost."));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EDIT_CHANNEL_CPFLAGS:
|
||
{
|
||
int access = 0;
|
||
if (nullptr != s)
|
||
{
|
||
if (strcmp(reinterpret_cast<const char *>(s), "join") == 0)
|
||
{
|
||
access = CHANNEL_PLAYER_JOIN;
|
||
}
|
||
else if (strcmp(reinterpret_cast<const char *>(s),
|
||
"receive") == 0)
|
||
{
|
||
access = CHANNEL_PLAYER_RECEIVE;
|
||
}
|
||
else if (strcmp(reinterpret_cast<const char *>(s),
|
||
"transmit") == 0)
|
||
{
|
||
access = CHANNEL_PLAYER_TRANSMIT;
|
||
}
|
||
}
|
||
|
||
if (access)
|
||
{
|
||
if (add_remove)
|
||
{
|
||
ch->type |= access;
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@cpflags: Set."));
|
||
}
|
||
else
|
||
{
|
||
ch->type &= ~access;
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@cpflags: Cleared."));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@cpflags: Unknown Flag."));
|
||
}
|
||
}
|
||
break;
|
||
|
||
case EDIT_CHANNEL_COFLAGS:
|
||
{
|
||
int access = 0;
|
||
if (nullptr != s)
|
||
{
|
||
if (strcmp(reinterpret_cast<const char *>(s), "join") == 0)
|
||
{
|
||
access = CHANNEL_OBJECT_JOIN;
|
||
}
|
||
else if (strcmp(reinterpret_cast<const char *>(s),
|
||
"receive") == 0)
|
||
{
|
||
access = CHANNEL_OBJECT_RECEIVE;
|
||
}
|
||
else if (strcmp(reinterpret_cast<const char *>(s),
|
||
"transmit") == 0)
|
||
{
|
||
access = CHANNEL_OBJECT_TRANSMIT;
|
||
}
|
||
}
|
||
|
||
if (access)
|
||
{
|
||
if (add_remove)
|
||
{
|
||
ch->type |= access;
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@coflags: Set."));
|
||
}
|
||
else
|
||
{
|
||
ch->type &= ~access;
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@coflags: Cleared."));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@coflags: Unknown Flag."));
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
sqlite_wt_channel(ch);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::CBoot(dbref executor, const UTF8 *pChannel,
|
||
const UTF8 *pVictim, int key)
|
||
{
|
||
struct channel *ch = select_channel(pChannel);
|
||
if (nullptr == ch)
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>("@cboot: Unknown channel."));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
struct comuser *user = select_user(ch, executor);
|
||
if (nullptr == user)
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>(
|
||
"@cboot: You are not on that channel."));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Permission check.
|
||
//
|
||
bool bControls = false;
|
||
m_pIPermissions->HasControl(executor, ch->charge_who, &bControls);
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
if (!bControls && !bCommAll)
|
||
{
|
||
m_pINotify->Notify(executor,
|
||
reinterpret_cast<const UTF8 *>(
|
||
"@cboot: You can’t do that!"));
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Resolve victim name to dbref.
|
||
//
|
||
dbref thing = NOTHING;
|
||
if (nullptr != pVictim)
|
||
{
|
||
m_pIObjectInfo->MatchThing(executor, pVictim, &thing);
|
||
}
|
||
bool bValid = false;
|
||
if (NOTHING != thing)
|
||
{
|
||
m_pIObjectInfo->IsValid(thing, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
struct comuser *vu = select_user(ch, thing);
|
||
if (nullptr == vu)
|
||
{
|
||
const UTF8 *pMoniker = nullptr;
|
||
m_pIObjectInfo->GetMoniker(thing, &pMoniker);
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("@cboot: %s is not on the channel."),
|
||
pMoniker ? reinterpret_cast<const char *>(pMoniker)
|
||
: "???");
|
||
m_pINotify->Notify(executor, msg);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Notify executor and victim.
|
||
//
|
||
const UTF8 *pExecMoniker = nullptr;
|
||
m_pIObjectInfo->GetMoniker(executor, &pExecMoniker);
|
||
const UTF8 *pVictMoniker = nullptr;
|
||
m_pIObjectInfo->GetMoniker(thing, &pVictMoniker);
|
||
|
||
UTF8 msg[MOD_LBUF_SIZE];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("You boot %s off channel %s."),
|
||
pVictMoniker ? reinterpret_cast<const char *>(pVictMoniker)
|
||
: "???",
|
||
reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->Notify(executor, msg);
|
||
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s boots you off channel %s."),
|
||
pExecMoniker ? reinterpret_cast<const char *>(pExecMoniker)
|
||
: "???",
|
||
reinterpret_cast<const char *>(ch->name));
|
||
m_pINotify->Notify(thing, msg);
|
||
|
||
if (!(key & CBOOT_QUIET))
|
||
{
|
||
// Broadcast boot message to channel.
|
||
//
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("%s %s boots %s off the channel."),
|
||
reinterpret_cast<const char *>(ch->header),
|
||
pExecMoniker ? reinterpret_cast<const char *>(pExecMoniker)
|
||
: "???",
|
||
pVictMoniker ? reinterpret_cast<const char *>(pVictMoniker)
|
||
: "???");
|
||
SendChannelMessage(executor, ch, msg, nullptr, false);
|
||
}
|
||
|
||
// Remove victim from channel.
|
||
//
|
||
do_delcomchannel(thing, ch->name, (key & CBOOT_QUIET) != 0);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::ProcessCommand(dbref executor, const UTF8 *pCmd,
|
||
bool *pbHandled)
|
||
{
|
||
*pbHandled = false;
|
||
|
||
// Parse "alias message" pattern.
|
||
//
|
||
const char *t = strchr(reinterpret_cast<const char *>(pCmd), ' ');
|
||
if ( nullptr == t
|
||
|| t - reinterpret_cast<const char *>(pCmd) > MAX_ALIAS_LEN
|
||
|| t[1] == '\0')
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Extract alias.
|
||
//
|
||
UTF8 alias[MAX_ALIAS_LEN + 1];
|
||
size_t nAlias = t - reinterpret_cast<const char *>(pCmd);
|
||
memcpy(alias, pCmd, nAlias);
|
||
alias[nAlias] = '\0';
|
||
|
||
// Look up channel from alias.
|
||
//
|
||
const UTF8 *ch = get_channel_from_alias(executor, alias);
|
||
if (ch[0] == '\0')
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// Found a valid alias — dispatch to the channel message processor.
|
||
//
|
||
UTF8 *pMsg = const_cast<UTF8 *>(reinterpret_cast<const UTF8 *>(t + 1));
|
||
do_processcom(executor, ch, pMsg);
|
||
*pbHandled = true;
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::GetRevision(int *pRev)
|
||
{
|
||
if (nullptr == pRev)
|
||
{
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
*pRev = m_revision;
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Alias management.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::AddAlias(dbref executor, const UTF8 *pAlias,
|
||
const UTF8 *pChannel)
|
||
{
|
||
if (nullptr == pAlias || '\0' == pAlias[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You need to specify a valid alias."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
if (nullptr == pChannel || '\0' == pChannel[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You need to specify a channel."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Validate alias length.
|
||
//
|
||
size_t nAlias = strlen(reinterpret_cast<const char *>(pAlias));
|
||
if (nAlias > MAX_ALIAS_LEN)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You need to specify a valid alias."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
struct channel *ch = select_channel(pChannel);
|
||
if (nullptr == ch)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel %s does not exist yet."),
|
||
reinterpret_cast<const char *>(pChannel));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
if (!test_join_access(executor, ch))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Sorry, this channel type does not allow you to join."));
|
||
}
|
||
return MUX_E_PERMISSION;
|
||
}
|
||
|
||
comsys_t &c = get_comsys(executor);
|
||
|
||
if (static_cast<int>(c.aliases.size()) >= MAX_ALIASES_PER_PLAYER)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Sorry, but you have reached the maximum number of "
|
||
"aliases allowed."));
|
||
}
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
// Check for duplicate alias.
|
||
//
|
||
for (const auto &ca : c.aliases)
|
||
{
|
||
if (ca.alias == reinterpret_cast<const char *>(pAlias))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("That alias is already in use for channel %s."),
|
||
ca.channel.c_str());
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
return MUX_E_FAIL;
|
||
}
|
||
}
|
||
|
||
// Append and sort.
|
||
//
|
||
com_alias ca;
|
||
ca.alias = reinterpret_cast<const char *>(pAlias);
|
||
ca.channel = reinterpret_cast<const char *>(ch->name);
|
||
c.aliases.push_back(std::move(ca));
|
||
|
||
sort_com_aliases(c);
|
||
sqlite_wt_player_channel(executor, pAlias, ch->name);
|
||
|
||
// Join channel if not already a user.
|
||
//
|
||
if (nullptr == select_user(ch, executor))
|
||
{
|
||
do_joinchannel(executor, ch);
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel %s added with alias %s."),
|
||
reinterpret_cast<const char *>(ch->name),
|
||
reinterpret_cast<const char *>(pAlias));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::DelAlias(dbref executor, const UTF8 *pAlias)
|
||
{
|
||
if (nullptr == pAlias || '\0' == pAlias[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("Need an alias to delete."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
comsys_t &c = get_comsys(executor);
|
||
|
||
for (auto it = c.aliases.begin(); it != c.aliases.end(); ++it)
|
||
{
|
||
if (it->alias != reinterpret_cast<const char *>(pAlias))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// Count other aliases for same channel.
|
||
//
|
||
int found = 0;
|
||
for (const auto &ca : c.aliases)
|
||
{
|
||
if (ca.channel == it->channel)
|
||
{
|
||
found++;
|
||
}
|
||
}
|
||
|
||
// If last alias for this channel, remove from channel.
|
||
//
|
||
if (found <= 1)
|
||
{
|
||
do_delcomchannel(executor,
|
||
reinterpret_cast<const UTF8 *>(it->channel.c_str()),
|
||
false);
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Alias %s for channel %s deleted."),
|
||
reinterpret_cast<const char *>(pAlias),
|
||
it->channel.c_str());
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
sqlite_wt_delete_player_channel(executor, pAlias);
|
||
c.aliases.erase(it);
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor, M_("Unable to find that alias."));
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::ClearAliases(dbref executor)
|
||
{
|
||
auto it = m_comsys.find(executor);
|
||
if (it == m_comsys.end())
|
||
{
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
comsys_t &c = it->second;
|
||
|
||
// Walk backwards to safely remove each alias.
|
||
//
|
||
while (!c.aliases.empty())
|
||
{
|
||
std::string alias = c.aliases.back().alias;
|
||
DelAlias(executor, reinterpret_cast<const UTF8 *>(alias.c_str()));
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Channel creation/destruction.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
MUX_RESULT CComsysMod::CreateChannel(dbref executor, const UTF8 *pName)
|
||
{
|
||
if (nullptr == pName || '\0' == pName[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You must specify a channel to create."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Check permission.
|
||
//
|
||
if (nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
if (!bCommAll)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You do not have permission to create channels."));
|
||
}
|
||
return MUX_E_PERMISSION;
|
||
}
|
||
}
|
||
|
||
// #1874: reject overlong names before any truncate/duplicate check.
|
||
// Truncating to MAX_CHANNEL_LEN after select_channel(full name) let
|
||
// "existing50byte" + "x" collide on the map key and replace the old
|
||
// channel without SQLite cleanup.
|
||
//
|
||
const size_t nName = strlen(reinterpret_cast<const char *>(pName));
|
||
if (nName > MAX_CHANNEL_LEN)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[128];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel names must be at most %d characters."),
|
||
MAX_CHANNEL_LEN);
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
// Check for existing channel (full name — same length as stored key).
|
||
//
|
||
if (nullptr != select_channel(pName))
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("That channel already exists."));
|
||
}
|
||
return MUX_E_FAIL;
|
||
}
|
||
|
||
// Create the channel.
|
||
//
|
||
auto ch = std::make_unique<channel>();
|
||
|
||
strncpy(reinterpret_cast<char *>(ch->name),
|
||
reinterpret_cast<const char *>(pName), MAX_CHANNEL_LEN);
|
||
ch->name[MAX_CHANNEL_LEN] = '\0';
|
||
|
||
mux_sprintf(ch->header, MAX_HEADER_LEN + 1,
|
||
T("[%s]"), reinterpret_cast<const char *>(ch->name));
|
||
|
||
ch->type = CHANNEL_DEFAULT;
|
||
ch->charge_who = executor;
|
||
ch->chan_obj = NOTHING;
|
||
|
||
channel *pCh = ch.get();
|
||
|
||
std::vector<UTF8> key(ch->name,
|
||
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
|
||
m_channels[key] = std::move(ch);
|
||
|
||
sqlite_wt_channel(pCh);
|
||
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel %s created."),
|
||
reinterpret_cast<const char *>(pCh->name));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
MUX_RESULT CComsysMod::DestroyChannel(dbref executor, const UTF8 *pName)
|
||
{
|
||
if (nullptr == pName || '\0' == pName[0])
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You must specify a channel to destroy."));
|
||
}
|
||
return MUX_E_INVALIDARG;
|
||
}
|
||
|
||
struct channel *ch = select_channel(pName);
|
||
if (nullptr == ch)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("That channel does not exist."));
|
||
}
|
||
return MUX_E_NOTFOUND;
|
||
}
|
||
|
||
// Check permission: must be owner or Comm_All.
|
||
//
|
||
bool bAllowed = (ch->charge_who == executor);
|
||
if (!bAllowed && nullptr != m_pIPermissions)
|
||
{
|
||
bool bCommAll = false;
|
||
m_pIPermissions->HasCommAll(executor, &bCommAll);
|
||
bAllowed = bCommAll;
|
||
}
|
||
|
||
if (!bAllowed)
|
||
{
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
m_pINotify->RawNotify(executor,
|
||
M_("You do not have permission to destroy that channel."));
|
||
}
|
||
return MUX_E_PERMISSION;
|
||
}
|
||
|
||
// Capture canonical name before the channel object goes away.
|
||
//
|
||
const std::string channelName(
|
||
reinterpret_cast<const char *>(ch->name));
|
||
|
||
// Remove from SQLite. ON DELETE CASCADE clears channel_users and
|
||
// player_channels rows for this channel name.
|
||
//
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
log_storage_failure(m_pIStorage->DeleteChannel(ch->name),
|
||
"DeleteChannel(channel=%s)",
|
||
reinterpret_cast<const char *>(ch->name));
|
||
bump_revision();
|
||
}
|
||
|
||
// #1199: purge other players' in-memory aliases that pointed here.
|
||
// (SQLite rows are already CASCADE-deleted; still drop local aliases.)
|
||
//
|
||
for (auto &kv : m_comsys)
|
||
{
|
||
comsys_t &c = kv.second;
|
||
for (auto ait = c.aliases.begin(); ait != c.aliases.end(); )
|
||
{
|
||
if (ait->channel == channelName)
|
||
{
|
||
sqlite_wt_delete_player_channel(c.who,
|
||
reinterpret_cast<const UTF8 *>(ait->alias.c_str()));
|
||
ait = c.aliases.erase(ait);
|
||
}
|
||
else
|
||
{
|
||
++ait;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Notify before erasing (ch will be destroyed by unique_ptr).
|
||
//
|
||
if (nullptr != m_pINotify)
|
||
{
|
||
UTF8 msg[256];
|
||
mux_sprintf(msg, sizeof(msg),
|
||
M_("Channel %s destroyed."),
|
||
reinterpret_cast<const char *>(pName));
|
||
m_pINotify->RawNotify(executor, msg);
|
||
}
|
||
|
||
// Remove from map — unique_ptr cleans up channel and all users.
|
||
//
|
||
std::vector<UTF8> key(ch->name,
|
||
ch->name + strlen(reinterpret_cast<const char *>(ch->name)) + 1);
|
||
m_channels.erase(key);
|
||
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// mux_IServerEventsSink implementation — server lifecycle events.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void CComsysMod::startup(void)
|
||
{
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module startup complete."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
void CComsysMod::presync_database(void)
|
||
{
|
||
// Nothing needed — SQLite write-through handles persistence.
|
||
}
|
||
|
||
void CComsysMod::presync_database_sigsegv(void)
|
||
{
|
||
// Nothing.
|
||
}
|
||
|
||
void CComsysMod::dump_database(int dump_type)
|
||
{
|
||
UNUSED_PARAMETER(dump_type);
|
||
// Nothing needed — SQLite WAL checkpoint handles durability.
|
||
}
|
||
|
||
void CComsysMod::dump_complete_signal(void)
|
||
{
|
||
// Nothing.
|
||
}
|
||
|
||
void CComsysMod::shutdown(void)
|
||
{
|
||
if (nullptr != m_pIStorage)
|
||
{
|
||
m_pIStorage->Release();
|
||
m_pIStorage = nullptr;
|
||
}
|
||
|
||
if (nullptr != m_pILog)
|
||
{
|
||
bool fStarted;
|
||
MUX_RESULT mr = m_pILog->start_log(&fStarted, LOG_ALWAYS,
|
||
T("INI"), T("INFO"));
|
||
if (MUX_SUCCEEDED(mr) && fStarted)
|
||
{
|
||
m_pILog->log_text(T("Comsys module shutting down."));
|
||
m_pILog->end_log();
|
||
}
|
||
}
|
||
}
|
||
|
||
void CComsysMod::dbck(void)
|
||
{
|
||
// Channel consistency checks (#1195):
|
||
// 1. Prune users with invalid dbrefs only — object listeners stay.
|
||
// 2. Reassign channel ownership if owner is gone.
|
||
// 3. Prune comsys alias rows for destroyed players.
|
||
//
|
||
for (auto &kv : m_channels)
|
||
{
|
||
struct channel *ch = kv.second.get();
|
||
|
||
for (auto it = ch->users.begin(); it != ch->users.end(); )
|
||
{
|
||
bool bValid = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsValid(it->second.who, &bValid);
|
||
}
|
||
if (!bValid)
|
||
{
|
||
it = ch->users.erase(it);
|
||
}
|
||
else
|
||
{
|
||
++it;
|
||
}
|
||
}
|
||
|
||
if (ch->charge_who != NOTHING && nullptr != m_pIObjectInfo)
|
||
{
|
||
bool bValid = false;
|
||
m_pIObjectInfo->IsValid(ch->charge_who, &bValid);
|
||
if (!bValid)
|
||
{
|
||
ch->charge_who = 1; // GOD
|
||
}
|
||
}
|
||
}
|
||
|
||
for (auto it = m_comsys.begin(); it != m_comsys.end(); )
|
||
{
|
||
bool bPlayer = false;
|
||
if (nullptr != m_pIObjectInfo)
|
||
{
|
||
m_pIObjectInfo->IsPlayer(it->first, &bPlayer);
|
||
}
|
||
if (!bPlayer)
|
||
{
|
||
it = m_comsys.erase(it);
|
||
}
|
||
else
|
||
{
|
||
++it;
|
||
}
|
||
}
|
||
}
|
||
|
||
void CComsysMod::connect(dbref player, int isnew, int num)
|
||
{
|
||
UNUSED_PARAMETER(isnew);
|
||
UNUSED_PARAMETER(num);
|
||
PlayerConnect(player);
|
||
}
|
||
|
||
void CComsysMod::disconnect(dbref player, int num)
|
||
{
|
||
UNUSED_PARAMETER(num);
|
||
PlayerDisconnect(player);
|
||
}
|
||
|
||
void CComsysMod::data_create(dbref object)
|
||
{
|
||
UNUSED_PARAMETER(object);
|
||
}
|
||
|
||
void CComsysMod::data_clone(dbref clone, dbref source)
|
||
{
|
||
UNUSED_PARAMETER(clone);
|
||
UNUSED_PARAMETER(source);
|
||
}
|
||
|
||
void CComsysMod::data_free(dbref object)
|
||
{
|
||
PlayerNuke(object);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// CComsysModFactory — boilerplate.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
CComsysModFactory::CComsysModFactory(void) : m_cRef(1)
|
||
{
|
||
}
|
||
|
||
CComsysModFactory::~CComsysModFactory()
|
||
{
|
||
}
|
||
|
||
MUX_RESULT CComsysModFactory::QueryInterface(MUX_IID iid, void **ppv)
|
||
{
|
||
if (mux_IID_IUnknown == iid)
|
||
{
|
||
*ppv = static_cast<mux_IClassFactory *>(this);
|
||
}
|
||
else if (mux_IID_IClassFactory == iid)
|
||
{
|
||
*ppv = static_cast<mux_IClassFactory *>(this);
|
||
}
|
||
else
|
||
{
|
||
*ppv = nullptr;
|
||
return MUX_E_NOINTERFACE;
|
||
}
|
||
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
|
||
return MUX_S_OK;
|
||
}
|
||
|
||
uint32_t CComsysModFactory::AddRef(void)
|
||
{
|
||
return m_cRef.fetch_add(1, std::memory_order_relaxed) + 1;
|
||
}
|
||
|
||
uint32_t CComsysModFactory::Release(void)
|
||
{
|
||
uint32_t prev = m_cRef.fetch_sub(1, std::memory_order_acq_rel);
|
||
if (1 == prev)
|
||
{
|
||
delete this;
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
}
|
||
|
||
MUX_RESULT CComsysModFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
|
||
MUX_IID iid, void **ppv)
|
||
{
|
||
UNUSED_PARAMETER(pUnknownOuter);
|
||
|
||
CComsysMod *pComsysMod = nullptr;
|
||
try
|
||
{
|
||
pComsysMod = new CComsysMod;
|
||
}
|
||
catch (...)
|
||
{
|
||
; // Nothing.
|
||
}
|
||
|
||
if (nullptr == pComsysMod)
|
||
{
|
||
return MUX_E_OUTOFMEMORY;
|
||
}
|
||
|
||
MUX_RESULT mr = pComsysMod->FinalConstruct();
|
||
if (MUX_FAILED(mr))
|
||
{
|
||
pComsysMod->Release();
|
||
return mr;
|
||
}
|
||
|
||
mr = pComsysMod->QueryInterface(iid, ppv);
|
||
pComsysMod->Release();
|
||
return mr;
|
||
}
|
||
|
||
MUX_RESULT CComsysModFactory::LockServer(bool bLock)
|
||
{
|
||
if (bLock)
|
||
{
|
||
g_cServerLocks++;
|
||
}
|
||
else
|
||
{
|
||
g_cServerLocks--;
|
||
}
|
||
return MUX_S_OK;
|
||
}
|