/*! \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 #include #include // 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 g_cComponents{0}; static std::atomic 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(&m_pILog)); if (MUX_FAILED(mr)) { return mr; } // Register for server events. // mux_IServerEventsSink *pIServerEventsSink = nullptr; mr = QueryInterface(IID_IServerEventsSink, reinterpret_cast(&pIServerEventsSink)); if (MUX_SUCCEEDED(mr)) { mr = mux_CreateInstance(CID_ServerEventsSource, nullptr, UseSameProcess, IID_IServerEventsControl, reinterpret_cast(&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(&m_pINotify)); if (MUX_FAILED(mr)) { return mr; } mr = mux_CreateInstance(CID_ObjectInfo, nullptr, UseSameProcess, IID_IObjectInfo, reinterpret_cast(&m_pIObjectInfo)); if (MUX_FAILED(mr)) { return mr; } mr = mux_CreateInstance(CID_AttributeAccess, nullptr, UseSameProcess, IID_IAttributeAccess, reinterpret_cast(&m_pIAttributeAccess)); if (MUX_FAILED(mr)) { return mr; } mr = mux_CreateInstance(CID_Evaluator, nullptr, UseSameProcess, IID_IEvaluator, reinterpret_cast(&m_pIEvaluator)); if (MUX_FAILED(mr)) { return mr; } mr = mux_CreateInstance(CID_Permissions, nullptr, UseSameProcess, IID_IPermissions, reinterpret_cast(&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(&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(this); } else if (IID_IComsysControl == iid) { *ppv = static_cast(this); } else if (IID_IServerEventsSink == iid) { *ppv = static_cast(this); } else { *ppv = nullptr; return MUX_E_NOINTERFACE; } reinterpret_cast(*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(ctx); auto ch = std::make_unique(); if (nullptr != name) { strncpy(reinterpret_cast(ch->name), reinterpret_cast(name), MAX_CHANNEL_LEN); ch->name[MAX_CHANNEL_LEN] = '\0'; } if (nullptr != header) { strncpy(reinterpret_cast(ch->header), reinterpret_cast(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 key(ch->name, ch->name + strlen(reinterpret_cast(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(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(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(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(alias); ca.channel = reinterpret_cast(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 key(name, name + strlen(reinterpret_cast(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(name), reinterpret_cast(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(alias)) { return reinterpret_cast(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(ch->header), reinterpret_cast(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(ch->header), reinterpret_cast(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(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(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(detail), sizeof(detail), reinterpret_cast(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(user.title.c_str())), "SyncChannelUser(channel=%s, who=#%d)", reinterpret_cast(channel_name), static_cast(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(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(who), reinterpret_cast(alias), reinterpret_cast(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(who), reinterpret_cast(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(channel_name), static_cast(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(pfxNormal)); mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."), reinterpret_cast(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(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(ch->header), reinterpret_cast(pMoniker)); if (!bSpoof) { mux_sprintf(pNoComtitle, nNoComtitle, T("%s"), reinterpret_cast(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(evaled)); } } if (bSpoof) { // The comtitle stands in for the speaker. // mux_sprintf(pNormal, nNormal, T("%s %s"), reinterpret_cast(ch->header), reinterpret_cast(title)); return; } mux_sprintf(pNormal, nNormal, T("%s %s %s"), reinterpret_cast(ch->header), reinterpret_cast(title), reinterpret_cast(pMoniker)); mux_sprintf(pNoComtitle, nNoComtitle, T("%s %s"), reinterpret_cast(ch->header), reinterpret_cast(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(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 "#" -- 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` 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(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(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(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(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(ltaNow.ReturnDateString(0)), reinterpret_cast(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(histattr), static_cast(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(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(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(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(pfxNormal)); mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has joined this channel."), reinterpret_cast(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(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(pfxNormal)); mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), M_("%s has left this channel."), reinterpret_cast(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(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(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(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(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(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(arg2), "on")) { do_joinchannel(player, ch); return; } if (0 == strcmp(reinterpret_cast(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(arg1)); m_pINotify->RawNotify(player, msg); } return; } if (0 == strcmp(reinterpret_cast(arg2), "who")) { do_comwho(player, ch); return; } // Handle "last [N]" — channel history recall. // if (0 == strncmp(reinterpret_cast(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(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(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: " ", 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: "" // pPose++; pFmt = T("%s%s"); } else { // Say: " says, “”" // pFmt = M_("%s says, “%s”"); } mux_sprintf(msg, sizeof(msg), pFmt, reinterpret_cast(pfxNormal), pPose); if (bHaveNoComtitle) { mux_sprintf(msgNoComtitle, sizeof(msgNoComtitle), pFmt, reinterpret_cast(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(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 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(ca.channel.c_str()), ca.alias); do_comconnectraw_notify(player, reinterpret_cast(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 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(ca.channel.c_str())); do_comdisconnectraw_notify(player, reinterpret_cast(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 aliases = c.aliases; for (const auto &ca : aliases) { do_delcomchannel(player, reinterpret_cast(ca.channel.c_str()), true); sqlite_wt_delete_player_channel(player, reinterpret_cast(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 destroyedNames; for (auto it = m_channels.begin(); it != m_channels.end(); ) { if (player == it->second->charge_who) { destroyedNames.emplace_back( reinterpret_cast(it->second->name)); if (nullptr != m_pIStorage) { log_storage_failure( m_pIStorage->DeleteChannel(it->second->name), "DeleteChannel(channel=%s) [owner #%d destroyed]", reinterpret_cast(it->second->name), static_cast(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(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(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(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(ca.channel.c_str()), const_cast(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(M_("Status"))); pos = append_bytes(header, sizeof(header), pos, " "); pos = append_bytes(header, sizeof(header), pos, reinterpret_cast(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(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(pPattern))) { continue; } UTF8 msg[256]; size_t pos = 0; pos = append_ljust_field(msg, sizeof(msg), pos, reinterpret_cast(ca.alias.c_str()), kComlistAliasCols); pos = append_bytes(msg, sizeof(msg), pos, " "); pos = append_ljust_field(msg, sizeof(msg), pos, reinterpret_cast(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(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(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(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(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(ch->name)); break; default: // Set title. // if (nullptr != pTitle && '\0' != *pTitle) { user->title.assign(reinterpret_cast(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(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(M_("Users"))); pos = append_bytes(header, sizeof(header), pos, " "); pos = append_bytes(header, sizeof(header), pos, reinterpret_cast(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(ch->name), reinterpret_cast(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(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(ch->name), reinterpret_cast(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(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(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( (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(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(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(msg), reinterpret_cast(pText), sizeof(msg) - 1); msg[sizeof(msg) - 1] = '\0'; } else { mux_sprintf(msg, sizeof(msg), T("%s %s"), reinterpret_cast(ch->header), reinterpret_cast(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(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(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(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(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(pChannel)); msg = msgbuf; break; case CSET_SPOOF: ch->type |= CHANNEL_SPOOF; mux_sprintf(msgbuf, sizeof(msgbuf), M_("@cset: Channel %s set spoofable."), reinterpret_cast(pChannel)); msg = msgbuf; break; case CSET_NOSPOOF: ch->type &= ~CHANNEL_SPOOF; mux_sprintf(msgbuf, sizeof(msgbuf), M_("@cset: Channel %s set unspoofable."), reinterpret_cast(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(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(ch->name), pName ? reinterpret_cast(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(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("Set."); } break; case CSET_LOG: { if (nullptr == pValue || '\0' == pValue[0]) { msg = reinterpret_cast( "@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( "@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( "@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(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(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(pChannel)); } else { mux_sprintf(msgbuf, sizeof(msgbuf), M_("@cset: Channel %s maximum history set."), reinterpret_cast(pChannel)); } msg = msgbuf; } break; case CSET_LOG_TIME: { if (nullptr == pValue || '\0' == pValue[0]) { msg = reinterpret_cast("@cset: Failed."); break; } int64_t value = mux_atoi64(pValue); if (value != 0 && value != 1) { msg = reinterpret_cast("@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(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(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(pChannel)); } else { mux_sprintf(msgbuf, sizeof(msgbuf), M_("@cset: Channel %s timestamp logging set."), reinterpret_cast(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(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("Set.")); } else { m_pINotify->Notify(executor, reinterpret_cast("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(c_charge); m_pINotify->Notify(executor, reinterpret_cast("Set.")); } else { m_pINotify->Notify(executor, reinterpret_cast( "That is not a reasonable cost.")); } } break; case EDIT_CHANNEL_CPFLAGS: { int access = 0; if (nullptr != s) { if (strcmp(reinterpret_cast(s), "join") == 0) { access = CHANNEL_PLAYER_JOIN; } else if (strcmp(reinterpret_cast(s), "receive") == 0) { access = CHANNEL_PLAYER_RECEIVE; } else if (strcmp(reinterpret_cast(s), "transmit") == 0) { access = CHANNEL_PLAYER_TRANSMIT; } } if (access) { if (add_remove) { ch->type |= access; m_pINotify->Notify(executor, reinterpret_cast("@cpflags: Set.")); } else { ch->type &= ~access; m_pINotify->Notify(executor, reinterpret_cast("@cpflags: Cleared.")); } } else { m_pINotify->Notify(executor, reinterpret_cast("@cpflags: Unknown Flag.")); } } break; case EDIT_CHANNEL_COFLAGS: { int access = 0; if (nullptr != s) { if (strcmp(reinterpret_cast(s), "join") == 0) { access = CHANNEL_OBJECT_JOIN; } else if (strcmp(reinterpret_cast(s), "receive") == 0) { access = CHANNEL_OBJECT_RECEIVE; } else if (strcmp(reinterpret_cast(s), "transmit") == 0) { access = CHANNEL_OBJECT_TRANSMIT; } } if (access) { if (add_remove) { ch->type |= access; m_pINotify->Notify(executor, reinterpret_cast("@coflags: Set.")); } else { ch->type &= ~access; m_pINotify->Notify(executor, reinterpret_cast("@coflags: Cleared.")); } } else { m_pINotify->Notify(executor, reinterpret_cast("@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("@cboot: Unknown channel.")); return MUX_S_OK; } struct comuser *user = select_user(ch, executor); if (nullptr == user) { m_pINotify->Notify(executor, reinterpret_cast( "@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( "@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(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(pVictMoniker) : "???", reinterpret_cast(ch->name)); m_pINotify->Notify(executor, msg); mux_sprintf(msg, sizeof(msg), M_("%s boots you off channel %s."), pExecMoniker ? reinterpret_cast(pExecMoniker) : "???", reinterpret_cast(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(ch->header), pExecMoniker ? reinterpret_cast(pExecMoniker) : "???", pVictMoniker ? reinterpret_cast(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(pCmd), ' '); if ( nullptr == t || t - reinterpret_cast(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(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(reinterpret_cast(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(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(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(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(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(pAlias); ca.channel = reinterpret_cast(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(ch->name), reinterpret_cast(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(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(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(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(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(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(); strncpy(reinterpret_cast(ch->name), reinterpret_cast(pName), MAX_CHANNEL_LEN); ch->name[MAX_CHANNEL_LEN] = '\0'; mux_sprintf(ch->header, MAX_HEADER_LEN + 1, T("[%s]"), reinterpret_cast(ch->name)); ch->type = CHANNEL_DEFAULT; ch->charge_who = executor; ch->chan_obj = NOTHING; channel *pCh = ch.get(); std::vector key(ch->name, ch->name + strlen(reinterpret_cast(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(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(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(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(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(pName)); m_pINotify->RawNotify(executor, msg); } // Remove from map — unique_ptr cleans up channel and all users. // std::vector key(ch->name, ch->name + strlen(reinterpret_cast(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(this); } else if (mux_IID_IClassFactory == iid) { *ppv = static_cast(this); } else { *ppv = nullptr; return MUX_E_NOINTERFACE; } reinterpret_cast(*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; }