tinymux/mux/modules/engine/engine_com.cpp

6165 lines
160 KiB
C++
Raw Permalink Normal View History

/*! \file engine_com.cpp
* \brief Engine-side COM class implementations and COM front-door.
*
* These classes implement the server-provided COM interfaces that
* external modules use to interact with the game engine.
*
* engine.so is a proper COM server. Only 4 extern "C" functions are
* exported: mux_Register, mux_Unregister, mux_GetClassObject, and
* mux_CanUnloadNow. All other symbols have hidden visibility.
* g_debug_cmd lives in libmux.so (shared crash breadcrumb).
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "sqlite_backend.h"
#include "mguests.h"
#include "engine_api.h"
#include "routing.h"
#include "walk.h"
// g_debug_cmd moved to libmux.cpp — shared by all layers.
// ---------------------------------------------------------------------------
// Factory class declarations for engine-side COM components.
// These are internal to engine.so (no DCL_EXPORT).
// ---------------------------------------------------------------------------
#define DEFINE_ENGINE_FACTORY(x) \
class x : public mux_IClassFactory \
{ \
public: \
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv); \
virtual uint32_t AddRef(void); \
virtual uint32_t Release(void); \
virtual MUX_RESULT CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv); \
virtual MUX_RESULT LockServer(bool bLock); \
x(void); \
virtual ~x(); \
private: \
uint32_t m_cRef; \
};
// Factory classes defined in log.cpp (also in engine.so).
// Full declarations needed here for the COM front-door dispatch.
//
DEFINE_ENGINE_FACTORY(CLogFactory)
class CLogPSFactory : public mux_IPSFactoryBuffer, public mux_IClassFactory
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT CreateProxy(mux_IUnknown *pUnknownOuter, MUX_IID riid, mux_IRpcProxyBuffer **ppProxy, void **ppv);
virtual MUX_RESULT CreateStub(MUX_IID riid, mux_IUnknown *pUnknownOuter, mux_IRpcStubBuffer **ppStub);
virtual MUX_RESULT CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv);
virtual MUX_RESULT LockServer(bool bLock);
CLogPSFactory(void);
virtual ~CLogPSFactory();
private:
uint32_t m_cRef;
};
DEFINE_ENGINE_FACTORY(CServerEventsSourceFactory)
DEFINE_ENGINE_FACTORY(CQueryClientFactory)
DEFINE_ENGINE_FACTORY(CFunctionsFactory)
DEFINE_ENGINE_FACTORY(CNotifyFactory)
DEFINE_ENGINE_FACTORY(CObjectInfoFactory)
DEFINE_ENGINE_FACTORY(CAttributeAccessFactory)
DEFINE_ENGINE_FACTORY(CEvaluatorFactory)
DEFINE_ENGINE_FACTORY(CPermissionsFactory)
feat(modules): GAME_CONFIG -- game-policy config reaches modules (#1654) The parking condition on #1654 was "a second consumer"; the tally reached three, all documented divergences: searchcost mail non-wizard self @mail/stats was free eval_comtitle comsys comtitles always evaluated under the module money_name_* mail the charge-refusal text could not be composed ## Shape A new engine-registered class rather than an Initialize signature change: mux_IGameConfig::GetGameConfig(GAME_CONFIG *) CID_GameConfig No existing IID moves, so there is no ABI break in either direction: an old module never asks, and a new module against an old engine gets CLASSNOTAVAILABLE, soft-fails to nullptr, and keeps its prior behaviour. Two design rules baked in rather than documented and hoped for: * Queried per CALL, never cached at Initialize. A boot-time snapshot is #1613's bug -- @admin reports Set. while the module keeps stale values. @admin search_cost=50 now takes effect on the next @mail/stats. * Versioned by cbSize: caller zeroes the struct and sets cbSize; the engine fills what fits. The struct can grow without a new interface, and zero must stay a safe default for every future field. ## Both consumers, verified as a mortal The gap was wizard-invisible -- payfor() exempts wizards and every harness runs as God -- so verification used muxscript -p with a @pcreate'd mortal: search_cost 5, rich engine 150->145 module 150->145 IDENTICAL search_cost 99999, poor both: "Finding mail stats costs 99999 Pennies.", money untouched eval_comtitle default [ec] 3 Wizard says ... both sides eval_comtitle 0 [ec] [strlen(abc)] Wizard says both sides The eval_comtitle rows close the divergence documented at channel_speaker_name since #1640/#1647. Conformance and smoke are unchanged by construction (wizard runs), and were run anyway. make test: Smoke 1561 x3, conformance PASSED, handoff 13. TESTEXIT=0. Refs #1613, #1614, #1631, #1640, #1647. Closes #1654. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:35 -06:00
DEFINE_ENGINE_FACTORY(CGameConfigFactory)
DEFINE_ENGINE_FACTORY(CMailDeliveryFactory)
DEFINE_ENGINE_FACTORY(CHelpSystemFactory)
DEFINE_ENGINE_FACTORY(CGameEngineFactory)
DEFINE_ENGINE_FACTORY(CPlayerSessionFactory)
DEFINE_ENGINE_FACTORY(CComsysStorageFactory)
DEFINE_ENGINE_FACTORY(CMailStorageFactory)
#if defined(TINYMUX_JIT)
// JIT Compile factory — implementation delegates to jit_lua.cpp.
extern MUX_RESULT jit_compile_create_instance(MUX_IID iid, void **ppv);
DEFINE_ENGINE_FACTORY(CJITCompileFactory)
#endif
// Lua module factory — implementation delegates to lua_mod.cpp.
extern MUX_RESULT lua_mod_create_instance(MUX_IID iid, void **ppv);
DEFINE_ENGINE_FACTORY(CLuaModFactory)
// CServerEventsSource component which is not directly accessible.
//
class CServerEventsSource : public mux_IServerEventsControl
{
public:
// mux_IUnknown
//
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
// mux_IServerEventsControl
//
virtual MUX_RESULT Advise(mux_IServerEventsSink *pIServerEvents);
CServerEventsSource(void);
virtual ~CServerEventsSource();
private:
uint32_t m_cRef;
mux_IServerEventsSink *m_pSink;
};
CServerEventsSource::CServerEventsSource(void) : m_cRef(1), m_pSink(nullptr)
{
}
ServerEventsSinkNode *g_pServerEventsSinkListHead = nullptr;
CServerEventsSource::~CServerEventsSource()
{
if (nullptr != m_pSink)
{
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
ServerEventsSinkNode *q = nullptr;
while (nullptr != p)
{
if (p->pSink == m_pSink)
{
// Unlink node p from list.
//
if (nullptr == q)
{
g_pServerEventsSinkListHead = p->pNext;
}
else
{
q->pNext = p->pNext;
}
p->pNext = nullptr;
// Free sink and node.
//
p->pSink->Release();
p->pSink = nullptr;
delete p;
break;
}
q = p;
p = p->pNext;
}
m_pSink->Release();
m_pSink = nullptr;
}
}
MUX_RESULT CServerEventsSource::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IServerEventsControl *>(this);
}
else if (IID_IServerEventsControl == iid)
{
*ppv = static_cast<mux_IServerEventsControl *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CServerEventsSource::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CServerEventsSource::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CServerEventsSource::Advise(mux_IServerEventsSink *pIServerEventsSink)
{
if (nullptr == pIServerEventsSink)
{
return MUX_E_INVALIDARG;
}
// If this pointer is already in the list, we will prevent it from being
// added again.
//
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
if (p->pSink == pIServerEventsSink)
{
return MUX_E_FAIL;
}
p = p->pNext;
}
// Allocate a list node.
//
p = nullptr;
try
{
p = new ServerEventsSinkNode;
}
catch (...)
{
; // Nothing.
}
if (nullptr == p)
{
return MUX_E_OUTOFMEMORY;
}
// Add the pointer to the list.
//
p->pNext = g_pServerEventsSinkListHead;
pIServerEventsSink->AddRef();
p->pSink = pIServerEventsSink;
pIServerEventsSink->AddRef();
m_pSink = pIServerEventsSink;
g_pServerEventsSinkListHead = p;
return MUX_S_OK;
}
// Factory for CServerEventsSource component which is not directly accessible.
//
CServerEventsSourceFactory::CServerEventsSourceFactory(void) : m_cRef(1)
{
}
CServerEventsSourceFactory::~CServerEventsSourceFactory()
{
}
MUX_RESULT CServerEventsSourceFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CServerEventsSourceFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CServerEventsSourceFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CServerEventsSourceFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
// Disallow attempts to aggregate this component.
//
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CServerEventsSource *pServerEventsSource = nullptr;
try
{
pServerEventsSource = new CServerEventsSource;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pServerEventsSource)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pServerEventsSource->QueryInterface(iid, ppv);
pServerEventsSource->Release();
return mr;
}
MUX_RESULT CServerEventsSourceFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// CQueryClient component which is not directly accessible.
//
class CQueryClient : public mux_IQuerySink
{
public:
// mux_IUnknown
//
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
// mux_IQuerySink
//
virtual MUX_RESULT Result(uint32_t iQueryHandle, uint32_t iError, QUEUE_INFO *pqiResultsSet);
CQueryClient(void);
virtual ~CQueryClient();
private:
uint32_t m_cRef;
};
CQueryClient::CQueryClient(void) : m_cRef(1)
{
}
CQueryClient::~CQueryClient()
{
}
MUX_RESULT CQueryClient::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IQuerySink *>(this);
}
else if (IID_IQuerySink == iid)
{
*ppv = static_cast<mux_IQuerySink *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CQueryClient::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CQueryClient::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CQueryClient::Result(uint32_t hQuery, uint32_t iError, QUEUE_INFO *pqiResultsSet)
{
#if defined(STUB_SLAVE)
CResultsSet *prs = nullptr;
try
{
prs = new CResultsSet(pqiResultsSet);
}
catch (...)
{
; // Nothing.
}
query_complete(hQuery, iError, prs);
prs->Release();
#else
UNUSED_PARAMETER(hQuery);
UNUSED_PARAMETER(iError);
UNUSED_PARAMETER(pqiResultsSet);
#endif // STUB_SLAVE
return MUX_S_OK;
}
// Factory for CQueryClient component which is not directly accessible.
//
CQueryClientFactory::CQueryClientFactory(void) : m_cRef(1)
{
}
CQueryClientFactory::~CQueryClientFactory()
{
}
MUX_RESULT CQueryClientFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CQueryClientFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CQueryClientFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CQueryClientFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
// Disallow attempts to aggregate this component.
//
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CQueryClient *pLog = nullptr;
try
{
pLog = new CQueryClient;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pLog)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pLog->QueryInterface(iid, ppv);
pLog->Release();
return mr;
}
MUX_RESULT CQueryClientFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
CResultsSet::CResultsSet(QUEUE_INFO *pqi) : m_cRef(1), m_nFields(0),
m_nBlob(0), m_bLoaded(false), m_iError(QS_SUCCESS), m_nRows(0)
{
m_pBlob = nullptr;
m_pRows = nullptr;
size_t nWanted = sizeof(m_nFields);
if ( Pipe_GetBytes(pqi, &nWanted, &m_nFields)
&& nWanted == sizeof(m_nFields))
{
size_t nRows;
m_nBlob = Pipe_QueueLength(pqi);
if (sizeof(nRows) < m_nBlob)
{
bool bError = false;
m_nBlob -= sizeof(nRows);
if (0 < m_nBlob)
{
try
{
m_pBlob = new UTF8[m_nBlob];
}
catch (...)
{
; // Nothing.
}
nWanted = m_nBlob;
if ( nullptr == m_pBlob
|| !Pipe_GetBytes(pqi, &nWanted, m_pBlob)
|| nWanted != m_nBlob)
{
bError = true;
}
}
if (!bError)
{
nWanted = sizeof(nRows);
if ( Pipe_GetBytes(pqi, &nWanted, &nRows)
&& nWanted == sizeof(nRows))
{
m_nRows = static_cast<int>(nRows);
try
{
m_pRows = new PUTF8[m_nRows];
}
catch (...)
{
; // Nothing.
}
if (nullptr != m_pRows)
{
// #1878: never trust field lengths without an end
// bound. A truncated or hostile blob used to
// memcpy/advance past m_pBlob + m_nBlob.
//
int i, j;
UTF8 *p = m_pBlob;
UTF8 *const pEnd = m_pBlob + m_nBlob;
bool bRowError = false;
for (i = 0; i < m_nRows && !bRowError; i++)
{
m_pRows[i] = p;
for (j = 0; j < m_nFields; j++)
{
if (static_cast<size_t>(pEnd - p) < sizeof(size_t))
{
bRowError = true;
break;
}
size_t n;
memcpy(&n, p, sizeof(size_t));
p += sizeof(size_t);
if (static_cast<size_t>(pEnd - p) < n)
{
bRowError = true;
break;
}
p += n;
}
}
if (!bRowError && p == pEnd && i == m_nRows)
{
m_bLoaded = true;
}
}
}
}
}
}
}
bool CResultsSet::isLoaded(void)
{
return m_bLoaded;
}
void CResultsSet::SetError(uint32_t iError)
{
m_iError = iError;
}
uint32_t CResultsSet::GetError(void)
{
return m_iError;
}
int CResultsSet::GetRowCount(void)
{
return m_nRows;
}
const UTF8 *CResultsSet::FirstField(int iRow)
{
if ( 0 <= iRow
&& iRow < m_nRows
&& nullptr != m_pRows
&& 0 < m_nFields)
{
m_pCurrentField = m_pRows[iRow];
m_iCurrentField = 1;
}
else
{
m_pCurrentField = nullptr;
m_iCurrentField = 1;
}
return m_pCurrentField;
}
const UTF8 *CResultsSet::NextField(void)
{
// #1878: validate the current field header and payload fit in the blob
// before advancing; return null on a truncated or oversize length.
//
if ( nullptr == m_pCurrentField
|| nullptr == m_pBlob
|| 0 >= m_nFields
|| m_iCurrentField >= m_nFields)
{
return nullptr;
}
UTF8 *const pEnd = m_pBlob + m_nBlob;
if (m_pCurrentField < m_pBlob
|| m_pCurrentField >= pEnd
|| static_cast<size_t>(pEnd - m_pCurrentField) < sizeof(size_t))
{
m_pCurrentField = nullptr;
return nullptr;
}
size_t n;
memcpy(&n, m_pCurrentField, sizeof(size_t));
const UTF8 *pNext = m_pCurrentField + sizeof(size_t);
if (static_cast<size_t>(pEnd - pNext) < n)
{
m_pCurrentField = nullptr;
return nullptr;
}
pNext += n;
if (pNext > pEnd)
{
m_pCurrentField = nullptr;
return nullptr;
}
m_iCurrentField++;
m_pCurrentField = pNext;
return m_pCurrentField;
}
CResultsSet::~CResultsSet(void)
{
if (nullptr != m_pBlob)
{
delete [] m_pBlob;
m_pBlob = nullptr;
}
if (nullptr != m_pRows)
{
delete [] m_pRows;
m_pRows = nullptr;
}
}
uint32_t CResultsSet::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
uint32_t CResultsSet::AddRef(void)
{
m_cRef++;
return m_cRef;
}
// ---------------------------------------------------------------------------
// CNotify — mux_INotify implementation (in-process only, no marshaling).
// ---------------------------------------------------------------------------
class CNotify : public mux_INotify
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT Notify(dbref target, const UTF8 *msg);
virtual MUX_RESULT RawNotify(dbref target, const UTF8 *msg);
virtual MUX_RESULT NotifyCheck(dbref target, dbref sender,
const UTF8 *msg, int key);
CNotify(void);
virtual ~CNotify();
private:
uint32_t m_cRef;
};
CNotify::CNotify(void) : m_cRef(1)
{
}
CNotify::~CNotify()
{
}
MUX_RESULT CNotify::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_INotify *>(this);
}
else if (IID_INotify == iid)
{
*ppv = static_cast<mux_INotify *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CNotify::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CNotify::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CNotify::Notify(dbref target, const UTF8 *msg)
{
if (!Good_obj(target))
{
return MUX_E_INVALIDARG;
}
notify(target, msg);
return MUX_S_OK;
}
MUX_RESULT CNotify::RawNotify(dbref target, const UTF8 *msg)
{
if (!Good_obj(target))
{
return MUX_E_INVALIDARG;
}
raw_notify(target, msg);
return MUX_S_OK;
}
MUX_RESULT CNotify::NotifyCheck(dbref target, dbref sender,
const UTF8 *msg, int key)
{
if (!Good_obj(target))
{
return MUX_E_INVALIDARG;
}
notify_check(target, sender, msg, key);
return MUX_S_OK;
}
CNotifyFactory::CNotifyFactory(void) : m_cRef(1)
{
}
CNotifyFactory::~CNotifyFactory()
{
}
MUX_RESULT CNotifyFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CNotifyFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CNotifyFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CNotifyFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CNotify *pNotify = nullptr;
try
{
pNotify = new CNotify;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pNotify)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pNotify->QueryInterface(iid, ppv);
pNotify->Release();
return mr;
}
MUX_RESULT CNotifyFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CObjectInfo — mux_IObjectInfo implementation (in-process only).
// ---------------------------------------------------------------------------
class CObjectInfo : public mux_IObjectInfo
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT IsValid(dbref obj, bool *pValid);
virtual MUX_RESULT GetName(dbref obj, const UTF8 **ppName);
virtual MUX_RESULT GetOwner(dbref obj, dbref *pOwner);
virtual MUX_RESULT GetLocation(dbref obj, dbref *pLocation);
virtual MUX_RESULT GetType(dbref obj, int *pType);
virtual MUX_RESULT IsConnected(dbref obj, bool *pConnected);
virtual MUX_RESULT IsPlayer(dbref obj, bool *pPlayer);
virtual MUX_RESULT IsGoing(dbref obj, bool *pGoing);
virtual MUX_RESULT GetMoniker(dbref obj, const UTF8 **ppMoniker);
virtual MUX_RESULT MatchThing(dbref executor, const UTF8 *pName,
dbref *pResult);
virtual MUX_RESULT GetFlags(dbref obj, int word, unsigned int *pFlags);
virtual MUX_RESULT SetFlags(dbref obj, int word, unsigned int flags);
virtual MUX_RESULT GetPowers(dbref obj, unsigned int *pPowers);
virtual MUX_RESULT GetPennies(dbref obj, int *pPennies);
virtual MUX_RESULT PayFor(dbref who, int cost, bool *pPaid);
virtual MUX_RESULT GiveTo(dbref who, int amount);
virtual MUX_RESULT GetPureName(dbref obj, const UTF8 **ppName);
virtual MUX_RESULT DecodeFlags(dbref player, dbref obj, UTF8 **ppStr);
fix(comsys): the module's command surface, compared against the engine (#1640) Four divergences from the issue, plus three more the probe found once the fixtures stopped being plain ASCII. From #1640: * @clist/full ignored the switch and printed the default listing, losing Header, Access, Users and Msgs -- the four columns the switch exists for. * @cwho printed a bare name where the engine prints unparse_object(), so staff lost the dbref and flags that identify WHICH object is on a channel. * comtitles reached no message at all. The module stored them and round-tripped them through SyncChannelUser, then never read them back when composing one: not speech, not poses, not join/leave, and not spoof channels, where the comtitle is supposed to REPLACE the speaker's name. * delcom emitted the channel broadcast instead of the leaver's own confirmation -- which the leaver cannot even see, since clearing bConnected first is what suppresses it, so delcom looked like it had done nothing and any trigger matching "^You have left channel" stopped firing. Comtitles are one fix, not four: BuildSpeakerPrefix mirrors the engine's BuildChannelMessage, including that the LISTENER decides whether a comtitle is shown, so SendChannelMessage now takes both variants and picks per recipient. Found while verifying, none of which an ASCII fixture can see: * The ENGINE conflated byte offset with column offset in @clist/full (comsys.cpp:2877), resetting iPos.m_column to the byte offset after writing JXR. Those are equal only while every preceding field is plain ASCII; a colored channel header makes the byte count larger, PadField believes it is already past column 56, and the Users column shifts left. * The module stored channel headers with ANSI uncollapsed -- a raw strncpy where do_cheader runs StripTabsAndTruncate -- so `[ansi(r,RED)][ansi(b,BLU)]` persisted an extra reset between the codes. A divergence in SQLite, not on screen, so it survived a handoff. The same strncpy truncated at MAX_HEADER_LEN *bytes*, which can split a codepoint or a color code. * @clist/headers was missing the engine's pad to column 79. Column layout in the module now goes through StripTabsAndTruncate/PadField rather than printf field widths. A printf precision counts codepoints and knows nothing about PUA color; StripTabsAndTruncate carries a byte limit and a column limit as a pair and reserves budget for the closing color sequence. My first version of this fix hand-rolled the column stops with %-14.13s and verified byte-identical output against the engine -- for ASCII only, which is exactly the blind spot that produced two of the three findings above. #1649 covers the general problem; nothing here waits on it. @cwho deliberately reproduces the engine's composition including strip_color(): @cwho discards color so its width arithmetic is honest while @clist two functions away preserves it. Both are coping strategies for the primitive #1649 proposes, and this file's job is parity, not unilaterally improving one side into a fresh divergence. mux_IObjectInfo gains UnparseObject: the visibility rule is Examinable() plus the CHOWN_OK/JUMP_OK/LINK_OK/DESTROY_OK/ABODE exceptions, which a module cannot compute from GetFlags/DecodeFlags. Caller-supplied buffer, matching AtrGet, so nothing crosses the DLL boundary needing to be freed on the far side. CID_ObjectInfo is UseSameProcess only, so there is no proxy/stub to update. New harness, tests/comsys_cmdparity: one command stream, two fresh databases, output diffed. The two existing comsys harnesses compare state handoff and delivery hooks; neither compares plain command output, and both scored green against all seven of these. Fixtures are deliberately colored and CJK. Verified it goes red: reinstating the engine's PadField conflation fails case 3 while cases 1 and 2 still pass, which is the ASCII blind spot reproduced on demand. Windows Server 2022, MSVC 14.51, Release x64: three parity streams identical; comsys_handoff 13/13; comsys_mogrify 5/5; smoke 1555 succeeded / 5 failed / 0 crashes, unchanged, the five being the pre-existing no-OpenSSL gap (#1641). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:31:58 -06:00
virtual MUX_RESULT UnparseObject(dbref player, dbref target,
bool bObeyMyopic, UTF8 *pBuf, size_t nBufMax);
virtual MUX_RESULT IsWizard(dbref obj, bool *pResult);
virtual MUX_RESULT IsWizRoy(dbref obj, bool *pResult);
virtual MUX_RESULT CanIdle(dbref obj, bool *pResult);
virtual MUX_RESULT WizardWho(dbref obj, bool *pResult);
virtual MUX_RESULT SeeHidden(dbref obj, bool *pResult);
virtual MUX_RESULT AtrAddRaw(dbref obj, int attrnum, const UTF8 *value);
virtual MUX_RESULT AtrClr(dbref obj, int attrnum);
virtual MUX_RESULT AtrGet(dbref obj, int attrnum, UTF8 *pValue,
size_t nValueMax, dbref *pOwner, int *pFlags);
virtual MUX_RESULT AtrPGet(dbref obj, int attrnum, UTF8 *pValue,
size_t nValueMax, dbref *pOwner, int *pFlags);
virtual MUX_RESULT LookupPlayer(dbref executor, const UTF8 *pName,
bool bConnected, dbref *pResult);
virtual MUX_RESULT FetchConnectionInfoFields(dbref player,
int64_t anFields[4]);
virtual MUX_RESULT PutConnectionInfoFields(dbref player,
int64_t anFields[4], CLinearTimeAbsolute &ltaNow);
CObjectInfo(void);
virtual ~CObjectInfo();
private:
uint32_t m_cRef;
};
CObjectInfo::CObjectInfo(void) : m_cRef(1)
{
}
CObjectInfo::~CObjectInfo()
{
}
MUX_RESULT CObjectInfo::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IObjectInfo *>(this);
}
else if (IID_IObjectInfo == iid)
{
*ppv = static_cast<mux_IObjectInfo *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CObjectInfo::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CObjectInfo::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CObjectInfo::IsValid(dbref obj, bool *pValid)
{
if (nullptr == pValid)
{
return MUX_E_INVALIDARG;
}
*pValid = Good_obj(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetName(dbref obj, const UTF8 **ppName)
{
if (nullptr == ppName)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*ppName = nullptr;
return MUX_E_INVALIDARG;
}
*ppName = Name(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetOwner(dbref obj, dbref *pOwner)
{
if (nullptr == pOwner)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pOwner = NOTHING;
return MUX_E_INVALIDARG;
}
*pOwner = Owner(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetLocation(dbref obj, dbref *pLocation)
{
if (nullptr == pLocation)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pLocation = NOTHING;
return MUX_E_INVALIDARG;
}
*pLocation = Location(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetType(dbref obj, int *pType)
{
if (nullptr == pType)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pType = TYPE_GARBAGE;
return MUX_E_INVALIDARG;
}
*pType = Typeof(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::IsConnected(dbref obj, bool *pConnected)
{
if (nullptr == pConnected)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pConnected = false;
return MUX_E_INVALIDARG;
}
*pConnected = Connected(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::IsPlayer(dbref obj, bool *pPlayer)
{
if (nullptr == pPlayer)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pPlayer = false;
return MUX_E_INVALIDARG;
}
*pPlayer = isPlayer(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::IsGoing(dbref obj, bool *pGoing)
{
if (nullptr == pGoing)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pGoing = false;
return MUX_E_INVALIDARG;
}
*pGoing = Going(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetMoniker(dbref obj, const UTF8 **ppMoniker)
{
if (nullptr == ppMoniker)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*ppMoniker = nullptr;
return MUX_E_INVALIDARG;
}
*ppMoniker = Moniker(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::MatchThing(dbref executor, const UTF8 *pName,
dbref *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (nullptr == pName)
{
*pResult = NOTHING;
return MUX_E_INVALIDARG;
}
*pResult = match_thing(executor, pName);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetFlags(dbref obj, int word, unsigned int *pFlags)
{
if (nullptr == pFlags)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj) || word < FLAG_WORD1 || word > FLAG_WORD3)
{
*pFlags = 0;
return MUX_E_INVALIDARG;
}
*pFlags = db[obj].fs.word[word];
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::SetFlags(dbref obj, int word, unsigned int flags)
{
if (!Good_obj(obj) || word < FLAG_WORD1 || word > FLAG_WORD3)
{
return MUX_E_INVALIDARG;
}
s_Flags(obj, word, flags);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetPowers(dbref obj, unsigned int *pPowers)
{
if (nullptr == pPowers)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pPowers = 0;
return MUX_E_INVALIDARG;
}
*pPowers = Powers(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetPennies(dbref obj, int *pPennies)
{
if (nullptr == pPennies)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pPennies = 0;
return MUX_E_INVALIDARG;
}
*pPennies = Pennies(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::PayFor(dbref who, int cost, bool *pPaid)
{
if (nullptr == pPaid)
{
return MUX_E_INVALIDARG;
}
*pPaid = false;
if (!Good_obj(who))
{
return MUX_E_INVALIDARG;
}
// payfor() is wizard/free-money aware and debits Owner(who).
//
*pPaid = payfor(who, cost);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GiveTo(dbref who, int amount)
{
if (!Good_obj(who))
{
return MUX_E_INVALIDARG;
}
// giveto() no-ops for wizards/free-money holders.
//
giveto(who, amount);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::GetPureName(dbref obj, const UTF8 **ppName)
{
if (nullptr == ppName)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*ppName = nullptr;
return MUX_E_INVALIDARG;
}
*ppName = PureName(obj);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::DecodeFlags(dbref player, dbref obj, UTF8 **ppStr)
{
if (nullptr == ppStr)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*ppStr = nullptr;
return MUX_E_INVALIDARG;
}
*ppStr = decode_flags(player, &(db[obj].fs));
return MUX_S_OK;
}
fix(comsys): the module's command surface, compared against the engine (#1640) Four divergences from the issue, plus three more the probe found once the fixtures stopped being plain ASCII. From #1640: * @clist/full ignored the switch and printed the default listing, losing Header, Access, Users and Msgs -- the four columns the switch exists for. * @cwho printed a bare name where the engine prints unparse_object(), so staff lost the dbref and flags that identify WHICH object is on a channel. * comtitles reached no message at all. The module stored them and round-tripped them through SyncChannelUser, then never read them back when composing one: not speech, not poses, not join/leave, and not spoof channels, where the comtitle is supposed to REPLACE the speaker's name. * delcom emitted the channel broadcast instead of the leaver's own confirmation -- which the leaver cannot even see, since clearing bConnected first is what suppresses it, so delcom looked like it had done nothing and any trigger matching "^You have left channel" stopped firing. Comtitles are one fix, not four: BuildSpeakerPrefix mirrors the engine's BuildChannelMessage, including that the LISTENER decides whether a comtitle is shown, so SendChannelMessage now takes both variants and picks per recipient. Found while verifying, none of which an ASCII fixture can see: * The ENGINE conflated byte offset with column offset in @clist/full (comsys.cpp:2877), resetting iPos.m_column to the byte offset after writing JXR. Those are equal only while every preceding field is plain ASCII; a colored channel header makes the byte count larger, PadField believes it is already past column 56, and the Users column shifts left. * The module stored channel headers with ANSI uncollapsed -- a raw strncpy where do_cheader runs StripTabsAndTruncate -- so `[ansi(r,RED)][ansi(b,BLU)]` persisted an extra reset between the codes. A divergence in SQLite, not on screen, so it survived a handoff. The same strncpy truncated at MAX_HEADER_LEN *bytes*, which can split a codepoint or a color code. * @clist/headers was missing the engine's pad to column 79. Column layout in the module now goes through StripTabsAndTruncate/PadField rather than printf field widths. A printf precision counts codepoints and knows nothing about PUA color; StripTabsAndTruncate carries a byte limit and a column limit as a pair and reserves budget for the closing color sequence. My first version of this fix hand-rolled the column stops with %-14.13s and verified byte-identical output against the engine -- for ASCII only, which is exactly the blind spot that produced two of the three findings above. #1649 covers the general problem; nothing here waits on it. @cwho deliberately reproduces the engine's composition including strip_color(): @cwho discards color so its width arithmetic is honest while @clist two functions away preserves it. Both are coping strategies for the primitive #1649 proposes, and this file's job is parity, not unilaterally improving one side into a fresh divergence. mux_IObjectInfo gains UnparseObject: the visibility rule is Examinable() plus the CHOWN_OK/JUMP_OK/LINK_OK/DESTROY_OK/ABODE exceptions, which a module cannot compute from GetFlags/DecodeFlags. Caller-supplied buffer, matching AtrGet, so nothing crosses the DLL boundary needing to be freed on the far side. CID_ObjectInfo is UseSameProcess only, so there is no proxy/stub to update. New harness, tests/comsys_cmdparity: one command stream, two fresh databases, output diffed. The two existing comsys harnesses compare state handoff and delivery hooks; neither compares plain command output, and both scored green against all seven of these. Fixtures are deliberately colored and CJK. Verified it goes red: reinstating the engine's PadField conflation fails case 3 while cases 1 and 2 still pass, which is the ASCII blind spot reproduced on demand. Windows Server 2022, MSVC 14.51, Release x64: three parity streams identical; comsys_handoff 13/13; comsys_mogrify 5/5; smoke 1555 succeeded / 5 failed / 0 crashes, unchanged, the five being the pre-existing no-OpenSSL gap (#1641). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:31:58 -06:00
MUX_RESULT CObjectInfo::UnparseObject(dbref player, dbref target,
bool bObeyMyopic, UTF8 *pBuf, size_t nBufMax)
{
if ( nullptr == pBuf
|| 0 == nBufMax)
{
return MUX_E_INVALIDARG;
}
pBuf[0] = '\0';
UTF8 *pName = unparse_object(player, target, bObeyMyopic);
if (nullptr == pName)
{
return MUX_E_FAIL;
}
mux_strncpy(pBuf, pName, nBufMax - 1);
free_lbuf(pName);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::IsWizard(dbref obj, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = Wizard(obj) ? true : false;
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::IsWizRoy(dbref obj, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = WizRoy(obj) ? true : false;
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::CanIdle(dbref obj, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = Can_Idle(obj) ? true : false;
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::WizardWho(dbref obj, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = Wizard_Who(obj) ? true : false;
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::SeeHidden(dbref obj, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = See_Hidden(obj) ? true : false;
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::AtrAddRaw(dbref obj, int attrnum, const UTF8 *value)
{
// Privileged raw write (no bCanSetAttr). Trusted callers only: driver
// lifecycle and modules that must touch AF_INTERNAL attrs mail_mod cannot
// reach through IAttributeAccess (#1229). Untrusted softcode must use
// IAttributeAccess instead.
//
if (!Good_obj(obj))
{
return MUX_E_INVALIDARG;
}
atr_add_raw(obj, attrnum, value);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::AtrClr(dbref obj, int attrnum)
{
if (!Good_obj(obj))
{
return MUX_E_INVALIDARG;
}
atr_clr(obj, attrnum);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::AtrGet(dbref obj, int attrnum, UTF8 *pValue,
size_t nValueMax, dbref *pOwner, int *pFlags)
{
if ( nullptr == pValue
|| nullptr == pOwner
|| nullptr == pFlags
|| 0 == nValueMax)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pValue = '\0';
return MUX_E_INVALIDARG;
}
LBuf p = LBuf_Adopt(atr_get("com_bridge", obj, attrnum, pOwner, pFlags));
mux_strncpy(pValue, p, nValueMax - 1);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::AtrPGet(dbref obj, int attrnum, UTF8 *pValue,
size_t nValueMax, dbref *pOwner, int *pFlags)
{
if ( nullptr == pValue
|| nullptr == pOwner
|| nullptr == pFlags
|| 0 == nValueMax)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pValue = '\0';
return MUX_E_INVALIDARG;
}
LBuf p = LBuf_Adopt(atr_pget(obj, attrnum, pOwner, pFlags));
mux_strncpy(pValue, p, nValueMax - 1);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::LookupPlayer(dbref executor, const UTF8 *pName,
bool bConnected, dbref *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (nullptr == pName)
{
*pResult = NOTHING;
return MUX_E_INVALIDARG;
}
*pResult = lookup_player(executor, pName, bConnected);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::FetchConnectionInfoFields(dbref player,
int64_t anFields[4])
{
if (!Good_obj(player))
{
return MUX_E_INVALIDARG;
}
fetch_ConnectionInfoFields(player, anFields);
return MUX_S_OK;
}
MUX_RESULT CObjectInfo::PutConnectionInfoFields(dbref player,
int64_t anFields[4], CLinearTimeAbsolute &ltaNow)
{
if (!Good_obj(player))
{
return MUX_E_INVALIDARG;
}
put_ConnectionInfoFields(player, anFields, ltaNow);
return MUX_S_OK;
}
CObjectInfoFactory::CObjectInfoFactory(void) : m_cRef(1)
{
}
CObjectInfoFactory::~CObjectInfoFactory()
{
}
MUX_RESULT CObjectInfoFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CObjectInfoFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CObjectInfoFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CObjectInfoFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CObjectInfo *pObjectInfo = nullptr;
try
{
pObjectInfo = new CObjectInfo;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pObjectInfo)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pObjectInfo->QueryInterface(iid, ppv);
pObjectInfo->Release();
return mr;
}
MUX_RESULT CObjectInfoFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CAttributeAccess — mux_IAttributeAccess implementation (in-process only).
// ---------------------------------------------------------------------------
class CAttributeAccess : public mux_IAttributeAccess
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT GetAttribute(dbref executor, dbref obj,
const UTF8 *pAttrName, UTF8 *pValue, size_t nValueMax,
size_t *pnValueLen);
virtual MUX_RESULT SetAttribute(dbref executor, dbref obj,
const UTF8 *pAttrName, const UTF8 *pValue);
CAttributeAccess(void);
virtual ~CAttributeAccess();
private:
uint32_t m_cRef;
};
CAttributeAccess::CAttributeAccess(void) : m_cRef(1)
{
}
CAttributeAccess::~CAttributeAccess()
{
}
MUX_RESULT CAttributeAccess::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IAttributeAccess *>(this);
}
else if (IID_IAttributeAccess == iid)
{
*ppv = static_cast<mux_IAttributeAccess *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CAttributeAccess::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CAttributeAccess::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CAttributeAccess::GetAttribute(dbref executor, dbref obj,
const UTF8 *pAttrName, UTF8 *pValue, size_t nValueMax,
size_t *pnValueLen)
{
if (nullptr == pValue || 0 == nValueMax || nullptr == pAttrName)
{
return MUX_E_INVALIDARG;
}
pValue[0] = '\0';
if (nullptr != pnValueLen)
{
*pnValueLen = 0;
}
if (!Good_obj(obj))
{
return MUX_E_INVALIDARG;
}
ATTR *pattr = atr_str(pAttrName);
if (nullptr == pattr)
{
return MUX_E_NOTFOUND;
}
if (!bCanReadAttr(executor, obj, pattr, false))
{
return MUX_E_PERMISSION;
}
dbref aowner;
int aflags;
size_t nLen;
LBuf buf = LBuf_Src("GetFormatCmd");
atr_get_str_LEN(buf, obj, pattr->number, &aowner, &aflags, &nLen);
if (0 == nLen)
{
return MUX_S_OK;
}
if (nLen >= nValueMax)
{
nLen = nValueMax - 1;
}
memcpy(pValue, buf, nLen);
pValue[nLen] = '\0';
if (nullptr != pnValueLen)
{
*pnValueLen = nLen;
}
return MUX_S_OK;
}
MUX_RESULT CAttributeAccess::SetAttribute(dbref executor, dbref obj,
const UTF8 *pAttrName, const UTF8 *pValue)
{
if (!Good_obj(obj) || nullptr == pAttrName)
{
return MUX_E_INVALIDARG;
}
// mkattr() looks up or creates the vattr as needed.
//
int anum = mkattr(executor, pAttrName);
if (anum <= 0)
{
return MUX_E_NOTFOUND;
}
ATTR *pattr = atr_num(anum);
if (nullptr == pattr)
{
return MUX_E_NOTFOUND;
}
if (!bCanSetAttr(executor, obj, pattr))
{
return MUX_E_PERMISSION;
}
dbref aowner;
int aflags;
atr_pget_info(obj, pattr->number, &aowner, &aflags);
atr_add(obj, pattr->number, pValue, Owner(executor), aflags);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CAttributeAccessFactory
// ---------------------------------------------------------------------------
CAttributeAccessFactory::CAttributeAccessFactory(void) : m_cRef(1)
{
}
CAttributeAccessFactory::~CAttributeAccessFactory()
{
}
MUX_RESULT CAttributeAccessFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CAttributeAccessFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CAttributeAccessFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CAttributeAccessFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CAttributeAccess *pAttributeAccess = nullptr;
try
{
pAttributeAccess = new CAttributeAccess;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pAttributeAccess)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pAttributeAccess->QueryInterface(iid, ppv);
pAttributeAccess->Release();
return mr;
}
MUX_RESULT CAttributeAccessFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CEvaluator — mux_IEvaluator implementation (in-process only).
// ---------------------------------------------------------------------------
class CEvaluator : public mux_IEvaluator
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT Eval(dbref executor, dbref caller, dbref enactor,
const UTF8 *pExpr, UTF8 *pResult, size_t nResultMax,
size_t *pnResultLen);
virtual MUX_RESULT EvalWithArgs(dbref executor, dbref caller, dbref enactor,
const UTF8 *pExpr, const UTF8 *args[], int nargs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen);
CEvaluator(void);
virtual ~CEvaluator();
private:
uint32_t m_cRef;
};
CEvaluator::CEvaluator(void) : m_cRef(1)
{
}
CEvaluator::~CEvaluator()
{
}
MUX_RESULT CEvaluator::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IEvaluator *>(this);
}
else if (IID_IEvaluator == iid)
{
*ppv = static_cast<mux_IEvaluator *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CEvaluator::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CEvaluator::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CEvaluator::Eval(dbref executor, dbref caller, dbref enactor,
const UTF8 *pExpr, UTF8 *pResult, size_t nResultMax,
size_t *pnResultLen)
{
if (nullptr == pExpr || nullptr == pResult || 0 == nResultMax)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(executor))
{
pResult[0] = '\0';
if (nullptr != pnResultLen)
{
*pnResultLen = 0;
}
return MUX_E_INVALIDARG;
}
// Use the caller/enactor if valid, otherwise default to executor.
//
if (!Good_obj(caller))
{
caller = executor;
}
if (!Good_obj(enactor))
{
enactor = executor;
}
// Evaluate into an LBUF, then copy to the caller's buffer.
//
LBuf buf = LBuf_Src("EvalExpr");
UTF8 *bufc = buf;
size_t nExpr = strlen((const char *)pExpr);
mux_exec(pExpr, nExpr, buf, &bufc, executor, caller, enactor,
EV_FCHECK | EV_STRIP_CURLY | EV_EVAL, nullptr, 0);
*bufc = '\0';
size_t nLen = bufc - buf;
if (nLen >= nResultMax)
{
nLen = nResultMax - 1;
}
memcpy(pResult, buf, nLen);
pResult[nLen] = '\0';
if (nullptr != pnResultLen)
{
*pnResultLen = nLen;
}
return MUX_S_OK;
}
MUX_RESULT CEvaluator::EvalWithArgs(dbref executor, dbref caller, dbref enactor,
const UTF8 *pExpr, const UTF8 *args[], int nargs,
UTF8 *pResult, size_t nResultMax, size_t *pnResultLen)
{
if (nullptr == pExpr || nullptr == pResult || 0 == nResultMax)
{
return MUX_E_INVALIDARG;
}
if (nargs < 0 || (nargs > 0 && nullptr == args))
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(executor))
{
pResult[0] = '\0';
if (nullptr != pnResultLen)
{
*pnResultLen = 0;
}
return MUX_E_INVALIDARG;
}
if (!Good_obj(caller))
{
caller = executor;
}
if (!Good_obj(enactor))
{
enactor = executor;
}
// Match call_mogrifier: EV_FCHECK|EV_EVAL|EV_TOP with %0..%n-1.
//
LBuf buf = LBuf_Src("EvalWithArgs");
UTF8 *bufc = buf;
size_t nExpr = strlen(reinterpret_cast<const char *>(pExpr));
mux_exec(pExpr, nExpr, buf, &bufc, executor, caller, enactor,
EV_FCHECK | EV_EVAL | EV_TOP, args, nargs);
*bufc = '\0';
size_t nLen = bufc - buf;
if (nLen >= nResultMax)
{
nLen = nResultMax - 1;
}
memcpy(pResult, buf, nLen);
pResult[nLen] = '\0';
if (nullptr != pnResultLen)
{
*pnResultLen = nLen;
}
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CEvaluatorFactory
// ---------------------------------------------------------------------------
CEvaluatorFactory::CEvaluatorFactory(void) : m_cRef(1)
{
}
CEvaluatorFactory::~CEvaluatorFactory()
{
}
MUX_RESULT CEvaluatorFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CEvaluatorFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CEvaluatorFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CEvaluatorFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CEvaluator *pEvaluator = nullptr;
try
{
pEvaluator = new CEvaluator;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pEvaluator)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pEvaluator->QueryInterface(iid, ppv);
pEvaluator->Release();
return mr;
}
MUX_RESULT CEvaluatorFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CPermissions — mux_IPermissions implementation (in-process only).
// ---------------------------------------------------------------------------
class CPermissions : public mux_IPermissions
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT IsWizard(dbref obj, bool *pWizard);
virtual MUX_RESULT IsGod(dbref obj, bool *pGod);
virtual MUX_RESULT HasControl(dbref who, dbref what, bool *pControls);
virtual MUX_RESULT HasCommAll(dbref obj, bool *pCommAll);
virtual MUX_RESULT CouldDoit(dbref who, dbref what, int atr, bool *pResult);
CPermissions(void);
virtual ~CPermissions();
private:
uint32_t m_cRef;
};
CPermissions::CPermissions(void) : m_cRef(1)
{
}
CPermissions::~CPermissions()
{
}
MUX_RESULT CPermissions::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IPermissions *>(this);
}
else if (IID_IPermissions == iid)
{
*ppv = static_cast<mux_IPermissions *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CPermissions::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CPermissions::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CPermissions::IsWizard(dbref obj, bool *pWizard)
{
if (nullptr == pWizard)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pWizard = false;
return MUX_E_INVALIDARG;
}
*pWizard = Wizard(obj);
return MUX_S_OK;
}
MUX_RESULT CPermissions::IsGod(dbref obj, bool *pGod)
{
if (nullptr == pGod)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pGod = false;
return MUX_E_INVALIDARG;
}
*pGod = God(obj);
return MUX_S_OK;
}
MUX_RESULT CPermissions::HasControl(dbref who, dbref what, bool *pControls)
{
if (nullptr == pControls)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(who) || !Good_obj(what))
{
*pControls = false;
return MUX_E_INVALIDARG;
}
*pControls = Controls(who, what);
return MUX_S_OK;
}
MUX_RESULT CPermissions::HasCommAll(dbref obj, bool *pCommAll)
{
if (nullptr == pCommAll)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(obj))
{
*pCommAll = false;
return MUX_E_INVALIDARG;
}
*pCommAll = Comm_All(obj);
return MUX_S_OK;
}
MUX_RESULT CPermissions::CouldDoit(dbref who, dbref what, int atr, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(who) || !Good_obj(what))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = could_doit(who, what, atr);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
feat(modules): GAME_CONFIG -- game-policy config reaches modules (#1654) The parking condition on #1654 was "a second consumer"; the tally reached three, all documented divergences: searchcost mail non-wizard self @mail/stats was free eval_comtitle comsys comtitles always evaluated under the module money_name_* mail the charge-refusal text could not be composed ## Shape A new engine-registered class rather than an Initialize signature change: mux_IGameConfig::GetGameConfig(GAME_CONFIG *) CID_GameConfig No existing IID moves, so there is no ABI break in either direction: an old module never asks, and a new module against an old engine gets CLASSNOTAVAILABLE, soft-fails to nullptr, and keeps its prior behaviour. Two design rules baked in rather than documented and hoped for: * Queried per CALL, never cached at Initialize. A boot-time snapshot is #1613's bug -- @admin reports Set. while the module keeps stale values. @admin search_cost=50 now takes effect on the next @mail/stats. * Versioned by cbSize: caller zeroes the struct and sets cbSize; the engine fills what fits. The struct can grow without a new interface, and zero must stay a safe default for every future field. ## Both consumers, verified as a mortal The gap was wizard-invisible -- payfor() exempts wizards and every harness runs as God -- so verification used muxscript -p with a @pcreate'd mortal: search_cost 5, rich engine 150->145 module 150->145 IDENTICAL search_cost 99999, poor both: "Finding mail stats costs 99999 Pennies.", money untouched eval_comtitle default [ec] 3 Wizard says ... both sides eval_comtitle 0 [ec] [strlen(abc)] Wizard says both sides The eval_comtitle rows close the divergence documented at channel_speaker_name since #1640/#1647. Conformance and smoke are unchanged by construction (wizard runs), and were run anyway. make test: Smoke 1561 x3, conformance PASSED, handoff 13. TESTEXIT=0. Refs #1613, #1614, #1631, #1640, #1647. Closes #1654. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:35 -06:00
// CGameConfig -- mux_IGameConfig (#1654).
//
// Game-policy values a module may need, snapshotted from mudconf at call
// time. See the GAME_CONFIG comment in modules.h for the cbSize contract
// and why callers must not cache the result (#1613).
// ---------------------------------------------------------------------------
class CGameConfig : public mux_IGameConfig
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT GetGameConfig(GAME_CONFIG *pConfig);
CGameConfig(void);
virtual ~CGameConfig();
private:
uint32_t m_cRef;
};
CGameConfig::CGameConfig(void) : m_cRef(1)
{
}
CGameConfig::~CGameConfig()
{
}
MUX_RESULT CGameConfig::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IGameConfig *>(this);
}
else if (IID_IGameConfig == iid)
{
*ppv = static_cast<mux_IGameConfig *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CGameConfig::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CGameConfig::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CGameConfig::GetGameConfig(GAME_CONFIG *pConfig)
{
if ( nullptr == pConfig
|| pConfig->cbSize < sizeof(GAME_CONFIG))
{
// A larger cbSize is fine -- a newer module against this engine gets
// the fields we know and keeps its zeros for the rest. A smaller
// one means a struct from before v1, which cannot exist.
//
return MUX_E_INVALIDARG;
}
pConfig->searchcost = mudconf.searchcost;
pConfig->eval_comtitle = mudconf.eval_comtitle;
mux_strncpy(pConfig->one_coin, mudconf.one_coin,
sizeof(pConfig->one_coin) - 1);
mux_strncpy(pConfig->many_coins, mudconf.many_coins,
sizeof(pConfig->many_coins) - 1);
return MUX_S_OK;
}
// CGameConfigFactory
// ---------------------------------------------------------------------------
CGameConfigFactory::CGameConfigFactory(void) : m_cRef(1)
{
}
CGameConfigFactory::~CGameConfigFactory()
{
}
MUX_RESULT CGameConfigFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CGameConfigFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CGameConfigFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CGameConfigFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CGameConfig *pGameConfig = nullptr;
try
{
pGameConfig = new CGameConfig;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pGameConfig)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pGameConfig->QueryInterface(iid, ppv);
pGameConfig->Release();
return mr;
}
MUX_RESULT CGameConfigFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// CPermissionsFactory
// ---------------------------------------------------------------------------
CPermissionsFactory::CPermissionsFactory(void) : m_cRef(1)
{
}
CPermissionsFactory::~CPermissionsFactory()
{
}
MUX_RESULT CPermissionsFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CPermissionsFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CPermissionsFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CPermissionsFactory::CreateInstance(mux_IUnknown *pUnknownOuter, MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CPermissions *pPermissions = nullptr;
try
{
pPermissions = new CPermissions;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pPermissions)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pPermissions->QueryInterface(iid, ppv);
pPermissions->Release();
return mr;
}
MUX_RESULT CPermissionsFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CMailDelivery — server-provided implementation of mux_IMailDelivery.
//
// Wraps server-internal lock evaluation, attribute triggers, flag management,
// and throttling so that the mail module can deliver mail without linking
// against server internals.
// ---------------------------------------------------------------------------
class CMailDelivery : public mux_IMailDelivery
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT MailCheck(dbref player, dbref target, bool *pResult);
virtual MUX_RESULT NotifyDelivery(dbref sender, dbref target,
const UTF8 *subject, bool silent);
virtual MUX_RESULT IsComposing(dbref player, bool *pResult);
virtual MUX_RESULT SetComposing(dbref player, bool bComposing);
virtual MUX_RESULT ThrottleCheck(dbref player, bool *pResult);
CMailDelivery(void);
virtual ~CMailDelivery();
private:
uint32_t m_cRef;
};
CMailDelivery::CMailDelivery(void) : m_cRef(1)
{
}
CMailDelivery::~CMailDelivery()
{
}
MUX_RESULT CMailDelivery::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IMailDelivery *>(this);
}
else if (IID_IMailDelivery == iid)
{
*ppv = static_cast<mux_IMailDelivery *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CMailDelivery::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CMailDelivery::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
// MailCheck — Evaluate A_LMAIL lock in both directions.
//
// Mirrors the server's mail_check() function:
// 1. If player can't pass target's A_LMAIL, call mail_return (sends MFAIL).
// 2. If target can't pass player's A_LMAIL, reject unless player is Wizard.
//
MUX_RESULT CMailDelivery::MailCheck(dbref player, dbref target, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(player) || !Good_obj(target))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
if (!could_doit(player, target, A_LMAIL))
{
// Target rejects player's mail — send MFAIL message.
//
dbref aowner;
int aflags;
LBuf str = LBuf_Adopt(atr_pget(target, A_MFAIL, &aowner, &aflags));
if (*str)
{
LBuf str2 = LBuf_Src("mail_delivery.check");
UTF8 *bp = str2.get();
mux_exec(str, LBUF_SIZE-1, str2, &bp, target, player, player,
AttrTrace(aflags, EV_FCHECK|EV_EVAL|EV_TOP|EV_NO_LOCATION),
nullptr, 0);
*bp = '\0';
if (*str2)
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
FIELDEDTIME ft;
ltaNow.ReturnFields(&ft);
raw_notify(player, tprintf(M_("MAIL: Reject message from %s: %s"),
Moniker(target), str2.get()));
raw_notify(target, tprintf(M_("[%d:%02d] MAIL: Reject message sent to %s."),
ft.iHour, ft.iMinute, Moniker(player)));
}
}
else
{
raw_notify(player, tprintf(M_("Sorry, %s is not accepting mail."),
Moniker(target)));
}
*pResult = false;
return MUX_S_OK;
}
if (!could_doit(target, player, A_LMAIL))
{
if (Wizard(player))
{
raw_notify(player, tprintf(
M_("Warning: %s cant return your mail."),
Moniker(target)));
*pResult = true;
}
else
{
raw_notify(player, tprintf(
M_("Sorry, %s cant return your mail."),
Moniker(target)));
*pResult = false;
}
return MUX_S_OK;
}
*pResult = true;
return MUX_S_OK;
}
// NotifyDelivery — Send post-delivery notifications and trigger attributes.
//
// Called by the module after a message has been stored. This handles:
// - "You sent your message to <target>" to sender (unless silent)
// - "You have new mail from <sender>" to target
// - did_it(sender, target, A_MAIL, ..., A_AMAIL, ...) for attribute triggers
//
MUX_RESULT CMailDelivery::NotifyDelivery(dbref sender, dbref target,
const UTF8 *subject, bool silent)
{
if (!Good_obj(sender) || !Good_obj(target))
{
return MUX_E_INVALIDARG;
}
if (!silent)
{
raw_notify(sender, tprintf(M_("MAIL: You sent your message to %s."),
Moniker(target)));
}
raw_notify(target, tprintf(
M_("MAIL: You have a new message from %s. Subject: %s"),
Moniker(sender), subject));
did_it(sender, target, A_MAIL, nullptr, 0, nullptr, A_AMAIL, 0,
nullptr, NOTHING);
return MUX_S_OK;
}
// IsComposing — Check PLAYER_MAILS flag in Flags2.
//
MUX_RESULT CMailDelivery::IsComposing(dbref player, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(player))
{
*pResult = false;
return MUX_E_INVALIDARG;
}
*pResult = (Flags2(player) & PLAYER_MAILS) != 0;
return MUX_S_OK;
}
// SetComposing — Set or clear the PLAYER_MAILS flag in Flags2.
//
MUX_RESULT CMailDelivery::SetComposing(dbref player, bool bComposing)
{
if (!Good_obj(player))
{
return MUX_E_INVALIDARG;
}
if (bComposing)
{
s_Flags(player, FLAG_WORD2, Flags2(player) | PLAYER_MAILS);
}
else
{
s_Flags(player, FLAG_WORD2, Flags2(player) & ~PLAYER_MAILS);
}
return MUX_S_OK;
}
// ThrottleCheck — Has player sent too much mail recently?
//
MUX_RESULT CMailDelivery::ThrottleCheck(dbref player, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
if (!Good_obj(player))
{
*pResult = true;
return MUX_E_INVALIDARG;
}
*pResult = ThrottleMail(player);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CMailDeliveryFactory
// ---------------------------------------------------------------------------
CMailDeliveryFactory::CMailDeliveryFactory(void) : m_cRef(1)
{
}
CMailDeliveryFactory::~CMailDeliveryFactory()
{
}
MUX_RESULT CMailDeliveryFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CMailDeliveryFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CMailDeliveryFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CMailDeliveryFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CMailDelivery *pMailDelivery = nullptr;
try
{
pMailDelivery = new CMailDelivery;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pMailDelivery)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pMailDelivery->QueryInterface(iid, ppv);
pMailDelivery->Release();
return mr;
}
MUX_RESULT CMailDeliveryFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CHelpSystem — server-provided implementation of mux_IHelpSystem.
//
// Wraps the existing help.cpp lookup and indexing functions so that
// modules can access the in-game help system without linking against
// server internals.
// ---------------------------------------------------------------------------
class CHelpSystem : public mux_IHelpSystem
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT LookupTopic(dbref executor, int iHelpfile,
const UTF8 *pTopic, UTF8 *pResult, size_t nResultMax,
size_t *pnResultLen);
virtual MUX_RESULT FindHelpFile(const UTF8 *pCommandName,
int *pIndex);
virtual MUX_RESULT GetHelpFileCount(int *pCount);
virtual MUX_RESULT ReloadIndexes(dbref player);
CHelpSystem(void);
virtual ~CHelpSystem();
private:
uint32_t m_cRef;
};
CHelpSystem::CHelpSystem(void) : m_cRef(1)
{
}
CHelpSystem::~CHelpSystem()
{
}
MUX_RESULT CHelpSystem::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IHelpSystem *>(this);
}
else if (IID_IHelpSystem == iid)
{
*ppv = static_cast<mux_IHelpSystem *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CHelpSystem::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CHelpSystem::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
// LookupTopic — look up a help topic and return the rendered text.
//
// Delegates to help_helper() which does the full lookup including
// prefix matching, file reading, and optional softcode evaluation.
//
MUX_RESULT CHelpSystem::LookupTopic(dbref executor, int iHelpfile,
const UTF8 *pTopic, UTF8 *pResult, size_t nResultMax,
size_t *pnResultLen)
{
if (nullptr == pResult || nullptr == pnResultLen)
{
return MUX_E_INVALIDARG;
}
if ( iHelpfile < 0
|| mudstate.mHelpDesc <= iHelpfile
|| mudstate.nHelpDesc <= iHelpfile)
{
*pnResultLen = 0;
pResult[0] = '\0';
return MUX_E_NOTFOUND;
}
// help_helper writes into an alloc_lbuf using safe_str pattern.
//
LBuf buff = LBuf_Src("CHelpSystem.LookupTopic");
UTF8 *bufc = buff.get();
feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module interfaces take `const UTF8 * const fargs[]`. Double-const is load-bearing: C++ qualification conversion needs const at both pointer levels, so builder-side `UTF8 *[]` arrays convert implicitly — the evaluator, the JIT marshaller, and every owner site need zero casts, and slot reassignment inside bodies becomes a compile error for free. The conversions: the flip landed first so the compiler enumerated every violation; this commit is that inventory worked to zero — ~250 sites across funceval, funceval2, functions, funmath, help, mail, session, powers, levels, predicates, conf, walkdb, stringutil, timeutil/ date_scan (regenerated, one-line diff), exp3, and mux_main, each classified per docs/campaign-2136-const-fargs.md's four recipes. New idioms (functions.h): trim_space_sep_n() — non-destructive trim for (pointer, length) consumers, so trim-then-scan sites need no copy at all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers. countwords() and DecodeListOfIntegers() rewritten non-destructive. The flip deleted more than it added: #2157's fun_munge list1 copy, the engine_com help-topic copy, fun_index's in-place NUL write, and five const_casts (process_sex x4, sha1_helper). const_cast budget: zero added. Trap recorded in the brief: an old-signature definition doesn't fail the build — it becomes a C++ overload, and the new-signature symbol stays undefined until dlopen(RTLD_NOW). delim_check, the conn_bridge bridges, the dbt_spike stub, and exp3::Call were all silently shadowed; muxscript was the only host that noticed, because netmux's own net.cpp resolved the flat-namespace lookup. After any signature flip, grep the old spelling. Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and make test-scenario, including the new tests/scenario/sidefx_fargs.py that live-probes the class-3 wrappers smoke never touches (pemit/ trigger/link/tel/wipe/destroy). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00
help_helper(executor, iHelpfile, pTopic, buff, &bufc);
*bufc = '\0';
size_t nLen = bufc - buff;
if (nLen > nResultMax)
{
nLen = nResultMax;
}
memcpy(pResult, buff, nLen);
pResult[nLen] = '\0';
*pnResultLen = nLen;
return MUX_S_OK;
}
// FindHelpFile — find the help file index for a given command name.
//
MUX_RESULT CHelpSystem::FindHelpFile(const UTF8 *pCommandName,
int *pIndex)
{
if (nullptr == pCommandName || nullptr == pIndex)
{
return MUX_E_INVALIDARG;
}
for (int i = 0; i < mudstate.nHelpDesc; i++)
{
if ( nullptr != mudstate.aHelpDesc[i].CommandName
&& 0 == mux_stricmp(pCommandName,
mudstate.aHelpDesc[i].CommandName))
{
*pIndex = i;
return MUX_S_OK;
}
}
*pIndex = -1;
return MUX_E_NOTFOUND;
}
// GetHelpFileCount — return the number of registered help files.
//
MUX_RESULT CHelpSystem::GetHelpFileCount(int *pCount)
{
if (nullptr == pCount)
{
return MUX_E_INVALIDARG;
}
*pCount = mudstate.nHelpDesc;
return MUX_S_OK;
}
// ReloadIndexes — reload all help file indexes.
//
MUX_RESULT CHelpSystem::ReloadIndexes(dbref player)
{
helpindex_load(player);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CHelpSystemFactory
// ---------------------------------------------------------------------------
CHelpSystemFactory::CHelpSystemFactory(void) : m_cRef(1)
{
}
CHelpSystemFactory::~CHelpSystemFactory()
{
}
MUX_RESULT CHelpSystemFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CHelpSystemFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CHelpSystemFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CHelpSystemFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CHelpSystem *pHelpSystem = nullptr;
try
{
pHelpSystem = new CHelpSystem;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pHelpSystem)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pHelpSystem->QueryInterface(iid, ppv);
pHelpSystem->Release();
return mr;
}
MUX_RESULT CHelpSystemFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CGameEngine — in-process COM wrapper for the game engine.
//
// The driver creates this via mux_CreateInstance(CID_GameEngine) and calls
// through the mux_IGameEngine interface. In the current single-binary
// build this is a thin delegation layer; when the engine moves to
// engine.so, this class moves with it and becomes the COM front door.
// ---------------------------------------------------------------------------
class CGameEngine : public mux_IGameEngine
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT LoadGame(const UTF8 *configFile, const UTF8 *inputDb,
bool bMinDB);
virtual MUX_RESULT Startup(void);
virtual MUX_RESULT RunTasks(CLinearTimeAbsolute &ltaNow);
virtual MUX_RESULT UpdateQuotas(CLinearTimeAbsolute &ltaLast,
const CLinearTimeAbsolute &ltaCurrent);
virtual MUX_RESULT WhenNext(CLinearTimeAbsolute *pltaWhen);
virtual MUX_RESULT HasPendingUserTasks(bool *pbResult);
virtual MUX_RESULT DumpDatabase(void);
virtual MUX_RESULT Shutdown(void);
virtual MUX_RESULT DbConvert(const UTF8 *infile, const UTF8 *outfile,
const UTF8 *basename, bool bCheck, bool bLoad, bool bUnload,
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
const UTF8 *comsys_file, const UTF8 *mail_file, bool bForce);
virtual MUX_RESULT GetConfig(DRIVER_CONFIG *pConfig);
virtual MUX_RESULT MarkConnected(dbref player);
virtual MUX_RESULT DumpChildExited(int child_pid);
virtual MUX_RESULT SetStartTime(const CLinearTimeAbsolute &time);
virtual MUX_RESULT GetStartTime(CLinearTimeAbsolute *pTime);
virtual MUX_RESULT SetRestartTime(const CLinearTimeAbsolute &time);
virtual MUX_RESULT SetRestartCount(unsigned int count);
virtual MUX_RESULT GetRestartCount(unsigned int *pCount);
virtual MUX_RESULT SetCpuCountFrom(const CLinearTimeAbsolute &time);
virtual MUX_RESULT SetRecordPlayers(int count);
virtual MUX_RESULT GetDoingHdr(UTF8 *buf, size_t bufSize);
virtual MUX_RESULT SetDoingHdr(const UTF8 *hdr, size_t len);
virtual MUX_RESULT GetRecordPlayers(int *pCount);
virtual MUX_RESULT GetBCanRestart(bool *pbCanRestart);
virtual MUX_RESULT CancelTask(void (*fpTask)(void *, int),
void *arg_voidptr, int arg_Integer);
virtual MUX_RESULT DeferImmediateTask(int iPriority,
void (*fpTask)(void *, int), void *arg_voidptr,
int arg_Integer);
virtual MUX_RESULT DeferTask(const CLinearTimeAbsolute &ltWhen,
int iPriority, void (*fpTask)(void *, int),
void *arg_voidptr, int arg_Integer);
virtual MUX_RESULT PrepareForCommand(dbref player);
virtual MUX_RESULT ProcessCommand(dbref executor, dbref caller,
dbref enactor, int eval, bool bHasCmdArg, UTF8 *command,
const UTF8 *cargs[], int ncargs, UTF8 **ppLogBuf);
virtual MUX_RESULT FinishCommand(void);
virtual MUX_RESULT HaltQueue(dbref executor, dbref target);
virtual MUX_RESULT WaitQueue(dbref executor, dbref caller,
dbref enactor, int eval, bool bTimed,
const CLinearTimeAbsolute &ltaWhen, dbref sem, int attr,
UTF8 *command, int ncargs, const UTF8 *cargs[],
reg_ref *regs[], NamedRegsMap *named);
virtual MUX_RESULT MoveObject(dbref thing, dbref dest);
virtual MUX_RESULT WhereRoom(dbref what, dbref *pRoom);
virtual MUX_RESULT TimeFormat1(int seconds, size_t maxWidth,
const UTF8 **ppResult);
virtual MUX_RESULT TimeFormat2(int seconds,
const UTF8 **ppResult);
virtual MUX_RESULT GetDbTop(int *pDbTop);
virtual MUX_RESULT GetInfoTable(const UTF8 ***pppTable);
virtual MUX_RESULT Report(void);
virtual MUX_RESULT PresyncDatabaseSigsegv(void);
virtual MUX_RESULT DoRestart(dbref executor, dbref caller,
dbref enactor, int eval, int key);
virtual MUX_RESULT CacheClose(void);
CGameEngine(void);
virtual ~CGameEngine();
private:
uint32_t m_cRef;
};
CGameEngine::CGameEngine(void) : m_cRef(1)
{
}
CGameEngine::~CGameEngine()
{
}
MUX_RESULT CGameEngine::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IGameEngine *>(this);
}
else if (IID_IGameEngine == iid)
{
*ppv = static_cast<mux_IGameEngine *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CGameEngine::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CGameEngine::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
// alloc.cpp callback for @list buffers output.
//
static void engine_alloc_notify(dbref player, const UTF8 *text)
{
notify(player, text);
}
// ---------------------------------------------------------------------------
// discover_comsys_mail_modules — post-SQLite module init (#1190)
//
// Must run only after init_dbfile()/cache_init() has set g_pSQLiteBackend.
// On any failure, Release the control pointer and leave it nullptr so the
// in-tree engine comsys/mail paths stay active.
// ---------------------------------------------------------------------------
static void discover_comsys_mail_modules(void)
{
mudstate.pIComsysControl = nullptr;
mudstate.pIMailControl = nullptr;
if (nullptr == g_pSQLiteBackend)
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_text(T("Skipping comsys/mail module init: SQLite backend not open."));
ENDLOG;
return;
}
MUX_RESULT mr;
// Comsys module.
//
mr = mux_CreateInstance(CID_Comsys, nullptr, UseSameProcess,
IID_IComsysControl,
reinterpret_cast<void **>(&mudstate.pIComsysControl));
if (MUX_SUCCEEDED(mr) && nullptr != mudstate.pIComsysControl)
{
mux_IComsysStorage *pComsysStorage = nullptr;
mr = mux_CreateInstance(CID_ComsysStorage, nullptr, UseSameProcess,
IID_IComsysStorage,
reinterpret_cast<void **>(&pComsysStorage));
if (MUX_SUCCEEDED(mr) && nullptr != pComsysStorage)
{
mr = mudstate.pIComsysControl->Initialize(pComsysStorage);
pComsysStorage->Release();
pComsysStorage = nullptr;
if (MUX_SUCCEEDED(mr))
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_text(T("Comsys module initialized (via engine storage)."));
ENDLOG;
}
else
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_printf(T("Comsys module Initialize failed (mr=%d); using built-in."), mr);
ENDLOG;
mudstate.pIComsysControl->Release();
mudstate.pIComsysControl = nullptr;
}
}
else
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_printf(T("Comsys storage interface creation failed (mr=%d); using built-in."), mr);
ENDLOG;
mudstate.pIComsysControl->Release();
mudstate.pIComsysControl = nullptr;
}
}
else
{
mudstate.pIComsysControl = nullptr;
}
fix(modules): say which comsys/mail implementation is live (#1581) #1581's item 1: the silent fallback in discover_comsys_mail_modules. Both subsystems have two implementations that demonstrably disagree -- #1564 (the module never wrote HISTORY_n), #1585 (LOG_TIMESTAMPS), #1572 (four MOGRIFY hooks), #1587 (mail message size) -- and nothing said which one a server got. Now, at startup: Comsys: using module implementation. Mail: using built-in engine implementation. Both outcomes, not only the failure, because "which one am I running" is the question and a silence that means success is still a silence. ## The log call is fprintf(stderr), and that is not laziness I tried STARTLOG first, matching the failure paths already in that function. No output. Then Log.tinyprintf, matching the surrounding LoadGame code. Also no output -- and the control settles it: the neighbouring Log.tinyprintf(T("LOADING: %s" ENDLINE), mudconf.mail_db); produces nothing either, in muxscript or through the smoke harness. So there is no working log channel at this point in LoadGame, and the consequence is worth recording: **the STARTLOG failure paths already in discover_comsys_mail_modules have never been visible to anyone.** Three existing branches that report "using built-in" on module-init failure emit nothing at all. That is a second, quieter instance of the same defect #1581 is about. fprintf(stderr) is what CSQLiteDB::MigrateSchema already uses from this same phase, and it is what the harness captures. ## Answering the question #1581 could not The issue establishes Windows behaviour and says: "What I could not determine is what Linux does -- and that is the question this issue needs answered first." I cannot answer for Linux either, but macOS arm64 now reports, and the answer is configuration-driven rather than platform-driven: bare config, no module directives -> built-in engine, both subsystems module comsys_mod / module mail_mod -> module, both subsystems So the modules do NOT load by default from game/bin; they load when asked for. tools/Smoke:145-147 generates those directives, so the suite exercises the module path here -- and tools/Makesmoke does not, so the upload phase exercises the built-in. The harness has been running both implementations all along, in different phases. Note the Windows finding in #1581 predates #1569, which fixed the missing DCL_EXPORT on the module entry points -- the reason the DLLs exported nothing and could not load. Whether Windows now matches this wants re-measuring there; this change makes that a one-line check. Suite: 1560 passed / 0 failed / 320 of 320. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:21:54 -06:00
// Say which implementation is live, both ways (#1581).
//
// comsys has two implementations -- this module and the engine's built-in
// (mux/modules/engine/comsys.cpp) -- and they demonstrably disagree: the
// module never wrote HISTORY_n until #1569, and still differs on
// LOG_TIMESTAMPS (#1585) and four MOGRIFY hooks (#1572). Until now
// nothing said which one a given server got: the two inner failure paths
// logged, but "the module is not there at all" fell through in silence and
// success said nothing either.
//
// That silence is why #1564 took a multi-session investigation and why the
// same test file passed on one platform and failed on another at the
// commit that added it. One line at startup makes it a glance.
//
// fprintf(stderr), not STARTLOG or Log.tinyprintf. Neither reaches
// anything at this point in LoadGame -- I checked both, including the
// neighbouring Log.tinyprintf("LOADING: ...") calls, and none of them
// produce output the harness or an operator can see. Which also means
// the STARTLOG failure paths above this have never been visible to
// anyone. stderr is what the SQLite migration notices already use from
// this same phase, and the smoke harness captures it.
//
fprintf(stderr, "Comsys: using %s implementation.\n",
(nullptr != mudstate.pIComsysControl) ? "module" : "built-in engine");
// Mail module.
//
mr = mux_CreateInstance(CID_Mail, nullptr, UseSameProcess,
IID_IMailControl,
reinterpret_cast<void **>(&mudstate.pIMailControl));
if (MUX_SUCCEEDED(mr) && nullptr != mudstate.pIMailControl)
{
mux_IMailStorage *pMailStorage = nullptr;
mr = mux_CreateInstance(CID_MailStorage, nullptr, UseSameProcess,
IID_IMailStorage,
reinterpret_cast<void **>(&pMailStorage));
if (MUX_SUCCEEDED(mr) && nullptr != pMailStorage)
{
mr = mudstate.pIMailControl->Initialize(pMailStorage,
mudconf.mail_expiration, mudconf.mail_max_per_player);
pMailStorage->Release();
pMailStorage = nullptr;
if (MUX_SUCCEEDED(mr))
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_text(T("Mail module initialized (via engine storage)."));
ENDLOG;
}
else
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_printf(T("Mail module Initialize failed (mr=%d); using built-in."), mr);
ENDLOG;
mudstate.pIMailControl->Release();
mudstate.pIMailControl = nullptr;
}
}
else
{
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_printf(T("Mail storage interface creation failed (mr=%d); using built-in."), mr);
ENDLOG;
mudstate.pIMailControl->Release();
mudstate.pIMailControl = nullptr;
}
}
else
{
mudstate.pIMailControl = nullptr;
}
fix(modules): say which comsys/mail implementation is live (#1581) #1581's item 1: the silent fallback in discover_comsys_mail_modules. Both subsystems have two implementations that demonstrably disagree -- #1564 (the module never wrote HISTORY_n), #1585 (LOG_TIMESTAMPS), #1572 (four MOGRIFY hooks), #1587 (mail message size) -- and nothing said which one a server got. Now, at startup: Comsys: using module implementation. Mail: using built-in engine implementation. Both outcomes, not only the failure, because "which one am I running" is the question and a silence that means success is still a silence. ## The log call is fprintf(stderr), and that is not laziness I tried STARTLOG first, matching the failure paths already in that function. No output. Then Log.tinyprintf, matching the surrounding LoadGame code. Also no output -- and the control settles it: the neighbouring Log.tinyprintf(T("LOADING: %s" ENDLINE), mudconf.mail_db); produces nothing either, in muxscript or through the smoke harness. So there is no working log channel at this point in LoadGame, and the consequence is worth recording: **the STARTLOG failure paths already in discover_comsys_mail_modules have never been visible to anyone.** Three existing branches that report "using built-in" on module-init failure emit nothing at all. That is a second, quieter instance of the same defect #1581 is about. fprintf(stderr) is what CSQLiteDB::MigrateSchema already uses from this same phase, and it is what the harness captures. ## Answering the question #1581 could not The issue establishes Windows behaviour and says: "What I could not determine is what Linux does -- and that is the question this issue needs answered first." I cannot answer for Linux either, but macOS arm64 now reports, and the answer is configuration-driven rather than platform-driven: bare config, no module directives -> built-in engine, both subsystems module comsys_mod / module mail_mod -> module, both subsystems So the modules do NOT load by default from game/bin; they load when asked for. tools/Smoke:145-147 generates those directives, so the suite exercises the module path here -- and tools/Makesmoke does not, so the upload phase exercises the built-in. The harness has been running both implementations all along, in different phases. Note the Windows finding in #1581 predates #1569, which fixed the missing DCL_EXPORT on the module entry points -- the reason the DLLs exported nothing and could not load. Whether Windows now matches this wants re-measuring there; this change makes that a one-line check. Suite: 1560 passed / 0 failed / 320 of 320. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:21:54 -06:00
// Same for mail -- two implementations, and #1587 records them disagreeing
// on message size (#1581).
//
// fprintf(stderr), not STARTLOG or Log.tinyprintf. Neither reaches
// anything at this point in LoadGame -- I checked both, including the
// neighbouring Log.tinyprintf("LOADING: ...") calls, and none of them
// produce output the harness or an operator can see. Which also means
// the STARTLOG failure paths above this have never been visible to
// anyone. stderr is what the SQLite migration notices already use from
// this same phase, and the smoke harness captures it.
//
fprintf(stderr, "Mail: using %s implementation.\n",
(nullptr != mudstate.pIMailControl) ? "module" : "built-in engine");
}
MUX_RESULT CGameEngine::LoadGame(const UTF8 *configFile,
const UTF8 *inputDb, bool bMinDB)
{
UNUSED_PARAMETER(inputDb);
MUX_RESULT mr;
// Wire up alloc.cpp's output callback so @list buffers works.
//
g_alloc_notify_fn = engine_alloc_notify;
// Initialize engine-owned pools. These depend on engine types
// (BOOLEXP, BQUE) that the caller (driver or script binary)
// cannot see without including engine headers.
//
pool_init(POOL_BOOL, sizeof(BOOLEXP));
pool_init(POOL_QENTRY, sizeof(BQUE));
// Initialize engine subsystems.
//
tcache_init();
pcache_init();
cf_init();
init_cmdtab();
init_flagtab();
init_powertab();
init_functab();
init_attrtab();
// Read configuration file.
//
if (nullptr != configFile)
{
mudconf.config_file = StringClone(configFile);
}
fix(conf): make an unreadable configuration file fatal (#1601) cf_read() returns -1 when the top-level config cannot be read, and its only call site -- CGameEngine::LoadGame -- discarded that. Both callers therefore took the success branch and the game came up on compiled-in defaults. On netmux a mistyped -c path produced a live server: it bound the default port 2860 and served a two-object database while the real one sat untouched, having logged one CNF/NFND line between two INI/LOAD lines that read like success. On muxscript it exited 0 and printed "loaded game from ...", so a harness could not distinguish a green run against the intended database from one against an empty default. LoadGame now returns MUX_E_NOTFOUND, and both callers name the config file rather than reporting only "LoadGame failed (-9)" -- this is the startup failure most likely to be a simple typo. Two cases stay deliberately non-fatal. An unrecognized directive still only logs: games carry config files forward across releases, and cf_include discards cf_set()'s per-line return, which is load-bearing rather than a matching oversight. An empty file still succeeds, and is the supported way to ask for the compiled-in defaults on purpose. Also in cf_include, where fgets returns nothing: - fopen() succeeds on a directory on Linux and macOS and only the read fails, so `-c somedir` was indistinguishable from an empty file and was silently accepted. Checking ferror() separates the two. - the early return skipped the fclose the normal path does, leaking the handle on every empty include. tests/config/run.sh covers all six corners, wired in as `make test-config`. The good-config case asserts mud_name actually took effect rather than just that the process exited 0 -- exit status alone cannot tell "read the config" from "silently used defaults", which is the bug itself. Verified to fail against the unfixed engine (2 of 6, the two non-fatal cases still passing). make test green: 1560/1560 on both smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:20:21 -06:00
fix(conf): a config file that cannot be opened is fatal (#1601) cf_read() has always returned cf_include()'s -1 when the config file cannot be opened, and its only call site -- in CGameEngine::LoadGame -- discarded it. LoadGame returned MUX_S_OK, and both callers took the success branch. netmux is the serious victim: it booted and *listened*, on whatever database the compiled-in defaults name rather than the one the operator asked for. Measured before this change, `netmux -c nosuch.conf` ran until a 20-second timeout killed it. After, it exits 2 in 30ms with no listener opened. muxscript printed "loaded game from" and exited 0, so a harness could not distinguish a green run against the intended database from a green run against an empty default one; it now exits 2 and prints no such line. -1 means exactly "could not open the file". cf_include returns it only for that and for being called outside configuration reading; an unparseable *directive* inside a file that does exist returns 0 and stays non-fatal. Keeping that distinction is deliberate -- games carry old config files naming directives this build no longer knows, and refusing to boot over one would be a compatibility break, while failing to find the file at all is never what anyone wanted. Reported on stderr as well as through the log. cf_log_notfound's STARTLOG is not visible at this point in LoadGame for muxscript, which is half of why this stayed hidden -- the diagnostic existed and nobody saw it. Both callers already tested MUX_FAILED and refused, so propagating the failure was the only missing link; neither caller needed changing. netmux -c nosuch.conf before: rc=124 (still listening at 20s) after: rc=2 in 30ms, no listener muxscript -c nosuch.conf before: rc=0, "loaded game from" after: rc=2, no such line muxscript -c p.conf unchanged: rc=0, "loaded game from" Negative control: with the return value discarded again, netmux boots on the nonexistent config and keeps running. make test green, both smoke routes 1560/1560. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:17:01 -06:00
// A config file that cannot be opened is fatal (#1601).
fix(conf): make an unreadable configuration file fatal (#1601) cf_read() returns -1 when the top-level config cannot be read, and its only call site -- CGameEngine::LoadGame -- discarded that. Both callers therefore took the success branch and the game came up on compiled-in defaults. On netmux a mistyped -c path produced a live server: it bound the default port 2860 and served a two-object database while the real one sat untouched, having logged one CNF/NFND line between two INI/LOAD lines that read like success. On muxscript it exited 0 and printed "loaded game from ...", so a harness could not distinguish a green run against the intended database from one against an empty default. LoadGame now returns MUX_E_NOTFOUND, and both callers name the config file rather than reporting only "LoadGame failed (-9)" -- this is the startup failure most likely to be a simple typo. Two cases stay deliberately non-fatal. An unrecognized directive still only logs: games carry config files forward across releases, and cf_include discards cf_set()'s per-line return, which is load-bearing rather than a matching oversight. An empty file still succeeds, and is the supported way to ask for the compiled-in defaults on purpose. Also in cf_include, where fgets returns nothing: - fopen() succeeds on a directory on Linux and macOS and only the read fails, so `-c somedir` was indistinguishable from an empty file and was silently accepted. Checking ferror() separates the two. - the early return skipped the fclose the normal path does, leaking the handle on every empty include. tests/config/run.sh covers all six corners, wired in as `make test-config`. The good-config case asserts mud_name actually took effect rather than just that the process exited 0 -- exit status alone cannot tell "read the config" from "silently used defaults", which is the bug itself. Verified to fail against the unfixed engine (2 of 6, the two non-fatal cases still passing). make test green: 1560/1560 on both smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:20:21 -06:00
//
// cf_read() returns -1 when the top-level configuration file could not
// be opened (or, after the ferror check in cf_include, when a path that
// fopen accepts cannot be read -- e.g. a directory on Linux/macOS).
// Errors in individual directives take a different route: cf_include
// calls cf_set() per line and discards its return, so a config carrying
// a directive this build no longer recognizes still reads as success.
// That asymmetry is deliberate -- games carry config files forward across
// releases, and refusing to boot over one stale line would be worse than
// logging it. An empty regular file also succeeds and is the supported
// way to ask for compiled-in defaults on purpose.
fix(conf): make an unreadable configuration file fatal (#1601) cf_read() returns -1 when the top-level config cannot be read, and its only call site -- CGameEngine::LoadGame -- discarded that. Both callers therefore took the success branch and the game came up on compiled-in defaults. On netmux a mistyped -c path produced a live server: it bound the default port 2860 and served a two-object database while the real one sat untouched, having logged one CNF/NFND line between two INI/LOAD lines that read like success. On muxscript it exited 0 and printed "loaded game from ...", so a harness could not distinguish a green run against the intended database from one against an empty default. LoadGame now returns MUX_E_NOTFOUND, and both callers name the config file rather than reporting only "LoadGame failed (-9)" -- this is the startup failure most likely to be a simple typo. Two cases stay deliberately non-fatal. An unrecognized directive still only logs: games carry config files forward across releases, and cf_include discards cf_set()'s per-line return, which is load-bearing rather than a matching oversight. An empty file still succeeds, and is the supported way to ask for the compiled-in defaults on purpose. Also in cf_include, where fgets returns nothing: - fopen() succeeds on a directory on Linux and macOS and only the read fails, so `-c somedir` was indistinguishable from an empty file and was silently accepted. Checking ferror() separates the two. - the early return skipped the fclose the normal path does, leaking the handle on every empty include. tests/config/run.sh covers all six corners, wired in as `make test-config`. The good-config case asserts mud_name actually took effect rather than just that the process exited 0 -- exit status alone cannot tell "read the config" from "silently used defaults", which is the bug itself. Verified to fail against the unfixed engine (2 of 6, the two non-fatal cases still passing). make test green: 1560/1560 on both smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:20:21 -06:00
//
// Discarding this return made an unreadable config silently non-fatal:
// netmux booted and *listened* on compiled-in defaults; muxscript printed
// "loaded game from" and exited 0. Callers already refuse on MUX_FAILED.
fix(conf): a config file that cannot be opened is fatal (#1601) cf_read() has always returned cf_include()'s -1 when the config file cannot be opened, and its only call site -- in CGameEngine::LoadGame -- discarded it. LoadGame returned MUX_S_OK, and both callers took the success branch. netmux is the serious victim: it booted and *listened*, on whatever database the compiled-in defaults name rather than the one the operator asked for. Measured before this change, `netmux -c nosuch.conf` ran until a 20-second timeout killed it. After, it exits 2 in 30ms with no listener opened. muxscript printed "loaded game from" and exited 0, so a harness could not distinguish a green run against the intended database from a green run against an empty default one; it now exits 2 and prints no such line. -1 means exactly "could not open the file". cf_include returns it only for that and for being called outside configuration reading; an unparseable *directive* inside a file that does exist returns 0 and stays non-fatal. Keeping that distinction is deliberate -- games carry old config files naming directives this build no longer knows, and refusing to boot over one would be a compatibility break, while failing to find the file at all is never what anyone wanted. Reported on stderr as well as through the log. cf_log_notfound's STARTLOG is not visible at this point in LoadGame for muxscript, which is half of why this stayed hidden -- the diagnostic existed and nobody saw it. Both callers already tested MUX_FAILED and refused, so propagating the failure was the only missing link; neither caller needed changing. netmux -c nosuch.conf before: rc=124 (still listening at 20s) after: rc=2 in 30ms, no listener muxscript -c nosuch.conf before: rc=0, "loaded game from" after: rc=2, no such line muxscript -c p.conf unchanged: rc=0, "loaded game from" Negative control: with the return value discarded again, netmux boots on the nonexistent config and keeps running. make test green, both smoke routes 1560/1560. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:17:01 -06:00
//
// stderr: STARTLOG is not visible at this phase under muxscript, which is
// half of why the defect stayed hidden. Callers also name the file.
fix(conf): make an unreadable configuration file fatal (#1601) cf_read() returns -1 when the top-level config cannot be read, and its only call site -- CGameEngine::LoadGame -- discarded that. Both callers therefore took the success branch and the game came up on compiled-in defaults. On netmux a mistyped -c path produced a live server: it bound the default port 2860 and served a two-object database while the real one sat untouched, having logged one CNF/NFND line between two INI/LOAD lines that read like success. On muxscript it exited 0 and printed "loaded game from ...", so a harness could not distinguish a green run against the intended database from one against an empty default. LoadGame now returns MUX_E_NOTFOUND, and both callers name the config file rather than reporting only "LoadGame failed (-9)" -- this is the startup failure most likely to be a simple typo. Two cases stay deliberately non-fatal. An unrecognized directive still only logs: games carry config files forward across releases, and cf_include discards cf_set()'s per-line return, which is load-bearing rather than a matching oversight. An empty file still succeeds, and is the supported way to ask for the compiled-in defaults on purpose. Also in cf_include, where fgets returns nothing: - fopen() succeeds on a directory on Linux and macOS and only the read fails, so `-c somedir` was indistinguishable from an empty file and was silently accepted. Checking ferror() separates the two. - the early return skipped the fclose the normal path does, leaking the handle on every empty include. tests/config/run.sh covers all six corners, wired in as `make test-config`. The good-config case asserts mud_name actually took effect rather than just that the process exited 0 -- exit status alone cannot tell "read the config" from "silently used defaults", which is the bug itself. Verified to fail against the unfixed engine (2 of 6, the two non-fatal cases still passing). make test green: 1560/1560 on both smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:20:21 -06:00
//
if (0 != cf_read())
{
fix(conf): a config file that cannot be opened is fatal (#1601) cf_read() has always returned cf_include()'s -1 when the config file cannot be opened, and its only call site -- in CGameEngine::LoadGame -- discarded it. LoadGame returned MUX_S_OK, and both callers took the success branch. netmux is the serious victim: it booted and *listened*, on whatever database the compiled-in defaults name rather than the one the operator asked for. Measured before this change, `netmux -c nosuch.conf` ran until a 20-second timeout killed it. After, it exits 2 in 30ms with no listener opened. muxscript printed "loaded game from" and exited 0, so a harness could not distinguish a green run against the intended database from a green run against an empty default one; it now exits 2 and prints no such line. -1 means exactly "could not open the file". cf_include returns it only for that and for being called outside configuration reading; an unparseable *directive* inside a file that does exist returns 0 and stays non-fatal. Keeping that distinction is deliberate -- games carry old config files naming directives this build no longer knows, and refusing to boot over one would be a compatibility break, while failing to find the file at all is never what anyone wanted. Reported on stderr as well as through the log. cf_log_notfound's STARTLOG is not visible at this point in LoadGame for muxscript, which is half of why this stayed hidden -- the diagnostic existed and nobody saw it. Both callers already tested MUX_FAILED and refused, so propagating the failure was the only missing link; neither caller needed changing. netmux -c nosuch.conf before: rc=124 (still listening at 20s) after: rc=2 in 30ms, no listener muxscript -c nosuch.conf before: rc=0, "loaded game from" after: rc=2, no such line muxscript -c p.conf unchanged: rc=0, "loaded game from" Negative control: with the return value discarded again, netmux boots on the nonexistent config and keeps running. make test green, both smoke routes 1560/1560. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:17:01 -06:00
fprintf(stderr,
"FATAL: cannot read configuration file '%s' -- refusing to start "
"on compiled-in defaults.\n",
fix(conf): a config file that cannot be opened is fatal (#1601) cf_read() has always returned cf_include()'s -1 when the config file cannot be opened, and its only call site -- in CGameEngine::LoadGame -- discarded it. LoadGame returned MUX_S_OK, and both callers took the success branch. netmux is the serious victim: it booted and *listened*, on whatever database the compiled-in defaults name rather than the one the operator asked for. Measured before this change, `netmux -c nosuch.conf` ran until a 20-second timeout killed it. After, it exits 2 in 30ms with no listener opened. muxscript printed "loaded game from" and exited 0, so a harness could not distinguish a green run against the intended database from a green run against an empty default one; it now exits 2 and prints no such line. -1 means exactly "could not open the file". cf_include returns it only for that and for being called outside configuration reading; an unparseable *directive* inside a file that does exist returns 0 and stays non-fatal. Keeping that distinction is deliberate -- games carry old config files naming directives this build no longer knows, and refusing to boot over one would be a compatibility break, while failing to find the file at all is never what anyone wanted. Reported on stderr as well as through the log. cf_log_notfound's STARTLOG is not visible at this point in LoadGame for muxscript, which is half of why this stayed hidden -- the diagnostic existed and nobody saw it. Both callers already tested MUX_FAILED and refused, so propagating the failure was the only missing link; neither caller needed changing. netmux -c nosuch.conf before: rc=124 (still listening at 20s) after: rc=2 in 30ms, no listener muxscript -c nosuch.conf before: rc=0, "loaded game from" after: rc=2, no such line muxscript -c p.conf unchanged: rc=0, "loaded game from" Negative control: with the return value discarded again, netmux boots on the nonexistent config and keeps running. make test green, both smoke routes 1560/1560. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:17:01 -06:00
reinterpret_cast<const char *>(mudconf.config_file));
fix(conf): make an unreadable configuration file fatal (#1601) cf_read() returns -1 when the top-level config cannot be read, and its only call site -- CGameEngine::LoadGame -- discarded that. Both callers therefore took the success branch and the game came up on compiled-in defaults. On netmux a mistyped -c path produced a live server: it bound the default port 2860 and served a two-object database while the real one sat untouched, having logged one CNF/NFND line between two INI/LOAD lines that read like success. On muxscript it exited 0 and printed "loaded game from ...", so a harness could not distinguish a green run against the intended database from one against an empty default. LoadGame now returns MUX_E_NOTFOUND, and both callers name the config file rather than reporting only "LoadGame failed (-9)" -- this is the startup failure most likely to be a simple typo. Two cases stay deliberately non-fatal. An unrecognized directive still only logs: games carry config files forward across releases, and cf_include discards cf_set()'s per-line return, which is load-bearing rather than a matching oversight. An empty file still succeeds, and is the supported way to ask for the compiled-in defaults on purpose. Also in cf_include, where fgets returns nothing: - fopen() succeeds on a directory on Linux and macOS and only the read fails, so `-c somedir` was indistinguishable from an empty file and was silently accepted. Checking ferror() separates the two. - the early return skipped the fclose the normal path does, leaking the handle on every empty include. tests/config/run.sh covers all six corners, wired in as `make test-config`. The good-config case asserts mud_name actually took effect rather than just that the process exited 0 -- exit status alone cannot tell "read the config" from "silently used defaults", which is the bug itself. Verified to fail against the unfixed engine (2 of 6, the two non-fatal cases still passing). make test green: 1560/1560 on both smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:20:21 -06:00
STARTLOG(LOG_ALWAYS, "CNF", "LOAD");
log_printf(T("Fatal: configuration file '%s' could not be read."),
mudconf.config_file);
ENDLOG;
return MUX_E_NOTFOUND;
}
// Optional gettext domain for server notifies (#1419). Softcode #-1
// diagnostics use S_() and stay English. Locale packs live under
// game/locale/<lang>/LC_MESSAGES/tinymux.mo (cwd is typically game/).
// Path is not a player message — do not run it through T()/gettext.
//
feat(nls): `language` in netmux.conf, and one catalogue reader everywhere (#1702) Selecting a language was the one server-wide setting that did not live in netmux.conf, and the documented way to do it did not work on every platform. LANGUAGE=ko ./bin/netmux # mux/po/README.md libintl honours LANGUAGE. The built-in reader -- the MSVC path, since the Windows SDK ships no <libintl.h> -- reads only LC_ALL, LC_MESSAGES and LANG, so on Windows that selected nothing and the server ran English with no diagnostic. Two readers, two answers, one documented recipe. ## `language <catalogue>` language ko names game/locale/ko/LC_MESSAGES/tinymux.mo. Empty (every existing config) keeps the environment behaviour exactly. It is passed to mux_nls_init() as a parameter rather than exported into the environment, so selection cannot mean different things on different platforms. ## One reader libintl is no longer used for lookup: mux_nls.cpp does not include <libintl.h> and calls neither gettext nor ngettext. The built-in MO reader serves every platform. That removes gettext's rule that the C/POSIX locale suppresses translation outright. Via libintl, `language ko` in a bare service environment could only print "cannot take effect" and continue in English; opening a catalogue by path has no such rule, so it now simply works: LANG=C LC_ALL=C, language ko -> kowidget을(를) 사물 #12(으)로 만들었습니다 The format is unchanged -- .po/.mo, msgfmt, xgettext, msgmerge, Plural-Forms. Only the runtime is ours now, so a translator's workflow is untouched. ## Plural-Forms, which is what libintl was really providing The built-in reader returned English plural forms regardless of catalogue, so Windows already got "1 vs many" for all 18 msgid_plural entries -- wrong for ko, which declares nplurals=1. Making it the only reader meant implementing the rule properly: a recursive-descent evaluator over the grammar gettext uses (?: || && == != < > <= >= + - * / % ! and parens, over n). Total and bounded by construction: no allocation, division and modulo by zero yield 0, an unparseable rule or an out-of-range form falls back to form 0. tests/nls/test_plural.cpp covers it directly -- 43 cases over en, ko, fr, ru, pl and ar rules plus div-by-zero, unbalanced parens, truncated ternaries, garbage and out-of-range forms. Reaching plural_eval() means including mux_nls.cpp, the same way tests/dbt/test_interp.cpp reaches its file-static mem_check. Wired as `make test-nls-plural`. Mutation-checked rather than assumed: flipping % to / in the evaluator fails 13 cases across ru/pl/ar while en and ko still pass, so the suite measures the thing it is named after. ## A regression the matrix caught Dropping libintl dropped its LANGUAGE support, and mo_language() had never read that variable -- so LANGUAGE=ko, the recipe in mux/po/README.md, would have silently stopped working on Unix. mo_language() now reads gettext's documented order (LANGUAGE, LC_ALL, LC_MESSAGES, LANG) and takes the first entry of a colon list, which also gives Windows LANGUAGE support it never had. Verified, each row distinguishing a fix from a no-op: language ko, no LANGUAGE Korean <- the case that failed no directive, nothing set English <- no regression no directive, LANGUAGE=ko Korean <- env still works no directive, LANGUAGE=ko:fr Korean <- priority list language ko vs LANGUAGE=xx Korean <- directive outranks env language ko under LANG=C Korean <- impossible via libintl tests/nls/run.sh gains the directive cases (7 total, from 5); confirmed both fail when the directive is disabled while the pre-existing cases pass. make test: Smoke 1561 x3, tests/nls 7 cases, ko 4 cases, plural 43 cases. TESTEXIT=0. Documented in wizhelp (& LANGUAGE) and mux/po/README.md. Per-player locale is deliberately not addressed; the parameter form leaves room for it without committing to a design. Refs #1419, #1444, #1473, #1523, #1580, #1622, #1702. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 13:04:47 -06:00
// mudconf.language is populated: this runs after the configuration file
// has been read (#1702). Empty means follow the environment.
//
mux_nls_init(reinterpret_cast<const UTF8 *>("locale"), mudconf.language);
mux_nls_refresh_messages();
// Sync function aliases into JIT lookup table now that config
// (including alias.conf) has been processed.
engine_api_sync_aliases();
// Sync core-layer globals from mudconf after config is loaded.
//
g_float_precision = mudconf.float_precision;
g_space_compress = mudstate.bStandAlone || mudconf.space_compress;
// Comsys/mail modules are discovered after SQLite is open (#1190).
// Leaving the control pointers null keeps the built-in paths active
// until discover_comsys_mail_modules() runs at the end of LoadGame.
//
mudstate.pIComsysControl = nullptr;
mudstate.pIMailControl = nullptr;
// Try to discover the Lua scripting module.
//
mudstate.pILuaControl = nullptr;
mr = mux_CreateInstance(CID_LuaMod, nullptr, UseSameProcess,
IID_ILuaControl,
reinterpret_cast<void **>(&mudstate.pILuaControl));
if (MUX_SUCCEEDED(mr))
{
mudstate.pILuaControl->SetLimits(
mudconf.lua_instruction_limit,
mudconf.lua_memory_limit);
STARTLOG(LOG_ALWAYS, "INI", "MOD");
log_text(T("Lua scripting module discovered."));
ENDLOG;
}
mr = mux_CreateInstance(CID_QueryServer, nullptr, UseSlaveProcess,
IID_IQueryControl,
(void **)&mudstate.pIQueryControl);
if (MUX_SUCCEEDED(mr))
{
mr = mudstate.pIQueryControl->Connect(mudconf.sql_server,
mudconf.sql_database, mudconf.sql_user, mudconf.sql_password);
if (MUX_SUCCEEDED(mr))
{
mux_IQuerySink *pIQuerySink = nullptr;
mr = mux_CreateInstance(CID_QueryClient, nullptr, UseSameProcess,
IID_IQuerySink,
(void **)&pIQuerySink);
if (MUX_SUCCEEDED(mr))
{
mr = mudstate.pIQueryControl->Advise(pIQuerySink);
if (MUX_SUCCEEDED(mr))
{
pIQuerySink->Release();
pIQuerySink = nullptr;
}
else
{
mudstate.pIQueryControl->Release();
mudstate.pIQueryControl = nullptr;
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_printf(T("Couldnt connect sink to server (%d)."), mr);
ENDLOG;
}
}
else
{
mudstate.pIQueryControl->Release();
mudstate.pIQueryControl = nullptr;
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_printf(T("Couldnt create Query Sink (%d)."), mr);
ENDLOG;
}
}
else
{
mudstate.pIQueryControl->Release();
mudstate.pIQueryControl = nullptr;
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_printf(T("Couldnt connect to Query Server (%d)."), mr);
ENDLOG;
}
}
else
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_text(T("Couldnt create interface to Query Server."));
ENDLOG;
}
#if defined(INLINESQL)
init_sql();
#endif // INLINESQL
fcache_init();
helpindex_init();
// PlayerSession interface is now created by the driver after LoadGame.
// Open or create the SQLite database.
//
if (bMinDB)
{
// Remove the SQLite database to start fresh.
//
fix(engine): bound the .sqlite path derivation (#1411) Two sites derived the ".sqlite" sibling of mudconf.indb by open-coding the same replace-or-append with strcpy/strcat, neither checking the remaining capacity: mux/modules/engine/engine_com.cpp (bMinDB RemoveFile) SIZEOF_PATHNAME mux/modules/engine/attrcache.cpp (cache_init) LBUF_SIZE input_database is cf_string_dyn with a maximum of SIZEOF_PATHNAME, so its content can be SIZEOF_PATHNAME-1 characters. ".sqlite" is seven more plus a terminator, so the engine_com buffer overflows in both branches: the append writes eight bytes past a full buffer, and even the ".db" branch that replaces three characters with seven needs four bytes it may not have. attrcache was safe only because indb is capped well below LBUF_SIZE -- an implicit cross-module invariant, not a check. Replaced with one bounds-checked derive_sqlite_path() in sqlite_backend, which both TUs already include. It returns false and leaves the buffer untouched when the result will not fit; callers log and take a defined path -- cache_init returns HF_OPEN_STATUS_ERROR after releasing the backend it had just allocated, and the bMinDB case skips the removal, since a path that cannot be formed names no file to remove. No new abort: the failure is reported, not asserted. Behaviour is unchanged for every input that fits, including the edge cases the old `n > 3` test produced -- ".db" appends rather than replaces, "a.dbx" and "db" append -- so this is not a silent change of the derived name. Verified with ASan on a standalone harness carrying both versions, an input_database at exactly the configured maximum: old, no .db suffix heap-buffer-overflow, WRITE of size 8 old, .db suffix heap-buffer-overflow, WRITE of size 8 new, both returns false, no diagnostic and the normal cases: netmux.db -> netmux.sqlite, netmux -> netmux.sqlite, data/netmux.db -> data/netmux.sqlite, plus the exact-fit boundary accepted and one byte over rejected. smoke 1489/1489 on both routes, 0 crashes, 314/314 dispatched. Closes #1411.
2026-07-26 13:59:30 -06:00
// Bounds-checked: input_database may be SIZEOF_PATHNAME-1 long,
// and ".sqlite" needs seven more bytes plus a terminator, so the
// old strcpy/strcat pair wrote past this buffer (#1411). If it
// does not fit there is nothing to remove under that name, so
// say so and carry on rather than starting from a bad path.
//
char sqlitefile[SIZEOF_PATHNAME];
fix(engine): bound the .sqlite path derivation (#1411) Two sites derived the ".sqlite" sibling of mudconf.indb by open-coding the same replace-or-append with strcpy/strcat, neither checking the remaining capacity: mux/modules/engine/engine_com.cpp (bMinDB RemoveFile) SIZEOF_PATHNAME mux/modules/engine/attrcache.cpp (cache_init) LBUF_SIZE input_database is cf_string_dyn with a maximum of SIZEOF_PATHNAME, so its content can be SIZEOF_PATHNAME-1 characters. ".sqlite" is seven more plus a terminator, so the engine_com buffer overflows in both branches: the append writes eight bytes past a full buffer, and even the ".db" branch that replaces three characters with seven needs four bytes it may not have. attrcache was safe only because indb is capped well below LBUF_SIZE -- an implicit cross-module invariant, not a check. Replaced with one bounds-checked derive_sqlite_path() in sqlite_backend, which both TUs already include. It returns false and leaves the buffer untouched when the result will not fit; callers log and take a defined path -- cache_init returns HF_OPEN_STATUS_ERROR after releasing the backend it had just allocated, and the bMinDB case skips the removal, since a path that cannot be formed names no file to remove. No new abort: the failure is reported, not asserted. Behaviour is unchanged for every input that fits, including the edge cases the old `n > 3` test produced -- ".db" appends rather than replaces, "a.dbx" and "db" append -- so this is not a silent change of the derived name. Verified with ASan on a standalone harness carrying both versions, an input_database at exactly the configured maximum: old, no .db suffix heap-buffer-overflow, WRITE of size 8 old, .db suffix heap-buffer-overflow, WRITE of size 8 new, both returns false, no diagnostic and the normal cases: netmux.db -> netmux.sqlite, netmux -> netmux.sqlite, data/netmux.db -> data/netmux.sqlite, plus the exact-fit boundary accepted and one byte over rejected. smoke 1489/1489 on both routes, 0 crashes, 314/314 dispatched. Closes #1411.
2026-07-26 13:59:30 -06:00
if (derive_sqlite_path(sqlitefile, sizeof(sqlitefile), mudconf.indb))
{
fix(engine): bound the .sqlite path derivation (#1411) Two sites derived the ".sqlite" sibling of mudconf.indb by open-coding the same replace-or-append with strcpy/strcat, neither checking the remaining capacity: mux/modules/engine/engine_com.cpp (bMinDB RemoveFile) SIZEOF_PATHNAME mux/modules/engine/attrcache.cpp (cache_init) LBUF_SIZE input_database is cf_string_dyn with a maximum of SIZEOF_PATHNAME, so its content can be SIZEOF_PATHNAME-1 characters. ".sqlite" is seven more plus a terminator, so the engine_com buffer overflows in both branches: the append writes eight bytes past a full buffer, and even the ".db" branch that replaces three characters with seven needs four bytes it may not have. attrcache was safe only because indb is capped well below LBUF_SIZE -- an implicit cross-module invariant, not a check. Replaced with one bounds-checked derive_sqlite_path() in sqlite_backend, which both TUs already include. It returns false and leaves the buffer untouched when the result will not fit; callers log and take a defined path -- cache_init returns HF_OPEN_STATUS_ERROR after releasing the backend it had just allocated, and the bMinDB case skips the removal, since a path that cannot be formed names no file to remove. No new abort: the failure is reported, not asserted. Behaviour is unchanged for every input that fits, including the edge cases the old `n > 3` test produced -- ".db" appends rather than replaces, "a.dbx" and "db" append -- so this is not a silent change of the derived name. Verified with ASan on a standalone harness carrying both versions, an input_database at exactly the configured maximum: old, no .db suffix heap-buffer-overflow, WRITE of size 8 old, .db suffix heap-buffer-overflow, WRITE of size 8 new, both returns false, no diagnostic and the normal cases: netmux.db -> netmux.sqlite, netmux -> netmux.sqlite, data/netmux.db -> data/netmux.sqlite, plus the exact-fit boundary accepted and one byte over rejected. smoke 1489/1489 on both routes, 0 crashes, 314/314 dispatched. Closes #1411.
2026-07-26 13:59:30 -06:00
RemoveFile((UTF8 *)sqlitefile);
}
else
{
fix(engine): bound the .sqlite path derivation (#1411) Two sites derived the ".sqlite" sibling of mudconf.indb by open-coding the same replace-or-append with strcpy/strcat, neither checking the remaining capacity: mux/modules/engine/engine_com.cpp (bMinDB RemoveFile) SIZEOF_PATHNAME mux/modules/engine/attrcache.cpp (cache_init) LBUF_SIZE input_database is cf_string_dyn with a maximum of SIZEOF_PATHNAME, so its content can be SIZEOF_PATHNAME-1 characters. ".sqlite" is seven more plus a terminator, so the engine_com buffer overflows in both branches: the append writes eight bytes past a full buffer, and even the ".db" branch that replaces three characters with seven needs four bytes it may not have. attrcache was safe only because indb is capped well below LBUF_SIZE -- an implicit cross-module invariant, not a check. Replaced with one bounds-checked derive_sqlite_path() in sqlite_backend, which both TUs already include. It returns false and leaves the buffer untouched when the result will not fit; callers log and take a defined path -- cache_init returns HF_OPEN_STATUS_ERROR after releasing the backend it had just allocated, and the bMinDB case skips the removal, since a path that cannot be formed names no file to remove. No new abort: the failure is reported, not asserted. Behaviour is unchanged for every input that fits, including the edge cases the old `n > 3` test produced -- ".db" appends rather than replaces, "a.dbx" and "db" append -- so this is not a silent change of the derived name. Verified with ASan on a standalone harness carrying both versions, an input_database at exactly the configured maximum: old, no .db suffix heap-buffer-overflow, WRITE of size 8 old, .db suffix heap-buffer-overflow, WRITE of size 8 new, both returns false, no diagnostic and the normal cases: netmux.db -> netmux.sqlite, netmux -> netmux.sqlite, data/netmux.db -> data/netmux.sqlite, plus the exact-fit boundary accepted and one byte over rejected. smoke 1489/1489 on both routes, 0 crashes, 314/314 dispatched. Closes #1411.
2026-07-26 13:59:30 -06:00
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_text(T("input_database too long to derive a .sqlite path; not removed."));
ENDLOG;
}
}
int ccPageFile = init_dbfile(mudconf.indb);
if (HF_OPEN_STATUS_ERROR == ccPageFile)
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD");
log_text(T("Couldnt open storage backend."));
ENDLOG;
return MUX_E_FAIL;
}
mudstate.record_players = 0;
bool bLoadedGameFromSQLite = false;
if (bMinDB)
{
if (!db_make_minimal())
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("Failed to build minimal database."));
ENDLOG
return MUX_E_FAIL;
}
}
else
{
bool bDoFlatfileLoad = true;
if (HF_OPEN_STATUS_OLD == ccPageFile)
{
int sqlite_load_rc = sqlite_load_game();
if (sqlite_load_rc > 0)
{
// Warm start: loaded everything from SQLite.
// No flatfile needed.
//
bDoFlatfileLoad = false;
bLoadedGameFromSQLite = true;
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
// Make the precedence visible: a populated SQLite database
// shadows the configured flatfile, so a freshly dropped-in
// flatfile would otherwise appear to be silently ignored.
//
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("Warm-started from SQLite database; the flatfile "));
log_text(mudconf.indb);
dbload: clear comsys/mail on a forced game load (#783) A flatfile game load replaces the database's game, but the comsys and mail tables were only cleared when -C/-m were passed -- so "db_load -f netmux new.flat" produced a database whose channels and mail belonged to the PREVIOUS game, referencing dbrefs in the new one. This scenario was impossible before -f existed (the only supported path was deleting the whole .sqlite, which removed comsys/mail too). Clear both table sets after any flatfile game load; the -C/-m imports that follow repopulate them when flatfiles are supplied (verified: export + forced reload with -C/-m round-trips channels and mail). Adjacent fixes from the same review: - The "already contains a game" refusal fired on ANY pre-existing file (HF_OPEN_STATUS_OLD only means the file existed) -- including the empty shell that running db_unload against a never-imported game creates as a side effect. Probe the db_top metadata and only refuse when the file actually holds a game; loading into an empty shell proceeds without -f. - db_load now accepts -f anywhere among the options, not just as the first argument. - The every-warm-boot log line advising "Remove the SQLite database to reload from a flatfile" invited destroying all changes since the seed flatfile (the .sqlite is authoritative; the flatfile is never refreshed). It now recommends exporting with db_unload first, then db_load -f. Verified end-to-end: refusal with a real game and no -f; forced load clears channels/channel_users/mail_bodies/mail_headers and the new game's marker replaces the old one; empty-shell load proceeds without -f; -C/-m repopulate after the clear. smoke 1115/0/0, 200 jit_diff clean, tests/libmux 51/0. Closes #783. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:33 -06:00
log_text(T(" was not consulted. The SQLite database is authoritative; to rebase"
" on a flatfile, export current state with db_unload first, then db_load -f."));
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
ENDLOG
}
else if (sqlite_load_rc < 0)
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("SQLite warm-load failed."));
ENDLOG
return MUX_E_FAIL;
}
}
int ccInFile = LOAD_GAME_SUCCESS;
if (bDoFlatfileLoad)
{
ccInFile = load_game(ccPageFile);
}
if (LOAD_GAME_NO_INPUT_DB == ccInFile)
{
// The input file didn't exist.
//
if (HF_OPEN_STATUS_NEW == ccPageFile)
{
// Since the .db file didn't exist, and the .pag/.dir files
// were newly created, just create a minimal DB.
//
if (!db_make_minimal())
{
ccInFile = LOAD_GAME_LOADING_PROBLEM;
}
else
{
ccInFile = LOAD_GAME_SUCCESS;
}
}
}
if (ccInFile != LOAD_GAME_SUCCESS)
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("Couldnt load: "));
log_text(mudconf.indb);
ENDLOG
return MUX_E_FAIL;
}
}
// Repair any out-of-range object field dbrefs from the loaded database
// (#810). No-op for a valid DB; prevents OOB at use (e.g. DOLIST chain
// walks) for a corrupt/malicious one. Covers both the warm (SQLite) and
// cold (flatfile) load paths, which both leave db[] populated here.
//
db_validate_refs();
// Warm-start path skips load_game(); explicitly load aux SQLite/flatfile
// subsystems here.
//
if (bLoadedGameFromSQLite)
{
int load_comsys_rc = sqlite_load_comsys();
if (load_comsys_rc < 0)
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("SQLite comsys load failed."));
ENDLOG
return MUX_E_FAIL;
}
if (0 == load_comsys_rc)
{
load_comsys(mudconf.comsys_db);
}
int load_mail_rc = sqlite_load_mail();
if (load_mail_rc < 0)
{
STARTLOG(LOG_ALWAYS, "INI", "LOAD")
log_text(T("SQLite mail load failed."));
ENDLOG
return MUX_E_FAIL;
}
if (0 == load_mail_rc)
{
FILE *f;
if (mux_fopen(&f, mudconf.mail_db, T("rb")))
{
setvbuf(f, nullptr, _IOFBF, 16384);
Log.tinyprintf(T("LOADING: %s" ENDLINE), mudconf.mail_db);
load_mail(f);
Log.tinyprintf(T("LOADING: %s (done)" ENDLINE),
mudconf.mail_db);
mux_fclose(f);
}
}
}
// #1190: Discover comsys/mail modules only after SQLite is open and
// game + aux data are loaded. Failure clears the control pointers so
// the built-in engine paths remain authoritative.
//
discover_comsys_mail_modules();
return MUX_S_OK;
}
MUX_RESULT CGameEngine::Startup(void)
{
// Set up the COM bridge so engine-side code can reach the driver's
// connection manager through free-function stubs.
//
conn_bridge_init();
Guest.StartUp();
// Do a consistency check and set up the freelist.
//
do_dbck(NOTHING, NOTHING, NOTHING, 0, 0);
route_init();
walk_init();
ValidateConfigurationDbrefs();
process_preload();
local_startup();
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->startup();
p = p->pNext;
}
init_timer();
return MUX_S_OK;
}
MUX_RESULT CGameEngine::RunTasks(CLinearTimeAbsolute &ltaNow)
{
scheduler.RunTasks(ltaNow);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::UpdateQuotas(CLinearTimeAbsolute &ltaLast,
const CLinearTimeAbsolute &ltaCurrent)
{
update_quotas(ltaLast, ltaCurrent);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::WhenNext(CLinearTimeAbsolute *pltaWhen)
{
if (nullptr == pltaWhen)
{
return MUX_E_INVALIDARG;
}
// Propagate whether a next task exists. When the scheduler is empty,
// scheduler.WhenNext() returns false and leaves *pltaWhen untouched, so
// we must report failure rather than success-with-garbage. Callers
// (muxscript's loop, the netmux idle loop) branch on MUX_FAILED to mean
// "no task scheduled."
//
if (!scheduler.WhenNext(pltaWhen))
{
return MUX_E_NOTFOUND;
}
return MUX_S_OK;
}
MUX_RESULT CGameEngine::HasPendingUserTasks(bool *pbResult)
{
if (nullptr == pbResult)
{
return MUX_E_INVALIDARG;
}
*pbResult = scheduler.HasPendingUserTasks();
return MUX_S_OK;
}
MUX_RESULT CGameEngine::DumpDatabase(void)
{
dump_database();
return MUX_S_OK;
}
#if defined(TINYMUX_JIT)
extern void dbt_compile_cleanup(void);
#endif
MUX_RESULT CGameEngine::Shutdown(void)
{
#if defined(TINYMUX_JIT)
dbt_compile_cleanup();
#endif
walk_shutdown();
route_shutdown();
conn_bridge_final();
local_shutdown();
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->shutdown();
p = p->pNext;
}
return MUX_S_OK;
}
static void safe_copy_str(UTF8 *dst, size_t dstSize, const UTF8 *src)
{
if (nullptr != src)
{
mux_strncpy(dst, src, dstSize - 1);
}
else
{
dst[0] = '\0';
}
}
MUX_RESULT CGameEngine::GetConfig(DRIVER_CONFIG *pConfig)
{
if (nullptr == pConfig)
{
return MUX_E_INVALIDARG;
}
memset(pConfig, 0, sizeof(DRIVER_CONFIG));
// Ports and networking.
//
pConfig->nPorts = static_cast<int>(mudconf.ports.size());
if (pConfig->nPorts > DRIVER_CONFIG_MAX_PORTS)
{
pConfig->nPorts = DRIVER_CONFIG_MAX_PORTS;
}
for (int i = 0; i < pConfig->nPorts; i++)
{
pConfig->ports[i] = mudconf.ports[i];
}
#if defined(UNIX_SSL) || defined(_WIN32)
pConfig->nSslPorts = static_cast<int>(mudconf.sslPorts.size());
if (pConfig->nSslPorts > DRIVER_CONFIG_MAX_PORTS)
{
pConfig->nSslPorts = DRIVER_CONFIG_MAX_PORTS;
}
for (int i = 0; i < pConfig->nSslPorts; i++)
{
pConfig->sslPorts[i] = mudconf.sslPorts[i];
}
mux_strncpy(pConfig->ssl_certificate_file, mudconf.ssl_certificate_file, sizeof(pConfig->ssl_certificate_file) - 1);
mux_strncpy(pConfig->ssl_certificate_key, mudconf.ssl_certificate_key, sizeof(pConfig->ssl_certificate_key) - 1);
mux_strncpy(pConfig->ssl_certificate_password, mudconf.ssl_certificate_password, sizeof(pConfig->ssl_certificate_password) - 1);
#else
pConfig->nSslPorts = 0;
#endif
safe_copy_str(pConfig->ip_address, sizeof(pConfig->ip_address), mudconf.ip_address);
pConfig->use_hostname = mudconf.use_hostname;
pConfig->retry_limit = mudconf.retry_limit;
feat(net): per-source failed-login throttle retry_limit (3) is per-SOCKET: after three bad passwords the connection closes and the attacker reconnects for three more. Nothing remembered anything across connections, so brute-force-by-reconnect was unbounded. Add that memory as a token bucket per source address, consumed only by failed connect attempts: login_fail_limit default 10, 0 = off -- burst of failed logins per source login_fail_period default 60 seconds -- interval over which it refills Sustained rate is limit/period, so the default is 10/minute against a previously unlimited rate. Checked in check_connect BEFORE ConnectPlayer, so a throttled source also stops costing a password hash per guess. Guests are exempt (fixed password, separately bounded by the guest pool). On refusal the socket is left open and retries_left untouched -- the attempt never reached a password check, so it is not a failed login -- and conn_timeout still reaps an idle one. Two shapes of this defense are actively harmful in a MUSH and are not used: * Per-ACCOUNT lockout. Player names are public (WHO, in-game, the directory), so anyone could lock any player -- including a wizard -- out of their own game by spamming failures at their name. That trades a brute-force risk for a guaranteed griefing tool. * A delay before answering a failed login. This server is single-threaded; sleeping to slow one attacker stops the world for every other player. The throttle must be non-blocking, so it refuses rather than stalls. Keying: IPv6 by /64, not by address. One IPv6 customer normally holds a whole /64, so a single host can source 2**64 addresses -- keying on the full address would let one attacker both evade the throttle and flood the table with single-use entries. mux_sockaddr::source_key() returns the 4-byte v4 address or the 8-byte v6 /64 prefix; differing lengths keep the families from colliding. The table must not become the resource it protects: a fixed 512-slot array scanned linearly, no allocation and no growth, consulted only on login attempts (already bounded by max_preauth_per_site). When full, eviction takes the LEAST suspicious entry (fullest bucket, oldest as tie-break) so table pressure never costs us the record of an active attacker. The dorm/NAT cost is real and deliberate: an exhausted bucket briefly refuses legitimate players from a shared address, including ones typing the correct password. It is bounded (continuous refill; seconds, not a lockout), the default is generous relative to how often real players mistype, and admins can widen or disable it. There is deliberately no "this source already has an authenticated session" exemption -- it would read as dorm-friendly while handing a full bypass to an existing player going after someone else's account. Verified on a live netmux at limit 3 / period 600s: guesses 1-3 rejected normally, 4-6 refused with the wait message, each from a FRESH connection -- reconnecting no longer buys a fresh batch. At limit 5 / period 20s the budget demonstrably refills. Both the new CON/THR line and the earlier NET/SITE pre-auth line confirmed to emit. Regression: smoke 1319/1319, stress 8/8, netaddr 46/46 (+7 source_key tests), ganl 14/14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:46:41 -06:00
pConfig->login_fail_limit = mudconf.login_fail_limit;
pConfig->login_fail_period = mudconf.login_fail_period;
feat(net): per-source connection-rate limit (connect/disconnect churn) The Rhost item our own follow-up list skipped, and the vector was genuinely uncovered. max_preauth_sitecons bounds how many pre-auth connections are held AT ONCE; login_fail_limit bounds FAILED LOGINS. An attacker who connects and immediately disconnects -- never logging in, never failing a login -- is touched by neither, while every cycle still costs an accept, a DESC, the welcome screen's file dump, the site checks and a log line. max_lastsite_cnt default 40, 0 = off -- connections per source per window min_con_attempt default 60 seconds -- window over which it refills Both names are Rhost's. Checked in the GANL accept path before the pre-auth cap (it is the cheaper test). Only ACCEPTED connections are charged: a refused attempt costs the attacker nothing extra, but charging it would hold a shared address at zero for as long as one attacker kept trying, starving the legitimate users behind it of the refill. Two deliberate differences from Rhost: * The response is a transient refusal, not their lastsite_paranoia auto-register/auto-forbid. A permanent sitelock earned by a burst is precisely the wrong answer on a shared address: one abuser in a dorm would lock out the whole building until an admin undid it by hand. A refusal that heals as the bucket refills costs a legitimate player seconds. Admins wanting the permanent form already have forbid_site, now with graduated thresholds. We therefore ship no lastsite_paranoia knob at all rather than one whose values mean something different from theirs. * The counting is a true per-source bucket. Rhost keeps a single "last site" slot, so only CONSECUTIVE connections from one address count and one interleaved connection from anywhere else resets the counter -- trivially walked around by alternating two addresses. Keyed per source (v6 by /64), ours cannot be reset that way. Default is 40/60 rather than Rhost's 20/60 because ours counts strictly harder AND is on by default where theirs is gated behind lastsite_paranoia 0. 40/60 still cuts churn from unlimited to 40/minute -- three orders of magnitude -- while clearing the burst that matters: a whole dorm reconnecting after a reboot. Verified 30 connect-and-login cycles from one address at the shipping default: 30 accepted, 0 refused. The failed-login bucket was generalized rather than copied: one source_bucket mechanism (fixed 512-slot table, lazy refill, least-suspicious eviction, /64 v6 keying) now backs both g_login_fail and g_connect_rate. Verified at max_lastsite_cnt 10 / min_con_attempt 60 with the pre-auth cap and login-fail throttle both DISABLED -- proving this defense alone catches it -- pure connect/disconnect churn ran 11 cycles then was refused, NET/RATE damped by nospam_connect to one line. At 5/10s, 5 of 12 rapid cycles refused and a connection 8s later succeeded. Regression at the shipping default: smoke 1319/1319, stress 8/8, netaddr 57/57, ganl 14/14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 13:43:52 -06:00
pConfig->max_lastsite_cnt = mudconf.max_lastsite_cnt;
pConfig->min_con_attempt = mudconf.min_con_attempt;
feat(net): damp connection-refusal logging (nospam_connect); adopt Rhost naming Surveyed RhostMUSH at ce5226ff for prior art on connection/login DoS defenses. Two results change this work. 1. Refusal logging is itself a DoS vector, and we had missed it. Refusing a connection still costs a log write, so a flood we successfully refuse fills a disk instead. Rhost's nospam_connect exists solely for this and their help text names it outright ("Real twinkish players may try multiple connects to overload a log file"). Both of our new defenses -- the pre-auth cap and the failed-login throttle -- logged every single refusal, converting a connection flood into a disk flood. Adopt their parameter by name and semantics: nospam_connect 0 = log every refusal 1 = log the first of a consecutive run from one address, then one summary line when the run ends (default) 2 = do not log refusals Two deliberate differences from Rhost: * Default 1, not their 0. The collapse loses no signal -- first line plus an exact count -- so there is no reason to ship the hole open. * Flushed on the periodic idle sweep, not only when a run ends. Rhost's refusals happen at accept, so an accepted connection ends the run. Ours also refuse at LOGIN, and every login attempt arrives on a freshly accepted socket -- flushing on accept would end the run before every single refusal and defeat the damping entirely. Flushing on the sweep also bounds refusal logging by TIME rather than by the attacker's rate, which is the property actually wanted: a sustained attacker produces neither a different address nor a successful login, so the count would otherwise sit unreported for the whole attack. State is one address slot plus a counter, as Rhost does it -- the anti-flood measure cannot itself be flooded. 2. Match Rhost's configuration vocabulary where the knob is the same thing. nospam_connect is adopted verbatim. max_preauth_per_site is renamed to max_preauth_sitecons to sit in Rhost's max_sitecons family -- but deliberately NOT named max_sitecons, because theirs caps ALL connections from a site and ours caps only unauthenticated ones. An identical name would be a false friend that reads as configured while behaving differently. The survey also validates two earlier decisions: Rhost has no per-account lockout anywhere and no per-IP failed-login throttle either, and every Rhost auto-generated ACL entry keys IPv6 on the full /128 address against a list with no cap or expiry -- exactly the evade-and-flood hole our /64 source_key avoids. Verified: 9 consecutive refusals collapse to 1 full line plus a periodic summary ("[127.0.0.1] Connection refused [total 4 more times]."), and the renamed knob still refuses correctly. Regression: smoke 1319/1319, stress 8/8, netaddr 46/46, ganl 14/14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:20:11 -06:00
pConfig->nospam_connect = mudconf.nospam_connect;
pConfig->idle_timeout = mudconf.idle_timeout;
pConfig->conn_timeout = mudconf.conn_timeout;
pConfig->cmd_quota_max = mudconf.cmd_quota_max;
pConfig->output_limit = mudconf.output_limit;
feat(net): own input_size in save_command + anti-runaway input backlog cap Hardening: the per-connection pending-input backlog (docs/survey- resource-defenses.md). Two parts, informed by measurement. Correctness (the real fix): save_command now owns d->input_size, incrementing it per enqueued line by the exact byte length it queues. Previously telnet.cpp incremented it in a batch (nInputBytes) while the dequeue decremented by cmd.size() — a latent drift — and the websocket save_command path never touched it at all. Ownership in the single choke point keeps the counter exactly in step with the dequeue and covers telnet + websocket uniformly. Anti-runaway backstop: new input_limit config (default 16*LBUF = 512KB, <=0 disables) caps per-connection pending-input bytes; excess is dropped with input_lost accounting and hysteresis on d->input_throttled to keep a flood from spamming the log (which would just move the amplification into logging). Deliberately a HIGH backstop, not a primary throttle: input is drop-sensitive (a dropped line silently corrupts a user's paste — a code attribute is one legit ~32KB line; @edit/multi-attribute uploads are legit bursts), unlike output. Measurement shows the app queue does not grow unbounded anyway — the single-threaded read/drain cadence + TCP flow control bound it (input_size peaked at ~44 bytes under a sustained flood with the drain throttled to command_quota 1). The proper form is read-side backpressure (stop reading when backlogged; TCP holds the excess with zero loss), deferred. Verified: two back-to-back 30KB attribute pastes store intact with zero drops; smoke 1319/1319; stress harness 8/8; server responsive throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:57:22 -06:00
pConfig->input_limit = mudconf.input_limit;
feat(net): damp connection-refusal logging (nospam_connect); adopt Rhost naming Surveyed RhostMUSH at ce5226ff for prior art on connection/login DoS defenses. Two results change this work. 1. Refusal logging is itself a DoS vector, and we had missed it. Refusing a connection still costs a log write, so a flood we successfully refuse fills a disk instead. Rhost's nospam_connect exists solely for this and their help text names it outright ("Real twinkish players may try multiple connects to overload a log file"). Both of our new defenses -- the pre-auth cap and the failed-login throttle -- logged every single refusal, converting a connection flood into a disk flood. Adopt their parameter by name and semantics: nospam_connect 0 = log every refusal 1 = log the first of a consecutive run from one address, then one summary line when the run ends (default) 2 = do not log refusals Two deliberate differences from Rhost: * Default 1, not their 0. The collapse loses no signal -- first line plus an exact count -- so there is no reason to ship the hole open. * Flushed on the periodic idle sweep, not only when a run ends. Rhost's refusals happen at accept, so an accepted connection ends the run. Ours also refuse at LOGIN, and every login attempt arrives on a freshly accepted socket -- flushing on accept would end the run before every single refusal and defeat the damping entirely. Flushing on the sweep also bounds refusal logging by TIME rather than by the attacker's rate, which is the property actually wanted: a sustained attacker produces neither a different address nor a successful login, so the count would otherwise sit unreported for the whole attack. State is one address slot plus a counter, as Rhost does it -- the anti-flood measure cannot itself be flooded. 2. Match Rhost's configuration vocabulary where the knob is the same thing. nospam_connect is adopted verbatim. max_preauth_per_site is renamed to max_preauth_sitecons to sit in Rhost's max_sitecons family -- but deliberately NOT named max_sitecons, because theirs caps ALL connections from a site and ours caps only unauthenticated ones. An identical name would be a false friend that reads as configured while behaving differently. The survey also validates two earlier decisions: Rhost has no per-account lockout anywhere and no per-IP failed-login throttle either, and every Rhost auto-generated ACL entry keys IPv6 on the full /128 address against a list with no cap or expiry -- exactly the evade-and-flood hole our /64 source_key avoids. Verified: 9 consecutive refusals collapse to 1 full line plus a periodic summary ("[127.0.0.1] Connection refused [total 4 more times]."), and the renamed knob still refuses correctly. Regression: smoke 1319/1319, stress 8/8, netaddr 46/46, ganl 14/14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:20:11 -06:00
pConfig->max_preauth_sitecons = mudconf.max_preauth_sitecons;
feat(net): proto_detect_window — make the 500ms connect wait configurable (#2193) Every classic telnet connect waited a hardcoded 500ms in total silence before the server sent its first byte. Measured over 30 connects to an idle server: min 500.6ms, p50 501.0ms, max 504.5ms -- the distribution is the grace window itself. Base command latency on the same setup is ~51us. The window exists so telnet negotiation cannot corrupt a WebSocket handshake (#1074), and the incentives are inverted: WebSocket and TLS clients speak first and are served immediately, while the classic MUD client -- which waits for the server to speak, and is the primary audience -- is the only kind that always pays in full. Now `proto_detect_window`, in milliseconds, default 500. Nothing changes for a site that does not touch it. 0 disables detection: the banner goes out at accept, as in 2.13. That is the correct setting for a port that never serves WebSocket -- it has nothing to detect and no reason to wait. On keeping 500 as the default rather than shaving it: the window is NOT covering a round trip, which is what the original issue text assumed. A WebSocket client's GET rides directly behind the handshake's final ACK, so the healthy case needs ~0ms however distant the client -- the RTT is already spent by the time the window opens. What the window must survive is that first packet being LOST, where the retransmit arrives on an RTO that Linux floors at 200ms. 500 covers one retransmit with headroom. 100 would sit in the dead zone -- past every healthy client, short of every retransmit -- and would break real WebSocket handshakes intermittently on a lossy link. A slow banner is a far better failure mode than that. The value was a literal in two places (the age-out sweep and the main-loop timeout clamp) with nothing tying them together, so they could drift and the clamp would silently stop bounding the sweep. One accessor now. 0 is expressed as arithmetic rather than a second finalize call site: with the window at 0, `age >= 0` is true on the first sweep and the clamp drives processEvents to a 0ms timeout, so the connection finalizes in the same main-loop iteration it was accepted in. A shortcut around the sweep would have had to duplicate both the #2018 exception barrier and the #1800 partial-preface replay. IID_IGameEngine bumped C9D2 -> C9D3. The vtable is unchanged, but GetConfig() memsets and fills sizeof(DRIVER_CONFIG) as the ENGINE sees it into storage the DRIVER sized, so a size disagreement is an out-of-bounds write rather than a wrong answer. tests/scenario/proto_detect.py asserts it against a live server: ok 1 - silent client waits the configured proto-detect window # 500 ms ok 2 - client that speaks first is served without waiting # 0 ms ok 3 - engine accepted the runtime @admin to 0 # config='0' ok 4 - #2193 window 0 serves a silent client at accept # 0 ms ok 5 - restoring the window restores the wait (live push, both ways) Catch-verified by reverting only the driver's USE of the knob, leaving the config plumbing intact -- the #1222 shape, where the engine reports the change and the driver ignores it until restart: ok 3 - engine accepted the runtime @admin to 0 # config='0' not ok 4 - #2193 window 0 serves a silent client at accept (500 ms) Clean rebuild, since DRIVER_CONFIG's layout changed (#2107). make test: 36 targets, 34 passed, 2 skipped (NLS), 0 failed config: jit=yes stubslave=yes nls=no realitylvls=yes wodrealms=yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:01:47 -06:00
pConfig->proto_detect_window = mudconf.proto_detect_window;
pConfig->default_charset = mudconf.default_charset;
pConfig->max_players = mudconf.max_players;
pConfig->control_flags = mudconf.control_flags;
// Guest configuration.
//
mux_strncpy(pConfig->guest_prefix, mudconf.guest_prefix, sizeof(pConfig->guest_prefix) - 1);
pConfig->number_guests = mudconf.number_guests;
pConfig->guest_char = mudconf.guest_char;
// Game identity.
//
mux_strncpy(pConfig->mud_name, mudconf.mud_name, sizeof(pConfig->mud_name) - 1);
// File paths (pointer fields copied into fixed buffers).
//
safe_copy_str(pConfig->pid_file, sizeof(pConfig->pid_file), mudconf.pid_file);
safe_copy_str(pConfig->log_dir, sizeof(pConfig->log_dir), mudconf.log_dir);
safe_copy_str(pConfig->config_file, sizeof(pConfig->config_file), mudconf.config_file);
// Messages.
//
mux_strncpy(pConfig->crash_msg, mudconf.crash_msg, sizeof(pConfig->crash_msg) - 1);
mux_strncpy(pConfig->downmotd_msg, mudconf.downmotd_msg, sizeof(pConfig->downmotd_msg) - 1);
mux_strncpy(pConfig->fullmotd_msg, mudconf.fullmotd_msg, sizeof(pConfig->fullmotd_msg) - 1);
mux_strncpy(pConfig->pueblo_msg, mudconf.pueblo_msg, sizeof(pConfig->pueblo_msg) - 1);
// Timing (raw 100ns ticks).
//
pConfig->max_cmdsecs = mudconf.max_cmdsecs.Return100ns();
pConfig->rpt_cmdsecs = mudconf.rpt_cmdsecs.Return100ns();
pConfig->timeslice = mudconf.timeslice.Return100ns();
Feature: Add MSSP, GMCP, @protect, and benchmark() Four new features identified from the TinyMUSH/PennMUSH/RhostMUSH survey: MSSP (MUD Server Status Protocol, telnet option 70): - Server sends structured key-value data (NAME, PLAYERS, UPTIME, PORT, CODEBASE, FAMILY) to MU* directory crawlers on IAC DO MSSP - Stateless response via send_mssp() in telnet.cpp using g_dc config basket - start_time_utc added to DRIVER_CONFIG for uptime calculation GMCP (Generic MUD Communication Protocol, telnet option 201): - Protocol negotiation: server offers WILL GMCP, tracks gmcp_enabled per DESC - Inbound: GMCP subneg queued as synthetic "\x01GMCP" command, dispatched to handle_gmcp() which fires A_GMCP attribute with %0=package %1=json - Outbound: gmcp(<player>, <package>, <json>) softcode function sends GMCP frames to all GMCP-enabled descriptors via SendGmcp COM method - Full COM architecture: mux_IConnectionManager::SendGmcp in driver, send_gmcp() bridge in engine, CConnectionManager impl in modules.cpp @protect (player name reservation): - @protect[/add] <name>, @protect/del <name>, @protect/list [<player>] - A_PROTECTNAME attribute (234) stores space-separated protected names - protectname_check() hooked into create_player() and do_name() - max_name_protect config param (default 5) benchmark(<expression>, <iterations>): - FN_NOEVAL, CA_PUBLIC, 10000 iteration cap - Uses clock_gettime(CLOCK_MONOTONIC) / QueryPerformanceCounter - Returns elapsed seconds as floating point Also fixes: add unicode_tables.c to libmux.so LIBMUX_C_SRC (resolves pre-existing tr_tolower_sbt etc. link errors). 505/505 smoke tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 22:08:55 -06:00
pConfig->start_time_utc = mudstate.start_time.ReturnSeconds();
// Behavior flags and limits.
//
pConfig->sig_action = mudconf.sig_action;
pConfig->fork_dump = mudconf.fork_dump;
pConfig->name_spaces = mudconf.name_spaces;
pConfig->idle_wiz_dark = mudconf.idle_wiz_dark;
pConfig->reset_players = mudconf.reset_players;
pConfig->site_chars = mudconf.site_chars;
pConfig->start_room = mudconf.start_room;
// SQL.
//
mux_strncpy(pConfig->sql_server, mudconf.sql_server, sizeof(pConfig->sql_server) - 1);
mux_strncpy(pConfig->sql_user, mudconf.sql_user, sizeof(pConfig->sql_user) - 1);
mux_strncpy(pConfig->sql_password, mudconf.sql_password, sizeof(pConfig->sql_password) - 1);
mux_strncpy(pConfig->sql_database, mudconf.sql_database, sizeof(pConfig->sql_database) - 1);
// Mail relay.
//
mux_strncpy(pConfig->mail_server, mudconf.mail_server, sizeof(pConfig->mail_server) - 1);
mux_strncpy(pConfig->mail_sendaddr, mudconf.mail_sendaddr, sizeof(pConfig->mail_sendaddr) - 1);
mux_strncpy(pConfig->mail_sendname, mudconf.mail_sendname, sizeof(pConfig->mail_sendname) - 1);
mux_strncpy(pConfig->mail_ehlo, mudconf.mail_ehlo, sizeof(pConfig->mail_ehlo) - 1);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::MarkConnected(dbref player)
{
if ( Good_obj(player)
&& isPlayer(player))
{
s_Connected(player);
return MUX_S_OK;
}
return MUX_E_INVALIDARG;
}
MUX_RESULT CGameEngine::DumpChildExited(int child_pid)
{
Rework Windows build for component-based directory layout Adapt all vcxproj files and solution to the new directory structure (src/ driver, lib/ shared library, modules/engine/ game logic, modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ loadable modules). Key changes: - libmux.dll exports utility symbols via LIBMUX_API macro (__declspec(dllexport) when BUILDING_LIBMUX, dllimport otherwise) - LIBMUX_API added to all shared headers: stringutil.h, timeutil.h, mathutil.h, utf8tables.h, svdhash.h, svdrand.h, sha1.h, alloc.h, dbutil.h, core.h - Per-file PreprocessorDefinitions in libmux.vcxproj inherit from project-level via %(PreprocessorDefinitions) - Driver factory declarations (CDriverControlFactory, CConnectionManagerFactory) guarded with BUILDING_DRIVER - PCG-XSH-RR-64/32 (pcg32) for Windows (no __int128 needed); Unix PCG-XSL-RR-128/64 unchanged - MSVC portability fixes: _strnicmp, _BitScanForward64, (std::min)(), HAVE_WORKING_FORK guards, WINDOWS_FILES/UNIX_FILES ModuleAdd paths - Remove stubslave.cpp and slave.cpp from netmux.vcxproj (separate processes) - Fix sqlproxy/sqlslave vcxproj relative paths for new layout - Add ws2_32.lib to engine.vcxproj for socket functions - Add strcasecmp/strtok_r/strndup compat shims for comsys/mail Builds successfully: libmux.dll, engine.dll, netmux.exe, exp3.dll, sqlproxy.dll, sqlslave.dll. Comsys/mail blocked on sqlite3 linking architecture (need COM-mediated or independent sqlite3 linkage). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:38:57 -06:00
#if defined(HAVE_WORKING_FORK)
if (!mudstate.dumping)
{
return MUX_S_FALSE;
}
// #1136: ignore reaps that are not the dump child once dumper is known.
// Under fork_dump the driver may report any child exit; a DNS slave or
// other helper must not clear dump state early. dumper == 0 means the
// classic race (SIGCHLD before fork() returned) — accept that child.
//
if ( 0 != mudstate.dumper
&& mudstate.dumper != static_cast<pid_t>(child_pid))
{
return MUX_S_FALSE;
}
mudstate.dumped = child_pid;
if (mudstate.dumper == mudstate.dumped)
{
// Normal completion — fork() returned before SIGCHLD.
//
mudstate.dumper = 0;
mudstate.dumped = 0;
}
else
{
// SIGCHLD arrived before fork() returned the PID.
// dumped is set; fork_and_dump will notice on return.
//
}
mudstate.dumping = false;
local_dump_complete_signal();
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->dump_complete_signal();
p = p->pNext;
}
return MUX_S_OK;
Rework Windows build for component-based directory layout Adapt all vcxproj files and solution to the new directory structure (src/ driver, lib/ shared library, modules/engine/ game logic, modules/{comsys,mail,exp3,sqlproxy,sqlslave}/ loadable modules). Key changes: - libmux.dll exports utility symbols via LIBMUX_API macro (__declspec(dllexport) when BUILDING_LIBMUX, dllimport otherwise) - LIBMUX_API added to all shared headers: stringutil.h, timeutil.h, mathutil.h, utf8tables.h, svdhash.h, svdrand.h, sha1.h, alloc.h, dbutil.h, core.h - Per-file PreprocessorDefinitions in libmux.vcxproj inherit from project-level via %(PreprocessorDefinitions) - Driver factory declarations (CDriverControlFactory, CConnectionManagerFactory) guarded with BUILDING_DRIVER - PCG-XSH-RR-64/32 (pcg32) for Windows (no __int128 needed); Unix PCG-XSL-RR-128/64 unchanged - MSVC portability fixes: _strnicmp, _BitScanForward64, (std::min)(), HAVE_WORKING_FORK guards, WINDOWS_FILES/UNIX_FILES ModuleAdd paths - Remove stubslave.cpp and slave.cpp from netmux.vcxproj (separate processes) - Fix sqlproxy/sqlslave vcxproj relative paths for new layout - Add ws2_32.lib to engine.vcxproj for socket functions - Add strcasecmp/strtok_r/strndup compat shims for comsys/mail Builds successfully: libmux.dll, engine.dll, netmux.exe, exp3.dll, sqlproxy.dll, sqlslave.dll. Comsys/mail blocked on sqlite3 linking architecture (need COM-mediated or independent sqlite3 linkage). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:38:57 -06:00
#else
UNUSED_PARAMETER(child_pid);
return MUX_S_FALSE;
#endif
}
MUX_RESULT CGameEngine::SetStartTime(const CLinearTimeAbsolute &time)
{
mudstate.start_time = time;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetStartTime(CLinearTimeAbsolute *pTime)
{
if (nullptr == pTime)
{
return MUX_E_INVALIDARG;
}
*pTime = mudstate.start_time;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::SetRestartTime(const CLinearTimeAbsolute &time)
{
mudstate.restart_time = time;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::SetRestartCount(unsigned int count)
{
mudstate.restart_count = count;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetRestartCount(unsigned int *pCount)
{
if (nullptr == pCount)
{
return MUX_E_INVALIDARG;
}
*pCount = mudstate.restart_count;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::SetCpuCountFrom(const CLinearTimeAbsolute &time)
{
mudstate.cpu_count_from = time;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::SetRecordPlayers(int count)
{
mudstate.record_players = count;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetDoingHdr(UTF8 *buf, size_t bufSize)
{
if (nullptr == buf || 0 == bufSize)
{
return MUX_E_INVALIDARG;
}
mux_strncpy(buf, mudstate.doing_hdr, bufSize - 1);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::SetDoingHdr(const UTF8 *hdr, size_t len)
{
if (nullptr == hdr)
{
return MUX_E_INVALIDARG;
}
size_t nCopy = len;
if (nCopy >= sizeof(mudstate.doing_hdr))
{
nCopy = sizeof(mudstate.doing_hdr) - 1;
}
memcpy(mudstate.doing_hdr, hdr, nCopy);
mudstate.doing_hdr[nCopy] = '\0';
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetRecordPlayers(int *pCount)
{
if (nullptr == pCount)
{
return MUX_E_INVALIDARG;
}
*pCount = mudstate.record_players;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetBCanRestart(bool *pbCanRestart)
{
if (nullptr == pbCanRestart)
{
return MUX_E_INVALIDARG;
}
*pbCanRestart = mudstate.bCanRestart;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::CancelTask(void (*fpTask)(void *, int),
void *arg_voidptr, int arg_Integer)
{
scheduler.CancelTask(fpTask, arg_voidptr, arg_Integer);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::DeferImmediateTask(int iPriority,
void (*fpTask)(void *, int), void *arg_voidptr, int arg_Integer)
{
// #1871: surface scheduler OOM/insert failure to the driver bridge.
//
if (!scheduler.DeferImmediateTask(iPriority, fpTask, arg_voidptr, arg_Integer))
{
return MUX_E_OUTOFMEMORY;
}
return MUX_S_OK;
}
MUX_RESULT CGameEngine::DeferTask(const CLinearTimeAbsolute &ltWhen,
int iPriority, void (*fpTask)(void *, int), void *arg_voidptr,
int arg_Integer)
{
if (!scheduler.DeferTask(ltWhen, iPriority, fpTask, arg_voidptr, arg_Integer))
{
return MUX_E_OUTOFMEMORY;
}
return MUX_S_OK;
}
MUX_RESULT CGameEngine::PrepareForCommand(dbref player)
{
mudstate.curr_executor = player;
mudstate.curr_enactor = player;
for (int i = 0; i < MAX_GLOBAL_REGS; i++)
{
if (mudstate.global_regs[i])
{
RegRelease(mudstate.global_regs[i]);
mudstate.global_regs[i] = nullptr;
}
}
NamedRegsClear(mudstate.named_regs);
#if defined(STUB_SLAVE)
mudstate.iRow = RS_TOP;
if (nullptr != mudstate.pResultsSet)
{
mudstate.pResultsSet->Release();
mudstate.pResultsSet = nullptr;
}
#endif // STUB_SLAVE
return MUX_S_OK;
}
MUX_RESULT CGameEngine::ProcessCommand(dbref executor, dbref caller,
dbref enactor, int eval, bool bHasCmdArg, UTF8 *command,
const UTF8 *cargs[], int ncargs, UTF8 **ppLogBuf)
{
if (nullptr == ppLogBuf)
{
return MUX_E_INVALIDARG;
}
*ppLogBuf = process_command(executor, caller, enactor, eval, bHasCmdArg,
command, cargs, ncargs);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::FinishCommand(void)
{
mudstate.curr_cmd = T("");
return MUX_S_OK;
}
MUX_RESULT CGameEngine::HaltQueue(dbref executor, dbref target)
{
halt_que(executor, target);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::WaitQueue(dbref executor, dbref caller,
dbref enactor, int eval, bool bTimed,
const CLinearTimeAbsolute &ltaWhen, dbref sem, int attr,
UTF8 *command, int ncargs, const UTF8 *cargs[],
reg_ref *regs[], NamedRegsMap *named)
{
wait_que(executor, caller, enactor, eval, bTimed, ltaWhen, sem, attr,
command, ncargs, cargs, regs, named);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::MoveObject(dbref thing, dbref dest)
{
move_object(thing, dest);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::WhereRoom(dbref what, dbref *pRoom)
{
if (nullptr == pRoom)
{
return MUX_E_INVALIDARG;
}
*pRoom = where_room(what);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::TimeFormat1(int seconds, size_t maxWidth,
const UTF8 **ppResult)
{
if (nullptr == ppResult)
{
return MUX_E_INVALIDARG;
}
*ppResult = time_format_1(seconds, maxWidth);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::TimeFormat2(int seconds,
const UTF8 **ppResult)
{
if (nullptr == ppResult)
{
return MUX_E_INVALIDARG;
}
*ppResult = time_format_2(seconds);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetDbTop(int *pDbTop)
{
if (nullptr == pDbTop)
{
return MUX_E_INVALIDARG;
}
*pDbTop = mudstate.db_top;
return MUX_S_OK;
}
MUX_RESULT CGameEngine::GetInfoTable(const UTF8 ***pppTable)
{
if (nullptr == pppTable)
{
return MUX_E_INVALIDARG;
}
*pppTable = local_get_info_table();
return MUX_S_OK;
}
MUX_RESULT CGameEngine::Report(void)
{
report();
return MUX_S_OK;
}
MUX_RESULT CGameEngine::PresyncDatabaseSigsegv(void)
{
local_presync_database_sigsegv();
// Notify all registered module sinks.
//
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->presync_database_sigsegv();
p = p->pNext;
}
return MUX_S_OK;
}
MUX_RESULT CGameEngine::DoRestart(dbref executor, dbref caller,
dbref enactor, int eval, int key)
{
do_restart(executor, caller, enactor, eval, key);
return MUX_S_OK;
}
MUX_RESULT CGameEngine::CacheClose(void)
{
cache_close();
return MUX_S_OK;
}
static void dbconvert_info(int fmt, int flags, int ver)
{
const UTF8 *cp;
if (fmt == F_MUX)
{
cp = T("MUX");
}
else
{
cp = T("*unknown*");
}
mux_fprintf(stderr, T("%s version %d:"), cp, ver);
if ( ver < MIN_SUPPORTED_VERSION
|| MAX_SUPPORTED_VERSION < ver)
{
mux_fprintf(stderr, T(" Unsupported version"));
exit(1);
}
else if ( ( ( 1 == ver
|| 2 == ver)
&& (flags & MANDFLAGS_V2) != MANDFLAGS_V2)
|| ( 3 == ver
&& (flags & MANDFLAGS_V3) != MANDFLAGS_V3)
|| ( 4 == ver
&& (flags & MANDFLAGS_V4) != MANDFLAGS_V4)
|| ( 5 == ver
&& (flags & MANDFLAGS_V5) != MANDFLAGS_V5))
{
mux_fprintf(stderr, T(" Unsupported flags"));
exit(1);
}
if (flags & V_DATABASE)
mux_fprintf(stderr, T(" Database"));
if (flags & V_ATRNAME)
mux_fprintf(stderr, T(" AtrName"));
if (flags & V_ATRKEY)
mux_fprintf(stderr, T(" AtrKey"));
if (flags & V_ATRMONEY)
mux_fprintf(stderr, T(" AtrMoney"));
mux_fprintf(stderr, T(ENDLINE));
}
MUX_RESULT CGameEngine::DbConvert(const UTF8 *infile, const UTF8 *outfile,
const UTF8 *basename, bool bCheck, bool bLoad, bool bUnload,
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
const UTF8 *comsys_file, const UTF8 *mail_file, bool bForce)
{
int setflags, clrflags, ver;
int db_ver, db_format, db_flags;
SeedRandomNumberGenerator();
pool_init(POOL_LBUF, LBUF_SIZE);
pool_init(POOL_MBUF, MBUF_SIZE);
pool_init(POOL_SBUF, SBUF_SIZE);
pool_init(POOL_BOOL, sizeof(struct boolexp));
pcache_init();
cf_init();
// dbconvert path: write-through attribute cache immediately (no
// scheduler). Cleared before return if the process continues (#1046).
//
mudstate.bStandAlone = true;
// Decide what conversions to do and how to format the output file.
//
setflags = clrflags = ver = 0;
bool do_redirect = false;
bool do_write = true;
if (bCheck || bLoad)
{
do_write = false;
}
if (bLoad)
{
clrflags = 0xffffffff;
setflags = OUTPUT_FLAGS;
ver = OUTPUT_VERSION;
do_redirect = true;
}
else if (bUnload)
{
clrflags = 0xffffffff;
setflags = UNLOAD_FLAGS;
ver = UNLOAD_VERSION;
}
// Open the database
//
init_attrtab();
int cc = init_dbfile(basename);
if (cc == HF_OPEN_STATUS_ERROR)
{
mux_fprintf(stderr, T("Cant open SQLite database.\n"));
return MUX_E_FAIL;
}
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
// Report exactly which file we opened so it is never a mystery where the
// database lives relative to the server's configured input_database.
//
const char *pDbPath = g_pSQLiteBackend->GetDB().GetPath();
mux_fprintf(stderr, T("Database file: %s\n"),
(nullptr != pDbPath && '\0' != pDbPath[0]) ? pDbPath : "(unknown)");
if (cc == HF_OPEN_STATUS_OLD)
{
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
if (setflags == OUTPUT_FLAGS && !bForce)
{
dbload: clear comsys/mail on a forced game load (#783) A flatfile game load replaces the database's game, but the comsys and mail tables were only cleared when -C/-m were passed -- so "db_load -f netmux new.flat" produced a database whose channels and mail belonged to the PREVIOUS game, referencing dbrefs in the new one. This scenario was impossible before -f existed (the only supported path was deleting the whole .sqlite, which removed comsys/mail too). Clear both table sets after any flatfile game load; the -C/-m imports that follow repopulate them when flatfiles are supplied (verified: export + forced reload with -C/-m round-trips channels and mail). Adjacent fixes from the same review: - The "already contains a game" refusal fired on ANY pre-existing file (HF_OPEN_STATUS_OLD only means the file existed) -- including the empty shell that running db_unload against a never-imported game creates as a side effect. Probe the db_top metadata and only refuse when the file actually holds a game; loading into an empty shell proceeds without -f. - db_load now accepts -f anywhere among the options, not just as the first argument. - The every-warm-boot log line advising "Remove the SQLite database to reload from a flatfile" invited destroying all changes since the seed flatfile (the .sqlite is authoritative; the flatfile is never refreshed). It now recommends exporting with db_unload first, then db_load -f. Verified end-to-end: refusal with a real game and no -f; forced load clears channels/channel_users/mail_bodies/mail_headers and the new game's marker replaces the old one; empty-shell load proceeds without -f; -C/-m repopulate after the clear. smoke 1115/0/0, 200 jit_diff clean, tests/libmux 51/0. Closes #783. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:33 -06:00
// HF_OPEN_STATUS_OLD only means the FILE existed. An empty
// shell (e.g. created as a side effect of running db_unload
// against a never-imported game) holds no game, so loading
// into it is not destructive -- don't refuse with a false
// "already contains a game" claim (#783).
dbload: mistake-proof the SQLite import path (#766) Migrating an existing game to the SQLite backend had two silent traps that both surfaced as "old passwords/characters don't work": 1. dbconvert resolves <basename>.sqlite relative to cwd, but the server reads data/<name>.sqlite. Running db_load from the wrong directory put the database where the server never looks, so old characters appeared to vanish while a fresh login still worked against the stock db. 2. A populated netmux.sqlite silently shadows the netmux.db flatfile at boot, so dropping in an old flatfile did nothing. Changes: - db_load/db_unload are now cwd-independent: the .sqlite always lands in the game's data/ dir (next to the script), file args are resolved against the caller's dir, and the scripts echo the absolute path. Arg handling is space-safe (set --) and POSIX sh. - dbconvert prints the exact database file it opened (CSQLiteDB::GetPath). - The "would overwrite" guard now names the file and gives two ways forward; a new -f/force option lets a load replace an existing db (the load already clears attributes/objects/attr-names cleanly). - The server logs a line when it warm-starts from SQLite and the flatfile was not consulted, making the precedence visible. - New docs/importing-a-game.md documents the two-file model and import steps. Verified: db_load from an unrelated dir lands the .sqlite in data/; the guard refuses without -f and replaces with -f; db_unload round-trip is byte-identical (passwords preserved). Smoke: 1078 ok / 0 new failures (TC001/TC009 pre-existing on master). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:23:12 -05:00
//
dbload: clear comsys/mail on a forced game load (#783) A flatfile game load replaces the database's game, but the comsys and mail tables were only cleared when -C/-m were passed -- so "db_load -f netmux new.flat" produced a database whose channels and mail belonged to the PREVIOUS game, referencing dbrefs in the new one. This scenario was impossible before -f existed (the only supported path was deleting the whole .sqlite, which removed comsys/mail too). Clear both table sets after any flatfile game load; the -C/-m imports that follow repopulate them when flatfiles are supplied (verified: export + forced reload with -C/-m round-trips channels and mail). Adjacent fixes from the same review: - The "already contains a game" refusal fired on ANY pre-existing file (HF_OPEN_STATUS_OLD only means the file existed) -- including the empty shell that running db_unload against a never-imported game creates as a side effect. Probe the db_top metadata and only refuse when the file actually holds a game; loading into an empty shell proceeds without -f. - db_load now accepts -f anywhere among the options, not just as the first argument. - The every-warm-boot log line advising "Remove the SQLite database to reload from a flatfile" invited destroying all changes since the seed flatfile (the .sqlite is authoritative; the flatfile is never refreshed). It now recommends exporting with db_unload first, then db_load -f. Verified end-to-end: refusal with a real game and no -f; forced load clears channels/channel_users/mail_bodies/mail_headers and the new game's marker replaces the old one; empty-shell load proceeds without -f; -C/-m repopulate after the clear. smoke 1115/0/0, 200 jit_diff clean, tests/libmux 51/0. Closes #783. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:33 -06:00
int db_top_val = 0;
bool bHasGame =
g_pSQLiteBackend->GetDB().GetMeta("db_top", &db_top_val)
&& 0 < db_top_val;
if (bHasGame)
{
// The target SQLite database already holds a game. Loading
// would replace it, so refuse unless the caller explicitly
// forces it. Name the file and spell out both ways forward
// rather than leaving a dead-end that tempts running from
// the wrong directory.
//
mux_fprintf(stderr,
T("Refusing to overwrite the existing SQLite database:\n"
" %s\n"
"That file already contains a game. To replace it, either:\n"
" * remove the file shown above and re-run, or\n"
" * re-run this load with the -f (force) option.\n"),
(nullptr != pDbPath && '\0' != pDbPath[0]) ? pDbPath : "(unknown)");
CLOSE;
return MUX_E_FAIL;
}
}
}
else if (cc == HF_OPEN_STATUS_NEW)
{
if (setflags == UNLOAD_FLAGS)
{
mux_fprintf(stderr, T("SQLite database is empty.\n"));
CLOSE;
return MUX_E_FAIL;
}
}
bool bLoadedFromSQLite = false;
if (nullptr == infile && HF_OPEN_STATUS_OLD == cc)
{
int sqlite_load_rc = sqlite_load_game();
if (sqlite_load_rc < 0)
{
mux_fprintf(stderr, T("Input: SQLite database load failed.\n"));
return MUX_E_FAIL;
}
bLoadedFromSQLite = (sqlite_load_rc > 0);
}
if (bLoadedFromSQLite)
{
mux_fprintf(stderr, T("Input: SQLite database\n"));
db_format = F_MUX;
db_ver = OUTPUT_VERSION;
db_flags = OUTPUT_FLAGS;
}
else
{
if (nullptr == infile)
{
mux_fprintf(stderr, T("No input flatfile provided and SQLite has no loadable game data.\n"));
return MUX_E_FAIL;
}
FILE *fpIn;
if (!mux_fopen(&fpIn, infile, T("rb")))
{
return MUX_E_FAIL;
}
setvbuf(fpIn, nullptr, _IOFBF, 16384);
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.Begin() || !sqldb.ClearAttributes())
{
sqldb.Rollback();
mux_fprintf(stderr, T("SQLite attribute clear failed before flatfile import.\n"));
mux_fclose(fpIn);
return MUX_E_FAIL;
}
mudstate.bSQLiteLoading = true;
if (db_read(fpIn, &db_format, &db_ver, &db_flags) < 0)
{
mudstate.bSQLiteLoading = false;
sqldb.Rollback();
// Queued attrs targeted the aborted transaction — drop them.
//
cache_discard_writes();
mux_fclose(fpIn);
mudstate.bStandAlone = false;
return MUX_E_FAIL;
}
// Flush remaining write-queue puts into the open import transaction
// before Commit (#1047). Must run while bSQLiteLoading is still true
// so flush does not open a nested Begin/Commit. With bStandAlone
// this is typically a no-op (write-through).
//
if (!cache_flush_writes())
{
mudstate.bSQLiteLoading = false;
sqldb.Rollback();
cache_discard_writes();
mux_fprintf(stderr, T("SQLite attribute import flush failed.\n"));
mux_fclose(fpIn);
mudstate.bStandAlone = false;
return MUX_E_FAIL;
}
mudstate.bSQLiteLoading = false;
if (!sqldb.Commit())
{
sqldb.Rollback();
cache_discard_writes();
mux_fprintf(stderr, T("SQLite attribute import commit failed.\n"));
mux_fclose(fpIn);
mudstate.bStandAlone = false;
return MUX_E_FAIL;
}
if (!sqlite_sync_runtime())
{
if (!clear_sqlite_after_sync_failure(sqldb))
{
mux_fprintf(stderr, T("SQLite cleanup failed after sync failure.\n"));
}
mux_fprintf(stderr, T("SQLite metadata sync failed.\n"));
return MUX_E_FAIL;
}
mux_fprintf(stderr, T("Input: "));
dbconvert_info(db_format, db_flags, db_ver);
if (bCheck)
{
do_dbck(NOTHING, NOTHING, NOTHING, 0, DBCK_FULL);
}
mux_fclose(fpIn);
}
dbload: clear comsys/mail on a forced game load (#783) A flatfile game load replaces the database's game, but the comsys and mail tables were only cleared when -C/-m were passed -- so "db_load -f netmux new.flat" produced a database whose channels and mail belonged to the PREVIOUS game, referencing dbrefs in the new one. This scenario was impossible before -f existed (the only supported path was deleting the whole .sqlite, which removed comsys/mail too). Clear both table sets after any flatfile game load; the -C/-m imports that follow repopulate them when flatfiles are supplied (verified: export + forced reload with -C/-m round-trips channels and mail). Adjacent fixes from the same review: - The "already contains a game" refusal fired on ANY pre-existing file (HF_OPEN_STATUS_OLD only means the file existed) -- including the empty shell that running db_unload against a never-imported game creates as a side effect. Probe the db_top metadata and only refuse when the file actually holds a game; loading into an empty shell proceeds without -f. - db_load now accepts -f anywhere among the options, not just as the first argument. - The every-warm-boot log line advising "Remove the SQLite database to reload from a flatfile" invited destroying all changes since the seed flatfile (the .sqlite is authoritative; the flatfile is never refreshed). It now recommends exporting with db_unload first, then db_load -f. Verified end-to-end: refusal with a real game and no -f; forced load clears channels/channel_users/mail_bodies/mail_headers and the new game's marker replaces the old one; empty-shell load proceeds without -f; -C/-m repopulate after the clear. smoke 1115/0/0, 200 jit_diff clean, tests/libmux 51/0. Closes #783. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:33 -06:00
// A flatfile game load replaces the database's game, so channel and
// mail rows referencing the OLD game's dbrefs must not survive into
// the new one (#783). Clear both table sets; the -C/-m imports
// below repopulate them when flatfiles are supplied. (The pre-force
// workflow -- deleting the whole .sqlite -- removed them too.)
//
if (bLoad && !bLoadedFromSQLite)
{
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if ( !sqldb.Begin()
|| !sqldb.ClearComsysTables()
|| !sqldb.ClearMailTables()
|| !sqldb.Commit())
{
sqldb.Rollback();
mux_fprintf(stderr,
T("SQLite comsys/mail clear failed after game load.\n"));
return MUX_E_FAIL;
}
mux_fprintf(stderr,
T("Cleared comsys and mail tables (game replaced).\n"));
}
// Import comsys from flatfile into SQLite.
//
if (bLoad && comsys_file)
{
load_comsys(const_cast<UTF8 *>(comsys_file));
if (!sqlite_sync_comsys())
{
mux_fprintf(stderr, T("Import comsys into SQLite failed.\n"));
return MUX_E_FAIL;
}
mux_fprintf(stderr, T("Imported comsys into SQLite.\n"));
}
// Import mail from flatfile into SQLite.
//
if (bLoad && mail_file)
{
FILE *fpMail;
if (mux_fopen(&fpMail, mail_file, T("rb")))
{
setvbuf(fpMail, nullptr, _IOFBF, 16384);
load_mail(fpMail);
mux_fclose(fpMail);
if (!sqlite_sync_mail())
{
mux_fprintf(stderr, T("Import mail into SQLite failed.\n"));
return MUX_E_FAIL;
}
mux_fprintf(stderr, T("Imported mail into SQLite.\n"));
}
}
// Export comsys from SQLite to flatfile.
//
if (bUnload && comsys_file)
{
int sqlite_comsys_rc = sqlite_load_comsys();
if (sqlite_comsys_rc > 0)
{
save_comsys(const_cast<UTF8 *>(comsys_file));
mux_fprintf(stderr, T("Exported comsys from SQLite.\n"));
}
else if (sqlite_comsys_rc < 0)
{
mux_fprintf(stderr, T("Export comsys from SQLite failed.\n"));
return MUX_E_FAIL;
}
}
// Export mail from SQLite to flatfile.
//
if (bUnload && mail_file)
{
int sqlite_mail_rc = sqlite_load_mail();
if (sqlite_mail_rc > 0)
{
FILE *fpMail;
if (mux_fopen(&fpMail, mail_file, T("wb")))
{
dump_mail(fpMail);
mux_fclose(fpMail);
mux_fprintf(stderr, T("Exported mail from SQLite.\n"));
}
}
else if (sqlite_mail_rc < 0)
{
mux_fprintf(stderr, T("Export mail from SQLite failed.\n"));
return MUX_E_FAIL;
}
}
if (do_write)
{
FILE *fpOut;
if (!mux_fopen(&fpOut, outfile, T("wb")))
{
return MUX_E_FAIL;
}
db_flags = (db_flags & ~clrflags) | setflags;
if (db_format != F_MUX)
{
db_ver = 3;
}
if (ver != 0)
{
db_ver = ver;
}
mux_fprintf(stderr, T("Output: "));
dbconvert_info(F_MUX, db_flags, db_ver);
setvbuf(fpOut, nullptr, _IOFBF, 16384);
// #1869: propagate flatfile I/O failure so dbconvert does not
// claim success on a truncated dump.
//
const dbref nWritten = db_write(fpOut, F_MUX, db_ver | db_flags);
mux_fclose(fpOut);
if (nWritten < 0)
{
CLOSE;
#ifdef SELFCHECK
db_free();
#endif
mudstate.bStandAlone = false;
return MUX_E_FAIL;
}
}
CLOSE;
#ifdef SELFCHECK
db_free();
#endif
mudstate.bStandAlone = false;
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CGameEngineFactory
// ---------------------------------------------------------------------------
CGameEngineFactory::CGameEngineFactory(void) : m_cRef(1)
{
}
CGameEngineFactory::~CGameEngineFactory()
{
}
MUX_RESULT CGameEngineFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CGameEngineFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CGameEngineFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CGameEngineFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CGameEngine *pGameEngine = nullptr;
try
{
pGameEngine = new CGameEngine;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pGameEngine)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pGameEngine->QueryInterface(iid, ppv);
pGameEngine->Release();
return mr;
}
MUX_RESULT CGameEngineFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CPlayerSession — engine-side implementation of mux_IPlayerSession.
// Handles player authentication, creation, and connect/disconnect lifecycle.
// The driver calls these methods; all player/game-state operations are here.
// ---------------------------------------------------------------------------
class CPlayerSession : public mux_IPlayerSession
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT ConnectPlayer(const UTF8 *name, const UTF8 *password,
const UTF8 *host, const UTF8 *username, const UTF8 *ipaddr,
dbref *pPlayer);
virtual MUX_RESULT CreatePlayer(const UTF8 *name, const UTF8 *password,
dbref creator, bool isRobot, dbref *pPlayer,
const UTF8 **ppMsg);
virtual MUX_RESULT AddToPublicChannel(dbref player);
virtual MUX_RESULT AddToPlayerChannels(dbref player);
virtual MUX_RESULT AnnounceConnect(dbref player, int numConnections,
bool isPueblo, bool isSuspect, const UTF8 *host,
const UTF8 *username, const UTF8 *ipaddr, int *pTimeout,
int64_t *pConnlogId);
virtual MUX_RESULT AnnounceDisconnect(dbref player, int numConnections,
bool isSuspect, bool wasAutoDark, const UTF8 *reason,
int64_t connlogId);
virtual MUX_RESULT FcacheSend(DESC *d, int num);
virtual MUX_RESULT FcacheRawSend(SOCKET fd, int num);
virtual MUX_RESULT CreateGuest(DESC *d, const UTF8 **ppName);
virtual MUX_RESULT CheckGuest(dbref player, bool *pResult);
CPlayerSession(void);
virtual ~CPlayerSession();
private:
uint32_t m_cRef;
};
CPlayerSession::CPlayerSession(void) : m_cRef(1)
{
}
CPlayerSession::~CPlayerSession()
{
}
MUX_RESULT CPlayerSession::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IPlayerSession *>(this);
}
else if (IID_IPlayerSession == iid)
{
*ppv = static_cast<mux_IPlayerSession *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CPlayerSession::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CPlayerSession::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CPlayerSession::ConnectPlayer(const UTF8 *name,
const UTF8 *password, const UTF8 *host, const UTF8 *username,
const UTF8 *ipaddr, dbref *pPlayer)
{
if (nullptr == pPlayer)
{
return MUX_E_INVALIDARG;
}
*pPlayer = connect_player(const_cast<UTF8 *>(name),
const_cast<UTF8 *>(password), const_cast<UTF8 *>(host),
const_cast<UTF8 *>(username), const_cast<UTF8 *>(ipaddr));
return (*pPlayer == NOTHING) ? MUX_E_NOTFOUND : MUX_S_OK;
}
MUX_RESULT CPlayerSession::CreatePlayer(const UTF8 *name,
const UTF8 *password, dbref creator, bool isRobot,
dbref *pPlayer, const UTF8 **ppMsg)
{
if (nullptr == pPlayer)
{
return MUX_E_INVALIDARG;
}
*pPlayer = create_player(name, password, creator, isRobot, ppMsg);
return (*pPlayer == NOTHING) ? MUX_E_FAIL : MUX_S_OK;
}
MUX_RESULT CPlayerSession::AddToPublicChannel(dbref player)
{
::AddToPublicChannel(player);
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::AddToPlayerChannels(dbref player)
{
::AddToPlayerChannels(player);
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::AnnounceConnect(dbref player, int numConnections,
bool isPueblo, bool isSuspect, const UTF8 *host,
const UTF8 *username, const UTF8 *ipaddr, int *pTimeout,
int64_t *pConnlogId)
{
// Preload attributes for the player and nearby rooms.
//
cache_preload_nearby(player, mudconf.cache_preload_depth);
// Track record player count.
//
int count = numConnections;
if (mudstate.record_players < count)
{
mudstate.record_players = count;
g_pSQLiteBackend->GetDB().PutMeta("record_players",
mudstate.record_players);
}
// Read A_TIMEOUT and return it to the driver.
//
if (nullptr != pTimeout)
{
LBuf buf = LBuf_Src("AnnounceConnect.timeout");
dbref aowner;
int aflags;
size_t nLen;
atr_pget_str_LEN(buf, player, A_TIMEOUT, &aowner, &aflags, &nLen);
if (nLen)
{
fix(win32): migrate the remaining mux_atol callers to mux_atoi64 (#1373) Completes the sweep the issue called for. mux_atol returns long, which is 32-bit on LLP64, so every caller silently truncated on Windows. Two of those were real defects (the truthiness family and cf_size, fixed in the preceding commits); the rest were latent, waiting for a value large enough to matter. Rather than audit 290 sites for whether each can reach 2^31 today, use the 64-bit parser everywhere and remove the class. A dbref cannot overflow now, but nothing stops a later caller passing that same site a timestamp or a byte count. Pure 1:1 substitution: 285 lines changed, and every removed line contained mux_atol while every added line contains mux_atoi64. No control flow, no types, no behaviour beyond the wider parse. This is a NO-OP on LP64 -- long is already 64-bit on Linux and macOS, so the generated code there is unchanged. It only widens the parse on Windows. Narrowing destinations are unaffected either way: `int x = mux_atoi64(s)` truncates exactly as `int x = mux_atol(s)` did, on both models. Left alone: mux_atol itself in mathutil, its declaration, and three comments that name it. Callers that genuinely want 32-bit semantics can still ask for them; none appear to. Verified on Windows: full solution builds clean with no new warnings, smoke is 1418 passed / 16 failed / 0 crashes / 306 of 306 dispatched -- identical to before the sweep, with the same 16 build-configuration failures (exp3 module not loaded, hmac/digest behind UNIX_DIGEST). Spot checks after the change: the boolean family returns 1 for multiples of 2^32, cf_size round-trips 3000000000 and still reads -1 as unlimited, and arithmetic, string and list functions are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:03:45 -06:00
*pTimeout = mux_atoi64(buf);
if (*pTimeout <= 0)
{
*pTimeout = mudconf.idle_timeout;
}
}
else
{
*pTimeout = mudconf.idle_timeout;
}
}
// Set Connected flag. Set Html if Pueblo client.
//
const dbref loc = Location(player);
s_Connected(player);
if (isPueblo)
{
s_Html(player);
}
// MOTD messages.
//
if ('\0' != mudconf.motd_msg[0])
{
raw_notify(player, tprintf(T("\n%sMOTD:%s %s\n"), COLOR_INTENSE,
COLOR_RESET, mudconf.motd_msg));
}
if (Wizard(player))
{
if ('\0' != mudconf.wizmotd_msg[0])
{
raw_notify(player, tprintf(T("%sWIZMOTD:%s %s\n"), COLOR_INTENSE,
COLOR_RESET, mudconf.wizmotd_msg));
}
if (!(mudconf.control_flags & CF_LOGIN))
{
raw_notify(player, M_("*** Logins are disabled."));
}
}
// Page lock warning.
//
{
LBuf buf = LBuf_Src("AnnounceConnect.lpage");
dbref aowner;
int aflags;
size_t nLen;
atr_get_str_LEN(buf, player, A_LPAGE, &aowner, &aflags, &nLen);
if (nLen)
{
raw_notify(player, M_("Your PAGE LOCK is set. You may be unable to receive some pages."));
}
}
// Check for forced encoding.
//
if (Unicode(player))
{
set_player_encoding(player, CHARSET_UTF8);
}
if (Ascii(player))
{
set_player_encoding(player, CHARSET_ASCII);
}
// Reset vacation flag. Clear DARK on guests.
//
s_Flags(player, FLAG_WORD2, Flags2(player) & ~VACATION);
if (Guest(player))
{
s_Flags(player, FLAG_WORD1, db[player].fs.word[FLAG_WORD1] & ~DARK);
}
// Room and monitor announcements.
//
const UTF8 *pRoomAnnounceFmt;
const UTF8 *pMonitorAnnounceFmt;
if (numConnections < 2)
{
pRoomAnnounceFmt = M_("%s has connected.");
do_comconnect(player);
if ( Hidden(player)
&& Can_Hide(player))
{
pMonitorAnnounceFmt = M_("GAME: %s has DARK-connected.");
}
else
{
pMonitorAnnounceFmt = M_("GAME: %s has connected.");
}
if ( Suspect(player)
|| isSuspect)
{
raw_broadcast(WIZARD, M_("[Suspect] %s has connected."),
Moniker(player));
}
}
else
{
pRoomAnnounceFmt = M_("%s has reconnected.");
pMonitorAnnounceFmt = M_("GAME: %s has reconnected.");
if ( Suspect(player)
|| isSuspect)
{
raw_broadcast(WIZARD, M_("[Suspect] %s has reconnected."),
Moniker(player));
}
}
LBuf buf = LBuf_Src("AnnounceConnect.room");
mux_sprintf(buf, LBUF_SIZE, pRoomAnnounceFmt, Moniker(player));
raw_broadcast(MONITOR, pMonitorAnnounceFmt, Moniker(player));
int key = MSG_INV;
if ( loc != NOTHING
&& !( Hidden(player)
&& Can_Hide(player)))
{
key |= (MSG_NBR | MSG_NBR_EXITS | MSG_LOC | MSG_FWDLIST);
}
dbref temp = mudstate.curr_enactor;
mudstate.curr_enactor = player;
#ifdef REALITY_LVLS
if (NOTHING == loc)
{
notify_check(player, player, buf, key);
}
else
{
notify_except_rlevel(loc, player, player, buf, 0);
}
#else
notify_check(player, player, buf, key);
#endif // REALITY_LVLS
// ACONNECT triggers: player, master room, zone.
//
dbref aowner, zone, obj;
int aflags;
size_t nLen;
CLinearTimeAbsolute lta;
atr_pget_str_LEN(buf, player, A_ACONNECT, &aowner, &aflags, &nLen);
if (nLen)
{
wait_que(player, player, player, AttrTrace(aflags, 0), false, lta,
NOTHING, 0, buf, 0, nullptr, nullptr);
}
if (mudconf.master_room != NOTHING)
{
atr_pget_str_LEN(buf, mudconf.master_room, A_ACONNECT, &aowner,
&aflags, &nLen);
if (nLen)
{
wait_que(mudconf.master_room, player, player,
AttrTrace(aflags, 0), false, lta, NOTHING, 0, buf,
0, nullptr, nullptr);
}
DOLIST(obj, Contents(mudconf.master_room))
{
atr_pget_str_LEN(buf, obj, A_ACONNECT, &aowner, &aflags, &nLen);
if (nLen)
{
wait_que(obj, player, player, AttrTrace(aflags, 0), false, lta,
NOTHING, 0, buf, 0, nullptr, nullptr);
}
}
}
// Zone ACONNECT.
//
if ( mudconf.have_zones
&& Good_obj(zone = Zone(loc)))
{
switch (Typeof(zone))
{
case TYPE_THING:
atr_pget_str_LEN(buf, zone, A_ACONNECT, &aowner, &aflags, &nLen);
if (nLen)
{
wait_que(zone, player, player, AttrTrace(aflags, 0), false,
lta, NOTHING, 0, buf, 0, nullptr, nullptr);
}
break;
case TYPE_ROOM:
DOLIST(obj, Contents(zone))
{
atr_pget_str_LEN(buf, obj, A_ACONNECT, &aowner, &aflags,
&nLen);
if (nLen)
{
wait_que(obj, player, player, AttrTrace(aflags, 0), false,
lta, NOTHING, 0, buf, 0, nullptr, nullptr);
}
}
break;
default:
log_printf(T("Invalid zone #%d for %s(#%d) has bad type %d"),
zone, PureName(player), player, Typeof(zone));
}
}
// Record login, check mail, show room.
//
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
const UTF8 *time_str = ltaNow.ReturnDateString(7);
record_login(player, true, time_str, host, username, ipaddr);
// Log connection to connlog table.
//
if (nullptr != pConnlogId)
{
CLinearTimeAbsolute ltaUtc;
ltaUtc.GetUTC();
int64_t utcSeconds = ltaUtc.ReturnSeconds();
*pConnlogId = g_pSQLiteBackend->GetDB().ConnlogInsert(
player, utcSeconds, host, ipaddr);
}
check_mail(player, 0, false);
look_in(player, Location(player), (LK_SHOWEXIT|LK_OBEYTERSE|LK_SHOWVRML));
mudstate.curr_enactor = temp;
local_connect(player, 0, numConnections);
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->connect(player, 0, numConnections);
p = p->pNext;
}
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::AnnounceDisconnect(dbref player,
int numConnections, bool isSuspect, bool wasAutoDark,
const UTF8 *reason, int64_t connlogId)
{
// Update connlog row with disconnect time and reason.
//
if (0 != connlogId)
{
CLinearTimeAbsolute ltaUtc;
ltaUtc.GetUTC();
int64_t utcSeconds = ltaUtc.ReturnSeconds();
g_pSQLiteBackend->GetDB().ConnlogUpdate(connlogId, utcSeconds, reason);
}
int key;
const dbref temp = mudstate.curr_enactor;
mudstate.curr_enactor = player;
const dbref loc = Location(player);
if (numConnections < 2)
{
// Last connection — full disconnect.
//
if ( Suspect(player)
|| isSuspect)
{
raw_broadcast(WIZARD, M_("[Suspect] %s has disconnected."),
Moniker(player));
}
LBuf buf = LBuf_Src("AnnounceDisconnect.only");
mux_sprintf(buf, LBUF_SIZE, M_("%s has disconnected."),
Moniker(player));
key = MSG_INV;
if ( loc != NOTHING
&& !( Hidden(player)
&& Can_Hide(player)))
{
key |= (MSG_NBR | MSG_NBR_EXITS | MSG_LOC | MSG_FWDLIST);
}
#ifdef REALITY_LVLS
if (NOTHING == loc)
{
notify_check(player, player, buf, key);
}
else
{
notify_except_rlevel(loc, player, player, buf, 0);
}
#else
notify_check(player, player, buf, key);
#endif // REALITY_LVLS
do_mail_purge(player);
raw_broadcast(MONITOR, M_("GAME: %s has disconnected. <%s>"),
Moniker(player), reason);
c_Connected(player);
do_comdisconnect(player);
// ADISCONNECT triggers: player, master room, zone.
//
dbref aowner, zone, obj;
int aflags;
size_t nLen;
CLinearTimeAbsolute lta;
atr_pget_str_LEN(buf, player, A_ADISCONNECT, &aowner, &aflags, &nLen);
if (nLen)
{
wait_que(player, player, player, AttrTrace(aflags, 0), false,
lta, NOTHING, 0, buf, 1, &reason, nullptr);
}
if (mudconf.master_room != NOTHING)
{
atr_pget_str_LEN(buf, mudconf.master_room, A_ADISCONNECT, &aowner,
&aflags, &nLen);
if (nLen)
{
wait_que(mudconf.master_room, player, player,
AttrTrace(aflags, 0), false, lta, NOTHING, 0, buf,
0, nullptr, nullptr);
}
DOLIST(obj, Contents(mudconf.master_room))
{
atr_pget_str_LEN(buf, obj, A_ADISCONNECT, &aowner, &aflags,
&nLen);
if (nLen)
{
wait_que(obj, player, player, AttrTrace(aflags, 0), false,
lta, NOTHING, 0, buf, 0, nullptr, nullptr);
}
}
}
// Zone ADISCONNECT.
//
if (mudconf.have_zones && Good_obj(zone = Zone(loc)))
{
switch (Typeof(zone))
{
case TYPE_THING:
atr_pget_str_LEN(buf, zone, A_ADISCONNECT, &aowner, &aflags,
&nLen);
if (nLen)
{
wait_que(zone, player, player, AttrTrace(aflags, 0),
false, lta, NOTHING, 0, buf, 0, nullptr, nullptr);
}
break;
case TYPE_ROOM:
DOLIST(obj, Contents(zone))
{
atr_pget_str_LEN(buf, obj, A_ADISCONNECT, &aowner, &aflags,
&nLen);
if (nLen)
{
wait_que(obj, player, player, AttrTrace(aflags, 0),
false, lta, NOTHING, 0, buf, 0, nullptr, nullptr);
}
}
break;
default:
log_printf(T("Invalid zone #%d for %s(#%d) has bad type %d"),
zone, PureName(player), player, Typeof(zone));
}
}
// Clear AUTODARK, darken guests, halt guest queues.
//
if (wasAutoDark)
{
s_Flags(player, FLAG_WORD1,
db[player].fs.word[FLAG_WORD1] & ~DARK);
}
if (Guest(player))
{
s_Flags(player, FLAG_WORD1,
db[player].fs.word[FLAG_WORD1] | DARK);
halt_que(NOTHING, player);
}
}
else
{
// Partial disconnect — other connections remain.
//
if ( Suspect(player)
|| isSuspect)
{
raw_broadcast(WIZARD,
M_("[Suspect] %s has partially disconnected."),
Moniker(player));
}
UTF8 *mbuf = alloc_mbuf("AnnounceDisconnect.partial");
mux_sprintf(mbuf, MBUF_SIZE, M_("%s has partially disconnected."),
Moniker(player));
key = MSG_INV;
if ( loc != NOTHING
&& !( Hidden(player)
&& Can_Hide(player)))
{
key |= (MSG_NBR | MSG_NBR_EXITS | MSG_LOC | MSG_FWDLIST);
}
#ifdef REALITY_LVLS
if (NOTHING == loc)
{
notify_check(player, player, mbuf, key);
}
else
{
notify_except_rlevel(loc, player, player, mbuf, 0);
}
#else
notify_check(player, player, mbuf, key);
#endif // REALITY_LVLS
raw_broadcast(MONITOR, M_("GAME: %s has partially disconnected."),
Moniker(player));
free_mbuf(mbuf);
}
mudstate.curr_enactor = temp;
local_disconnect(player, numConnections);
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->disconnect(player, numConnections);
p = p->pNext;
}
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::FcacheSend(DESC *d, int num)
{
if (nullptr == d)
{
return MUX_E_INVALIDARG;
}
fcache_dump(d, num);
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::FcacheRawSend(SOCKET fd, int num)
{
fcache_rawdump(fd, num);
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::CreateGuest(DESC *d, const UTF8 **ppName)
{
if (nullptr == ppName)
{
return MUX_E_INVALIDARG;
}
*ppName = Guest.Create(d);
if (nullptr == *ppName)
{
return MUX_E_FAIL;
}
return MUX_S_OK;
}
MUX_RESULT CPlayerSession::CheckGuest(dbref player, bool *pResult)
{
if (nullptr == pResult)
{
return MUX_E_INVALIDARG;
}
*pResult = Guest.CheckGuest(player);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CPlayerSessionFactory
// ---------------------------------------------------------------------------
CPlayerSessionFactory::CPlayerSessionFactory(void) : m_cRef(1)
{
}
CPlayerSessionFactory::~CPlayerSessionFactory()
{
}
MUX_RESULT CPlayerSessionFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
reinterpret_cast<mux_IUnknown *>(*ppv)->AddRef();
return MUX_S_OK;
}
uint32_t CPlayerSessionFactory::AddRef(void)
{
m_cRef++;
return m_cRef;
}
uint32_t CPlayerSessionFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
MUX_RESULT CPlayerSessionFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter)
{
return MUX_E_NOAGGREGATION;
}
CPlayerSession *pPlayerSession = nullptr;
try
{
pPlayerSession = new CPlayerSession;
}
catch (...)
{
; // Nothing.
}
if (nullptr == pPlayerSession)
{
return MUX_E_OUTOFMEMORY;
}
MUX_RESULT mr = pPlayerSession->QueryInterface(iid, ppv);
pPlayerSession->Release();
return mr;
}
MUX_RESULT CPlayerSessionFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ===========================================================================
// CComsysStorage — engine-provided SQLite access for comsys_mod.so.
// ===========================================================================
class CComsysStorage : public mux_IComsysStorage
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT LoadAllChannels(PFN_CHANNEL_CB pfn, void *context);
virtual MUX_RESULT LoadAllChannelUsers(PFN_CHANNEL_USER_CB pfn, void *context);
virtual MUX_RESULT LoadAllPlayerChannels(PFN_PLAYER_CHANNEL_CB pfn, void *context);
virtual MUX_RESULT SyncChannel(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);
virtual MUX_RESULT SyncChannelUser(const UTF8 *channel_name, int who,
bool is_on, bool comtitle_status, bool gag_join_leave,
const UTF8 *title);
virtual MUX_RESULT SyncPlayerChannel(int who, const UTF8 *alias,
const UTF8 *channel_name);
virtual MUX_RESULT DeleteChannel(const UTF8 *name);
virtual MUX_RESULT DeleteChannelUser(const UTF8 *channel_name, int who);
virtual MUX_RESULT DeletePlayerChannel(int who, const UTF8 *alias);
virtual MUX_RESULT DeleteAllPlayerChannels(int who);
virtual MUX_RESULT ClearComsysTables(void);
fix(comsys): let the module write engine-owned channel attributes (#1585, #1620) Both bugs are one refusal. The engine writes HISTORY_%d, MAX_LOG and LOG_TIMESTAMPS on the channel object as GOD with AF_CONST, and bCanSetAttr denies AF_CONST in every branch -- God included, which is unlike every other flag it checks. So the module's permission-checked SetAttribute is refused, and whichever implementation did not write a value first can never change it. #1585 a channel whose timestamps the engine enabled could not be turned off from the module, and before #1624 the module reported success anyway. #1620 the same refusal reaching HISTORY_%d once the ring wraps onto an engine-written slot. num_messages is a column and keeps counting, so the counter and the history diverge -- #1564's symptom arriving by a second route. mux_IComsysStorage gains SetChannelAttr: the module asks the ENGINE to perform the write. The engine owns the attribute layer and the permission check, and here it is writing its own data rather than a player's, so an attribute add is its operation to make. Two alternatives were weighed and rejected: - Dropping AF_CONST from the five comsys writes is the smallest change, but then anyone controlling the channel object can forge recall history with a plain @set. Channel logs stop being tamper-evident, which is what AF_CONST was buying. - Giving God an escape from AF_CONST in bCanSetAttr is one line and restores an invariant that holds for every other flag there, but it is a global permission change to fix one subsystem's problem. Scope is deliberately narrow. The channel is passed by NAME, so the engine resolves the object itself and this cannot become "write any attribute anywhere as GOD". Player-owned attributes are untouched -- mail's Mailcurf/Mailfolders keep going through the permission-checked mux_IAttributeAccess, which is the right path for anything a player owns. tests/comsys_handoff now passes with no TODO markers: ok 4 - module can clear a flag the engine set (#1585) ok 6 - module overwrites an engine-written history slot (#1620) === comsys handoff: 8 passed, 0 failed, 0 known-failing === The harness demanded this edit rather than allowing it: with the fix in and the markers still present it reported "2 unexpectedly passing" and failed the run, which is what those markers are for. One assertion changed direction rather than being deleted. #1624 added a check that a REFUSED write was reported honestly instead of as success; there is no refusal left to report, so it now checks that the clear reports success -- because it now is one. Asserting on the old failure text would have failed for the right reason in the wrong direction. make test green: 1561/1561 on all three smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:26:08 -06:00
virtual MUX_RESULT SetChannelAttr(const UTF8 *channel_name,
const UTF8 *pAttrName, const UTF8 *pValue);
CComsysStorage(void);
virtual ~CComsysStorage();
private:
uint32_t m_cRef;
};
CComsysStorage::CComsysStorage(void) : m_cRef(1) {}
CComsysStorage::~CComsysStorage() {}
MUX_RESULT CComsysStorage::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid || IID_IComsysStorage == iid)
{
*ppv = static_cast<mux_IComsysStorage *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CComsysStorage::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CComsysStorage::Release(void)
{
m_cRef--;
if (0 == m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
// #1190: Comsys/mail storage COM must not dereference a closed backend.
//
static CSQLiteDB *sqlite_storage_db(void)
{
if (nullptr == g_pSQLiteBackend)
{
return nullptr;
}
return &g_pSQLiteBackend->GetDB();
}
MUX_RESULT CComsysStorage::LoadAllChannels(PFN_CHANNEL_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllChannels(
[pfn, context](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)
{
pfn(context, name, header, type, temp1, temp2, charge,
charge_who, amount_col, num_messages, chan_obj);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::LoadAllChannelUsers(PFN_CHANNEL_USER_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllChannelUsers(
[pfn, context](const UTF8 *channel_name, int who,
bool is_on, bool comtitle_status, bool gag_join_leave,
const UTF8 *title)
{
pfn(context, channel_name, who, is_on, comtitle_status,
gag_join_leave, title);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::LoadAllPlayerChannels(PFN_PLAYER_CHANNEL_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllPlayerChannels(
[pfn, context](int who, const UTF8 *alias,
const UTF8 *channel_name)
{
pfn(context, who, alias, channel_name);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::SyncChannel(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)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->SyncChannel(name, header, type,
temp1, temp2, charge, charge_who, amount_col, num_messages, chan_obj);
if (ok)
{
// #1191 / #783: softcode reloads via sqlite_load_comsys() only when
// has_comsys meta is set. Module creates must open that gate.
//
ok = pDb->PutMeta("has_comsys", 1);
}
return ok ? MUX_S_OK : MUX_E_FAIL;
}
fix(comsys): let the module write engine-owned channel attributes (#1585, #1620) Both bugs are one refusal. The engine writes HISTORY_%d, MAX_LOG and LOG_TIMESTAMPS on the channel object as GOD with AF_CONST, and bCanSetAttr denies AF_CONST in every branch -- God included, which is unlike every other flag it checks. So the module's permission-checked SetAttribute is refused, and whichever implementation did not write a value first can never change it. #1585 a channel whose timestamps the engine enabled could not be turned off from the module, and before #1624 the module reported success anyway. #1620 the same refusal reaching HISTORY_%d once the ring wraps onto an engine-written slot. num_messages is a column and keeps counting, so the counter and the history diverge -- #1564's symptom arriving by a second route. mux_IComsysStorage gains SetChannelAttr: the module asks the ENGINE to perform the write. The engine owns the attribute layer and the permission check, and here it is writing its own data rather than a player's, so an attribute add is its operation to make. Two alternatives were weighed and rejected: - Dropping AF_CONST from the five comsys writes is the smallest change, but then anyone controlling the channel object can forge recall history with a plain @set. Channel logs stop being tamper-evident, which is what AF_CONST was buying. - Giving God an escape from AF_CONST in bCanSetAttr is one line and restores an invariant that holds for every other flag there, but it is a global permission change to fix one subsystem's problem. Scope is deliberately narrow. The channel is passed by NAME, so the engine resolves the object itself and this cannot become "write any attribute anywhere as GOD". Player-owned attributes are untouched -- mail's Mailcurf/Mailfolders keep going through the permission-checked mux_IAttributeAccess, which is the right path for anything a player owns. tests/comsys_handoff now passes with no TODO markers: ok 4 - module can clear a flag the engine set (#1585) ok 6 - module overwrites an engine-written history slot (#1620) === comsys handoff: 8 passed, 0 failed, 0 known-failing === The harness demanded this edit rather than allowing it: with the fix in and the markers still present it reported "2 unexpectedly passing" and failed the run, which is what those markers are for. One assertion changed direction rather than being deleted. #1624 added a check that a REFUSED write was reported honestly instead of as success; there is no refusal left to report, so it now checks that the clear reports success -- because it now is one. Asserting on the old failure text would have failed for the right reason in the wrong direction. make test green: 1561/1561 on all three smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:26:08 -06:00
// Write an engine-owned attribute on a channel object, as GOD (#1585/#1620).
//
// This bypasses bCanSetAttr deliberately, and that is the whole point: the
// engine is the owner of these attributes and of the permission check, and it
// is writing its own data rather than a player's. The module cannot reach
// atr_add itself, and its permission-checked route is refused by the AF_CONST
// the engine sets.
//
// The channel is named rather than passed as a dbref so the engine resolves
// the object, which keeps this from being a general "write any attribute
// anywhere as GOD" hole.
//
MUX_RESULT CComsysStorage::SetChannelAttr(const UTF8 *channel_name,
const UTF8 *pAttrName, const UTF8 *pValue)
{
if (nullptr == channel_name || nullptr == pAttrName)
{
return MUX_E_INVALIDARG;
}
struct channel *ch = select_channel(channel_name);
fix(comsys): let the module write engine-owned channel attributes (#1585, #1620) Both bugs are one refusal. The engine writes HISTORY_%d, MAX_LOG and LOG_TIMESTAMPS on the channel object as GOD with AF_CONST, and bCanSetAttr denies AF_CONST in every branch -- God included, which is unlike every other flag it checks. So the module's permission-checked SetAttribute is refused, and whichever implementation did not write a value first can never change it. #1585 a channel whose timestamps the engine enabled could not be turned off from the module, and before #1624 the module reported success anyway. #1620 the same refusal reaching HISTORY_%d once the ring wraps onto an engine-written slot. num_messages is a column and keeps counting, so the counter and the history diverge -- #1564's symptom arriving by a second route. mux_IComsysStorage gains SetChannelAttr: the module asks the ENGINE to perform the write. The engine owns the attribute layer and the permission check, and here it is writing its own data rather than a player's, so an attribute add is its operation to make. Two alternatives were weighed and rejected: - Dropping AF_CONST from the five comsys writes is the smallest change, but then anyone controlling the channel object can forge recall history with a plain @set. Channel logs stop being tamper-evident, which is what AF_CONST was buying. - Giving God an escape from AF_CONST in bCanSetAttr is one line and restores an invariant that holds for every other flag there, but it is a global permission change to fix one subsystem's problem. Scope is deliberately narrow. The channel is passed by NAME, so the engine resolves the object itself and this cannot become "write any attribute anywhere as GOD". Player-owned attributes are untouched -- mail's Mailcurf/Mailfolders keep going through the permission-checked mux_IAttributeAccess, which is the right path for anything a player owns. tests/comsys_handoff now passes with no TODO markers: ok 4 - module can clear a flag the engine set (#1585) ok 6 - module overwrites an engine-written history slot (#1620) === comsys handoff: 8 passed, 0 failed, 0 known-failing === The harness demanded this edit rather than allowing it: with the fix in and the markers still present it reported "2 unexpectedly passing" and failed the run, which is what those markers are for. One assertion changed direction rather than being deleted. #1624 added a check that a REFUSED write was reported honestly instead of as success; there is no refusal left to report, so it now checks that the clear reports success -- because it now is one. Asserting on the old failure text would have failed for the right reason in the wrong direction. make test green: 1561/1561 on all three smoke routes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:26:08 -06:00
if (nullptr == ch || !Good_obj(ch->chan_obj))
{
return MUX_E_NOTFOUND;
}
const int atr = mkattr(GOD, pAttrName);
if (atr <= 0)
{
return MUX_E_FAIL;
}
// atr_add treats an empty value as a delete, which is how the engine has
// always cleared these -- so the module gets the same two operations the
// engine has, through one call.
//
atr_add(ch->chan_obj, atr, (nullptr != pValue) ? pValue : T(""), GOD,
AF_CONST | AF_NOPROG | AF_NOPARSE);
return MUX_S_OK;
}
MUX_RESULT CComsysStorage::SyncChannelUser(const UTF8 *channel_name, int who,
bool is_on, bool comtitle_status, bool gag_join_leave, const UTF8 *title)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->SyncChannelUser(channel_name, who,
is_on, comtitle_status, gag_join_leave, title);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::SyncPlayerChannel(int who, const UTF8 *alias,
const UTF8 *channel_name)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->SyncPlayerChannel(who, alias,
channel_name);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::DeleteChannel(const UTF8 *name)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteChannel(name);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::DeleteChannelUser(const UTF8 *channel_name, int who)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteChannelUser(channel_name, who);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::DeletePlayerChannel(int who, const UTF8 *alias)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeletePlayerChannel(who, alias);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::DeleteAllPlayerChannels(int who)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteAllPlayerChannels(who);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CComsysStorage::ClearComsysTables(void)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->ClearComsysTables();
return ok ? MUX_S_OK : MUX_E_FAIL;
}
// CComsysStorageFactory
//
CComsysStorageFactory::CComsysStorageFactory(void) : m_cRef(1) {}
CComsysStorageFactory::~CComsysStorageFactory() {}
MUX_RESULT CComsysStorageFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid || mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CComsysStorageFactory::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CComsysStorageFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef) { delete this; return 0; }
return m_cRef;
}
MUX_RESULT CComsysStorageFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CComsysStorage *p = nullptr;
try { p = new CComsysStorage; } catch (...) { ; }
if (nullptr == p) return MUX_E_OUTOFMEMORY;
MUX_RESULT mr = p->QueryInterface(iid, ppv);
p->Release();
return mr;
}
MUX_RESULT CComsysStorageFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ===========================================================================
// CMailStorage — engine-provided SQLite access for mail_mod.so.
// ===========================================================================
class CMailStorage : public mux_IMailStorage
{
public:
virtual MUX_RESULT QueryInterface(MUX_IID iid, void **ppv);
virtual uint32_t AddRef(void);
virtual uint32_t Release(void);
virtual MUX_RESULT LoadAllMailHeaders(PFN_MAIL_HEADER_CB pfn, void *context);
virtual MUX_RESULT LoadAllMailBodies(PFN_MAIL_BODY_CB pfn, void *context);
virtual MUX_RESULT LoadAllMailAliases(PFN_MAIL_ALIAS_CB pfn, void *context);
virtual MUX_RESULT GetMeta(const UTF8 *key, int *pValue);
virtual MUX_RESULT PutMeta(const UTF8 *key, int value);
virtual MUX_RESULT InsertMailHeader(int to_player, int from_player,
int body_number, const UTF8 *tolist, const UTF8 *time_str,
const UTF8 *subject, int read_flags, int64_t *pRowid);
virtual MUX_RESULT UpdateMailReadFlags(int64_t rowid, int read_flags);
virtual MUX_RESULT DeleteMailHeader(int64_t rowid);
virtual MUX_RESULT DeleteAllMailHeaders(int to_player);
virtual MUX_RESULT SyncMailBody(int number, const UTF8 *message);
virtual MUX_RESULT DeleteMailBody(int number);
virtual MUX_RESULT SyncMailAlias(int owner, const UTF8 *name,
const UTF8 *desc, int desc_width, const UTF8 *members);
virtual MUX_RESULT ClearMailAliases(void);
virtual MUX_RESULT ClearMailTables(void);
CMailStorage(void);
virtual ~CMailStorage();
private:
uint32_t m_cRef;
};
CMailStorage::CMailStorage(void) : m_cRef(1) {}
CMailStorage::~CMailStorage() {}
MUX_RESULT CMailStorage::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid || IID_IMailStorage == iid)
{
*ppv = static_cast<mux_IMailStorage *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CMailStorage::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CMailStorage::Release(void)
{
m_cRef--;
if (0 == m_cRef) { delete this; return 0; }
return m_cRef;
}
MUX_RESULT CMailStorage::LoadAllMailHeaders(PFN_MAIL_HEADER_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllMailHeaders(
[pfn, context](int64_t rowid, int to_player, int from_player,
int body_number, const UTF8 *tolist, const UTF8 *time_str,
const UTF8 *subject, int read_flags)
{
pfn(context, rowid, to_player, from_player, body_number,
tolist, time_str, subject, read_flags);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::LoadAllMailBodies(PFN_MAIL_BODY_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllMailBodies(
[pfn, context](int number, const UTF8 *message)
{
pfn(context, number, message);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::LoadAllMailAliases(PFN_MAIL_ALIAS_CB pfn, void *context)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pfn) return MUX_E_INVALIDARG;
bool ok = pDb->LoadAllMailAliases(
[pfn, context](int owner, const UTF8 *name,
const UTF8 *desc, int desc_width, const UTF8 *members)
{
pfn(context, owner, name, desc, desc_width, members);
});
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::GetMeta(const UTF8 *key, int *pValue)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == key || nullptr == pValue) return MUX_E_INVALIDARG;
bool ok = pDb->GetMeta(
reinterpret_cast<const char *>(key), pValue);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::PutMeta(const UTF8 *key, int value)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == key) return MUX_E_INVALIDARG;
bool ok = pDb->PutMeta(
reinterpret_cast<const char *>(key), value);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::InsertMailHeader(int to_player, int from_player,
int body_number, const UTF8 *tolist, const UTF8 *time_str,
const UTF8 *subject, int read_flags, int64_t *pRowid)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
if (nullptr == pRowid) return MUX_E_INVALIDARG;
int64_t id = pDb->InsertMailHeaderReturningId(
to_player, from_player, body_number, tolist, time_str,
subject, read_flags);
if (id < 0) return MUX_E_FAIL;
*pRowid = id;
return MUX_S_OK;
}
MUX_RESULT CMailStorage::UpdateMailReadFlags(int64_t rowid, int read_flags)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->UpdateMailReadFlags(rowid, read_flags);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::DeleteMailHeader(int64_t rowid)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteMailHeader(rowid);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::DeleteAllMailHeaders(int to_player)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteAllMailHeaders(to_player);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::SyncMailBody(int number, const UTF8 *message)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->SyncMailBody(number, message);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::DeleteMailBody(int number)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->DeleteMailBody(number);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::SyncMailAlias(int owner, const UTF8 *name,
const UTF8 *desc, int desc_width, const UTF8 *members)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->SyncMailAlias(owner, name, desc,
desc_width, members);
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::ClearMailAliases(void)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->ClearMailAliases();
return ok ? MUX_S_OK : MUX_E_FAIL;
}
MUX_RESULT CMailStorage::ClearMailTables(void)
{
CSQLiteDB *pDb = sqlite_storage_db();
if (nullptr == pDb)
{
return MUX_E_FAIL;
}
bool ok = pDb->ClearMailTables();
return ok ? MUX_S_OK : MUX_E_FAIL;
}
// CMailStorageFactory
//
CMailStorageFactory::CMailStorageFactory(void) : m_cRef(1) {}
CMailStorageFactory::~CMailStorageFactory() {}
MUX_RESULT CMailStorageFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid || mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CMailStorageFactory::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CMailStorageFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef) { delete this; return 0; }
return m_cRef;
}
MUX_RESULT CMailStorageFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
UNUSED_PARAMETER(pUnknownOuter);
CMailStorage *p = nullptr;
try { p = new CMailStorage; } catch (...) { ; }
if (nullptr == p) return MUX_E_OUTOFMEMORY;
MUX_RESULT mr = p->QueryInterface(iid, ppv);
p->Release();
return mr;
}
MUX_RESULT CMailStorageFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ---------------------------------------------------------------------------
// CJITCompileFactory — Lua bytecode → native JIT compilation.
// ---------------------------------------------------------------------------
#if defined(TINYMUX_JIT)
CJITCompileFactory::CJITCompileFactory(void) : m_cRef(1) {}
CJITCompileFactory::~CJITCompileFactory() {}
MUX_RESULT CJITCompileFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IUnknown *>(static_cast<mux_IClassFactory *>(this));
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CJITCompileFactory::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CJITCompileFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef) { delete this; return 0; }
return m_cRef;
}
MUX_RESULT CJITCompileFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter) return MUX_E_NOAGGREGATION;
return jit_compile_create_instance(iid, ppv);
}
MUX_RESULT CJITCompileFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
#endif // TINYMUX_JIT
// ---------------------------------------------------------------------------
// CLuaModFactory — Lua 5.4 scripting (embedded in engine.so).
// ---------------------------------------------------------------------------
CLuaModFactory::CLuaModFactory(void) : m_cRef(1) {}
CLuaModFactory::~CLuaModFactory() {}
MUX_RESULT CLuaModFactory::QueryInterface(MUX_IID iid, void **ppv)
{
if (mux_IID_IUnknown == iid)
{
*ppv = static_cast<mux_IUnknown *>(static_cast<mux_IClassFactory *>(this));
}
else if (mux_IID_IClassFactory == iid)
{
*ppv = static_cast<mux_IClassFactory *>(this);
}
else
{
*ppv = nullptr;
return MUX_E_NOINTERFACE;
}
AddRef();
return MUX_S_OK;
}
uint32_t CLuaModFactory::AddRef(void) { m_cRef++; return m_cRef; }
uint32_t CLuaModFactory::Release(void)
{
m_cRef--;
if (0 == m_cRef) { delete this; return 0; }
return m_cRef;
}
MUX_RESULT CLuaModFactory::CreateInstance(mux_IUnknown *pUnknownOuter,
MUX_IID iid, void **ppv)
{
if (nullptr != pUnknownOuter) return MUX_E_NOAGGREGATION;
return lua_mod_create_instance(iid, ppv);
}
MUX_RESULT CLuaModFactory::LockServer(bool bLock)
{
UNUSED_PARAMETER(bLock);
return MUX_S_OK;
}
// ===========================================================================
// COM Front-Door — engine.so exports only these 4 functions.
// ===========================================================================
static MUX_CLASS_INFO engine_classes[] =
{
{ CID_Log },
{ CID_ServerEventsSource },
{ CID_QueryClient },
{ CID_Functions },
{ CID_LogPSFactory },
{ CID_Notify },
{ CID_ObjectInfo },
{ CID_AttributeAccess },
{ CID_Evaluator },
{ CID_Permissions },
feat(modules): GAME_CONFIG -- game-policy config reaches modules (#1654) The parking condition on #1654 was "a second consumer"; the tally reached three, all documented divergences: searchcost mail non-wizard self @mail/stats was free eval_comtitle comsys comtitles always evaluated under the module money_name_* mail the charge-refusal text could not be composed ## Shape A new engine-registered class rather than an Initialize signature change: mux_IGameConfig::GetGameConfig(GAME_CONFIG *) CID_GameConfig No existing IID moves, so there is no ABI break in either direction: an old module never asks, and a new module against an old engine gets CLASSNOTAVAILABLE, soft-fails to nullptr, and keeps its prior behaviour. Two design rules baked in rather than documented and hoped for: * Queried per CALL, never cached at Initialize. A boot-time snapshot is #1613's bug -- @admin reports Set. while the module keeps stale values. @admin search_cost=50 now takes effect on the next @mail/stats. * Versioned by cbSize: caller zeroes the struct and sets cbSize; the engine fills what fits. The struct can grow without a new interface, and zero must stay a safe default for every future field. ## Both consumers, verified as a mortal The gap was wizard-invisible -- payfor() exempts wizards and every harness runs as God -- so verification used muxscript -p with a @pcreate'd mortal: search_cost 5, rich engine 150->145 module 150->145 IDENTICAL search_cost 99999, poor both: "Finding mail stats costs 99999 Pennies.", money untouched eval_comtitle default [ec] 3 Wizard says ... both sides eval_comtitle 0 [ec] [strlen(abc)] Wizard says both sides The eval_comtitle rows close the divergence documented at channel_speaker_name since #1640/#1647. Conformance and smoke are unchanged by construction (wizard runs), and were run anyway. make test: Smoke 1561 x3, conformance PASSED, handoff 13. TESTEXIT=0. Refs #1613, #1614, #1631, #1640, #1647. Closes #1654. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:35 -06:00
{ CID_GameConfig },
{ CID_MailDelivery },
{ CID_HelpSystem },
{ CID_GameEngine },
{ CID_PlayerSession },
{ CID_ComsysStorage },
{ CID_MailStorage },
#if defined(TINYMUX_JIT)
{ CID_JITCompile },
#endif
{ CID_LuaMod },
};
#define NUM_ENGINE_CLASSES (sizeof(engine_classes)/sizeof(engine_classes[0]))
#define MAKE_FACTORY(cls, cid) \
if (cid == cid_arg) \
{ \
cls *pFactory = nullptr; \
try { pFactory = new cls; } catch (...) { ; } \
if (nullptr == pFactory) return MUX_E_OUTOFMEMORY; \
mr = pFactory->QueryInterface(iid, ppv); \
pFactory->Release(); \
return mr; \
}
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_GetClassObject(MUX_CID cid_arg, MUX_IID iid, void **ppv)
{
MUX_RESULT mr = MUX_E_CLASSNOTAVAILABLE;
MAKE_FACTORY(CLogFactory, CID_Log)
MAKE_FACTORY(CServerEventsSourceFactory, CID_ServerEventsSource)
MAKE_FACTORY(CQueryClientFactory, CID_QueryClient)
MAKE_FACTORY(CFunctionsFactory, CID_Functions)
MAKE_FACTORY(CLogPSFactory, CID_LogPSFactory)
MAKE_FACTORY(CNotifyFactory, CID_Notify)
MAKE_FACTORY(CObjectInfoFactory, CID_ObjectInfo)
MAKE_FACTORY(CAttributeAccessFactory, CID_AttributeAccess)
MAKE_FACTORY(CEvaluatorFactory, CID_Evaluator)
MAKE_FACTORY(CPermissionsFactory, CID_Permissions)
feat(modules): GAME_CONFIG -- game-policy config reaches modules (#1654) The parking condition on #1654 was "a second consumer"; the tally reached three, all documented divergences: searchcost mail non-wizard self @mail/stats was free eval_comtitle comsys comtitles always evaluated under the module money_name_* mail the charge-refusal text could not be composed ## Shape A new engine-registered class rather than an Initialize signature change: mux_IGameConfig::GetGameConfig(GAME_CONFIG *) CID_GameConfig No existing IID moves, so there is no ABI break in either direction: an old module never asks, and a new module against an old engine gets CLASSNOTAVAILABLE, soft-fails to nullptr, and keeps its prior behaviour. Two design rules baked in rather than documented and hoped for: * Queried per CALL, never cached at Initialize. A boot-time snapshot is #1613's bug -- @admin reports Set. while the module keeps stale values. @admin search_cost=50 now takes effect on the next @mail/stats. * Versioned by cbSize: caller zeroes the struct and sets cbSize; the engine fills what fits. The struct can grow without a new interface, and zero must stay a safe default for every future field. ## Both consumers, verified as a mortal The gap was wizard-invisible -- payfor() exempts wizards and every harness runs as God -- so verification used muxscript -p with a @pcreate'd mortal: search_cost 5, rich engine 150->145 module 150->145 IDENTICAL search_cost 99999, poor both: "Finding mail stats costs 99999 Pennies.", money untouched eval_comtitle default [ec] 3 Wizard says ... both sides eval_comtitle 0 [ec] [strlen(abc)] Wizard says both sides The eval_comtitle rows close the divergence documented at channel_speaker_name since #1640/#1647. Conformance and smoke are unchanged by construction (wizard runs), and were run anyway. make test: Smoke 1561 x3, conformance PASSED, handoff 13. TESTEXIT=0. Refs #1613, #1614, #1631, #1640, #1647. Closes #1654. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:57:35 -06:00
MAKE_FACTORY(CGameConfigFactory, CID_GameConfig)
MAKE_FACTORY(CMailDeliveryFactory, CID_MailDelivery)
MAKE_FACTORY(CHelpSystemFactory, CID_HelpSystem)
MAKE_FACTORY(CGameEngineFactory, CID_GameEngine)
MAKE_FACTORY(CPlayerSessionFactory, CID_PlayerSession)
MAKE_FACTORY(CComsysStorageFactory, CID_ComsysStorage)
MAKE_FACTORY(CMailStorageFactory, CID_MailStorage)
#if defined(TINYMUX_JIT)
MAKE_FACTORY(CJITCompileFactory, CID_JITCompile)
#endif
MAKE_FACTORY(CLuaModFactory, CID_LuaMod)
return mr;
}
static MUX_INTERFACE_INFO engine_interfaces[] =
{
{ IID_ILog, CID_LogPSFactory }
};
#define NUM_ENGINE_INTERFACES (sizeof(engine_interfaces)/sizeof(engine_interfaces[0]))
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Register(void)
{
MUX_RESULT mr = mux_RegisterClassObjects(NUM_ENGINE_CLASSES, engine_classes, nullptr);
if (MUX_SUCCEEDED(mr))
{
mr = mux_RegisterInterfaces(NUM_ENGINE_INTERFACES, engine_interfaces);
}
return mr;
}
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_Unregister(void)
{
mux_RevokeInterfaces(NUM_ENGINE_INTERFACES, engine_interfaces);
return mux_RevokeClassObjects(NUM_ENGINE_CLASSES, engine_classes);
}
extern "C" MUX_RESULT DCL_EXPORT DCL_API mux_CanUnloadNow(void)
{
// The engine can never be unloaded.
return MUX_S_FALSE;
}