/*! \file db.cpp * \brief Attribute interface, some flatfile and object routines. * * This file mainly has to do with attributes and objects in memory, * but it is somewhat bloated. Contents now include: the list of * built-in attributes, fwdlist routines, Name-related routines, * the \@attribute and \@fixdb commands, the attribute interface * (validation, name / number lookup, attribute list management, * encoding / decoding of attribute metadata, get / set, copy, chown), * creation throttle routines, in-memory object database routines, * new ("minimal") database setup, flatfile i/o, zone control, * comsys / mail resource cleanup, restart db i/o, and some basic * filesystem delete / rename routines. */ #include "copyright.h" #include "autoconf.h" #include "config.h" #include "externs.h" #include "routing.h" #include "sqlite_backend.h" #include "engine_api.h" #include #include #include #include using namespace std; // Per-attribute modification counter for JIT cache invalidation. // Key: (dbref << 32) | attrnum. Value: monotonic counter. // static std::unordered_map s_attr_mod_counts; static uint64_t attr_mod_key(dbref obj, int attrnum) { return (static_cast(static_cast(obj)) << 32) | static_cast(attrnum); } void attr_mod_count_inc(dbref obj, int attrnum) { uint64_t key = attr_mod_key(obj, attrnum); auto it = s_attr_mod_counts.find(key); if (it != s_attr_mod_counts.end()) { it->second++; } else { // First in-memory access: seed from backend so we stay // monotonic with the persisted counter. uint32_t base = 0; if (g_pSQLiteBackend) { base = g_pSQLiteBackend->GetModCount( static_cast(obj), static_cast(attrnum)); } s_attr_mod_counts[key] = base + 1; } } // Invalidate all in-memory mod_counts for a given object. // Used by bulk deletion paths (DelAllAttributes, object destroy). // Sets each known entry to UINT32_MAX so any compiled dependency // comparing against it will always detect staleness. // // Collect the attr list for an object from both in-memory and SQLite. // Does NOT mutate the in-memory map. Returns the set of keys that // need incrementing, with their current values seeded from SQLite // for any not yet in memory. // std::vector> attr_mod_count_collect_object(dbref obj) { std::vector> result; // Collect from backend first (covers attrs not yet in memory). if (g_pSQLiteBackend) { g_pSQLiteBackend->GetAllModCounts( static_cast(obj), [&](unsigned int attrnum, uint32_t mc) { uint64_t key = attr_mod_key(obj, static_cast(attrnum)); auto it = s_attr_mod_counts.find(key); uint32_t current = (it != s_attr_mod_counts.end()) ? it->second : mc; result.push_back({key, current}); }); } // Also collect any in-memory entries for this object that might // not be in SQLite (e.g., from a prior delete that already // removed the row but left the in-memory counter). uint32_t obj_bits = static_cast(obj); for (auto &kv : s_attr_mod_counts) { if (static_cast(kv.first >> 32) == obj_bits) { bool found = false; for (auto &r : result) { if (r.first == kv.first) { found = true; break; } } if (!found) { result.push_back({kv.first, kv.second}); } } } return result; } // Apply collected increments to the in-memory map. // Called only after the transactional delete has committed. // void attr_mod_count_apply_increments( const std::vector> &collected) { for (auto &[key, old_val] : collected) { s_attr_mod_counts[key] = old_val + 1; } } void attr_mod_count_invalidate_all() { // Full database clear: wipe the in-memory map. All counters // restart from 0 on next access, which is correct because the // JIT code cache is also cleared on database reload. s_attr_mod_counts.clear(); } uint32_t attr_mod_count_get(dbref obj, int attrnum) { auto it = s_attr_mod_counts.find(attr_mod_key(obj, attrnum)); if (it != s_attr_mod_counts.end()) { return it->second; } // Not in memory — check backend. if (g_pSQLiteBackend) { uint32_t mc = g_pSQLiteBackend->GetModCount( static_cast(obj), static_cast(attrnum)); s_attr_mod_counts[attr_mod_key(obj, attrnum)] = mc; return mc; } return 0; } #ifndef O_ACCMODE #define O_ACCMODE (O_RDONLY|O_WRONLY|O_RDWR) #endif // O_ACCMODE OBJ *db = nullptr; typedef struct atrcount ATRCOUNT; struct atrcount { dbref thing; int count; }; // List of Attributes. // ATTR AttrTable[] = { {T("Aahear"), A_AAHEAR, AF_ODARK | AF_NOPROG}, {T("Aclone"), A_ACLONE, AF_ODARK | AF_NOPROG}, {T("Aconnect"), A_ACONNECT, AF_ODARK | AF_NOPROG}, {T("ACreate"), A_ACREATE, AF_MDARK | AF_WIZARD | AF_NOCLONE | AF_NOPROG}, {T("Adesc"), A_ADESC, AF_ODARK | AF_NOPROG}, {T("ADestroy"), A_ADESTROY, AF_MDARK | AF_WIZARD | AF_NOCLONE | AF_NOPROG}, {T("Adfail"), A_ADFAIL, AF_ODARK | AF_NOPROG}, {T("Adisconnect"), A_ADISCONNECT, AF_ODARK | AF_NOPROG}, {T("Adrop"), A_ADROP, AF_ODARK | AF_NOPROG}, {T("Aefail"), A_AEFAIL, AF_ODARK | AF_NOPROG}, {T("Aenter"), A_AENTER, AF_ODARK | AF_NOPROG}, {T("Afail"), A_AFAIL, AF_ODARK | AF_NOPROG}, {T("Agfail"), A_AGFAIL, AF_ODARK | AF_NOPROG}, {T("Ahear"), A_AHEAR, AF_ODARK | AF_NOPROG}, {T("Akill"), A_AKILL, AF_ODARK | AF_NOPROG}, {T("Aleave"), A_ALEAVE, AF_ODARK | AF_NOPROG}, {T("Alfail"), A_ALFAIL, AF_ODARK | AF_NOPROG}, {T("Alias"), A_ALIAS, AF_NOPROG | AF_NOCMD | AF_NOCLONE | AF_PRIVATE | AF_CONST | AF_VISUAL}, {T("Allowance"), A_ALLOWANCE, AF_MDARK | AF_NOPROG | AF_WIZARD}, {T("Amail"), A_AMAIL, AF_ODARK | AF_NOPROG}, {T("Amhear"), A_AMHEAR, AF_ODARK | AF_NOPROG}, {T("Amove"), A_AMOVE, AF_ODARK | AF_NOPROG}, {T("Aparent"), A_APARENT, AF_ODARK | AF_NOPROG | AF_NOCLONE}, {T("Apay"), A_APAY, AF_ODARK | AF_NOPROG}, {T("Arfail"), A_ARFAIL, AF_ODARK | AF_NOPROG}, {T("Asucc"), A_ASUCC, AF_ODARK | AF_NOPROG}, {T("Atfail"), A_ATFAIL, AF_ODARK | AF_NOPROG}, {T("Atport"), A_ATPORT, AF_ODARK | AF_NOPROG}, {T("Atofail"), A_ATOFAIL, AF_ODARK | AF_NOPROG}, {T("Aufail"), A_AUFAIL, AF_ODARK | AF_NOPROG}, {T("Ause"), A_AUSE, AF_ODARK | AF_NOPROG}, {T("Away"), A_AWAY, AF_ODARK | AF_NOPROG}, {T("Charges"), A_CHARGES, AF_ODARK | AF_NOPROG}, {T("CmdCheck"), A_CMDCHECK, AF_DARK | AF_NOPROG | AF_NOCMD | AF_NOCLONE | AF_PRIVATE | AF_CONST | AF_NODECOMP}, {T("Comjoin"), A_COMJOIN, AF_ODARK | AF_NOPROG}, {T("Comleave"), A_COMLEAVE, AF_ODARK | AF_NOPROG}, {T("Comment"), A_COMMENT, AF_MDARK | AF_WIZARD}, {T("Comoff"), A_COMOFF, AF_ODARK | AF_NOPROG}, {T("Comon"), A_COMON, AF_ODARK | AF_NOPROG}, {T("ConFormat"), A_CONFORMAT, AF_ODARK | AF_NOPROG}, {T("Cost"), A_COST, AF_ODARK | AF_NOPROG}, {T("Created"), A_CREATED, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_NOCLONE | AF_CONST | AF_NODECOMP}, {T("Daily"), A_DAILY, AF_ODARK | AF_NOPROG}, {T("Desc"), A_DESC, AF_VISUAL | AF_NOPROG}, {T("DefaultLock"), A_LOCK, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK | AF_NODECOMP}, {T("DescFormat"), A_DESCFORMAT, AF_ODARK | AF_NOPROG}, {T("Destroyer"), A_DESTROYER, AF_MDARK | AF_WIZARD | AF_NOPROG}, {T("Dfail"), A_DFAIL, AF_ODARK | AF_NOPROG}, {T("Drop"), A_DROP, AF_ODARK | AF_NOPROG}, {T("DropLock"), A_LDROP, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Ealias"), A_EALIAS, AF_ODARK | AF_NOPROG}, {T("Efail"), A_EFAIL, AF_ODARK | AF_NOPROG}, {T("Enter"), A_ENTER, AF_ODARK | AF_NOPROG}, {T("EnterLock"), A_LENTER, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("ExitFormat"), A_EXITFORMAT, AF_ODARK | AF_NOPROG}, {T("ExitTo"), A_EXITVARDEST, AF_ODARK | AF_NOPROG | AF_WIZARD}, {T("Fail"), A_FAIL, AF_ODARK | AF_NOPROG}, {T("Filter"), A_FILTER, AF_ODARK | AF_NOPROG}, {T("Forwardlist"), A_FORWARDLIST, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_CONST}, {T("GetFromLock"), A_LGET, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Gfail"), A_GFAIL, AF_ODARK | AF_NOPROG}, {T("GiveLock"), A_LGIVE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Idesc"), A_IDESC, AF_ODARK | AF_NOPROG}, {T("Idle"), A_IDLE, AF_ODARK | AF_NOPROG}, {T("IdleTimeout"), A_IDLETMOUT, AF_ODARK | AF_NOPROG}, {T("Infilter"), A_INFILTER, AF_ODARK | AF_NOPROG}, {T("Inprefix"), A_INPREFIX, AF_ODARK | AF_NOPROG}, {T("Kill"), A_KILL, AF_ODARK | AF_NOPROG}, {T("Lalias"), A_LALIAS, AF_ODARK | AF_NOPROG}, {T("Last"), A_LAST, AF_WIZARD | AF_NOCMD | AF_NOPROG | AF_NOCLONE | AF_NODECOMP}, {T("Lastpage"), A_LASTPAGE, AF_INTERNAL | AF_NOCMD | AF_NOPROG | AF_GOD | AF_PRIVATE | AF_NODECOMP}, {T("Lastsite"), A_LASTSITE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_NOCLONE | AF_GOD | AF_NODECOMP}, {T("LastIP"), A_LASTIP, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_GOD | AF_NODECOMP}, {T("Leave"), A_LEAVE, AF_ODARK | AF_NOPROG}, {T("LeaveLock"), A_LLEAVE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Lfail"), A_LFAIL, AF_ODARK | AF_NOPROG}, {T("LinkLock"), A_LLINK, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Listen"), A_LISTEN, AF_ODARK | AF_NOPROG}, {T("Logindata"), A_LOGINDATA, AF_MDARK | AF_NOPROG | AF_NOCMD | AF_CONST | AF_NODECOMP}, {T("Mailcurf"), A_MAILCURF, AF_MDARK | AF_WIZARD | AF_NOPROG | AF_NOCLONE | AF_NODECOMP}, {T("Mailflags"), A_MAILFLAGS, AF_MDARK | AF_WIZARD | AF_NOPROG | AF_NOCLONE | AF_NODECOMP}, {T("Mailfolders"), A_MAILFOLDERS, AF_MDARK | AF_WIZARD | AF_NOPROG | AF_NOCLONE | AF_NODECOMP}, {T("MailLock"), A_LMAIL, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Mailmsg"), A_MAILMSG, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL | AF_NODECOMP}, {T("Mailsub"), A_MAILSUB, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL | AF_NODECOMP}, {T("Mailsucc"), A_MAIL, AF_ODARK | AF_NOPROG}, {T("Mailto"), A_MAILTO, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL | AF_NODECOMP}, {T("Mfail"), A_MFAIL, AF_ODARK | AF_NOPROG}, {T("Modified"), A_MODIFIED, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_NOCLONE | AF_CONST | AF_NODECOMP}, {T("Moniker"), A_MONIKER, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_CONST}, {T("Move"), A_MOVE, AF_ODARK | AF_NOPROG}, {T("Name"), A_NAME, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL | AF_NODECOMP}, {T("NameFormat"), A_NAMEFORMAT, AF_ODARK | AF_NOPROG | AF_WIZARD}, {T("Newobjs"), A_NEWOBJS, AF_DARK | AF_NOCMD | AF_NOPROG | AF_NOCLONE | AF_INTERNAL | AF_NODECOMP}, {T("Odesc"), A_ODESC, AF_ODARK | AF_NOPROG}, {T("Odfail"), A_ODFAIL, AF_ODARK | AF_NOPROG}, {T("Odrop"), A_ODROP, AF_ODARK | AF_NOPROG}, {T("Oefail"), A_OEFAIL, AF_ODARK | AF_NOPROG}, {T("Oenter"), A_OENTER, AF_ODARK | AF_NOPROG}, {T("Ofail"), A_OFAIL, AF_ODARK | AF_NOPROG}, {T("Ogfail"), A_OGFAIL, AF_ODARK | AF_NOPROG}, {T("Okill"), A_OKILL, AF_ODARK | AF_NOPROG}, {T("Oleave"), A_OLEAVE, AF_ODARK | AF_NOPROG}, {T("Olfail"), A_OLFAIL, AF_ODARK | AF_NOPROG}, {T("Omove"), A_OMOVE, AF_ODARK | AF_NOPROG}, {T("Opay"), A_OPAY, AF_ODARK | AF_NOPROG}, {T("OpenLock"), A_LOPEN, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Orfail"), A_ORFAIL, AF_ODARK | AF_NOPROG}, {T("Osucc"), A_OSUCC, AF_ODARK | AF_NOPROG}, {T("Otfail"), A_OTFAIL, AF_ODARK | AF_NOPROG}, {T("Otport"), A_OTPORT, AF_ODARK | AF_NOPROG}, {T("Otofail"), A_OTOFAIL, AF_ODARK | AF_NOPROG}, {T("Oufail"), A_OUFAIL, AF_ODARK | AF_NOPROG}, {T("Ouse"), A_OUSE, AF_ODARK | AF_NOPROG}, {T("Oxenter"), A_OXENTER, AF_ODARK | AF_NOPROG}, {T("Oxleave"), A_OXLEAVE, AF_ODARK | AF_NOPROG}, {T("Oxtport"), A_OXTPORT, AF_ODARK | AF_NOPROG}, {T("PageLock"), A_LPAGE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("ParentLock"), A_LPARENT, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Pay"), A_PAY, AF_ODARK | AF_NOPROG}, {T("Prefix"), A_PREFIX, AF_ODARK | AF_NOPROG}, {T("ProgCmd"), A_PROGCMD, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL | AF_NODECOMP}, {T("QueueMax"), A_QUEUEMAX, AF_MDARK | AF_WIZARD | AF_NOPROG}, {T("Quota"), A_QUOTA, AF_MDARK | AF_NOPROG | AF_GOD | AF_NOCMD | AF_NOCLONE | AF_NODECOMP}, {T("ReceiveLock"), A_LRECEIVE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Reject"), A_REJECT, AF_ODARK | AF_NOPROG}, {T("Rfail"), A_RFAIL, AF_ODARK | AF_NOPROG}, {T("Rquota"), A_RQUOTA, AF_MDARK | AF_NOPROG | AF_GOD | AF_NOCMD | AF_NOCLONE | AF_NODECOMP}, {T("Runout"), A_RUNOUT, AF_ODARK | AF_NOPROG}, {T("SayString"), A_SAYSTRING, AF_ODARK | AF_NOPROG}, {T("Semaphore"), A_SEMAPHORE, AF_ODARK | AF_NOPROG | AF_WIZARD | AF_NOCMD | AF_NOCLONE | AF_NODECOMP}, {T("Gmcp"), A_GMCP, AF_ODARK | AF_NOPROG}, {T("Pronoun"), A_PRONOUN, AF_VISUAL | AF_NOPROG}, {T("ProtectName"), A_PROTECTNAME, AF_MDARK | AF_NOPROG | AF_NOCMD | AF_WIZARD}, {T("Sex"), A_SEX, AF_VISUAL | AF_NOPROG}, {T("Signature"), A_SIGNATURE, AF_ODARK | AF_NOPROG}, {T("SpeechMod"), A_SPEECHMOD, AF_ODARK | AF_NOPROG}, {T("SpeechLock"), A_LSPEECH, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Startup"), A_STARTUP, AF_ODARK | AF_NOPROG}, {T("Succ"), A_SUCC, AF_ODARK | AF_NOPROG}, {T("TeloutLock"), A_LTELOUT, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Tfail"), A_TFAIL, AF_ODARK | AF_NOPROG}, {T("Timeout"), A_TIMEOUT, AF_MDARK | AF_NOPROG | AF_WIZARD}, {T("Tport"), A_TPORT, AF_ODARK | AF_NOPROG}, {T("TportLock"), A_LTPORT, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("Tofail"), A_TOFAIL, AF_ODARK | AF_NOPROG}, {T("Ufail"), A_UFAIL, AF_ODARK | AF_NOPROG}, {T("Use"), A_USE, AF_ODARK | AF_NOPROG}, {T("UseLock"), A_LUSE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("UserLock"), A_LUSER, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("VisibleLock"), A_LVISIBLE, AF_ODARK | AF_NOPROG | AF_NOCMD | AF_IS_LOCK}, {T("VA"), A_VA, AF_ODARK}, {T("VB"), A_VA + 1, AF_ODARK}, {T("VC"), A_VA + 2, AF_ODARK}, {T("VD"), A_VA + 3, AF_ODARK}, {T("VE"), A_VA + 4, AF_ODARK}, {T("VF"), A_VA + 5, AF_ODARK}, {T("VG"), A_VA + 6, AF_ODARK}, {T("VH"), A_VA + 7, AF_ODARK}, {T("VI"), A_VA + 8, AF_ODARK}, {T("VJ"), A_VA + 9, AF_ODARK}, {T("VK"), A_VA + 10, AF_ODARK}, {T("VL"), A_VA + 11, AF_ODARK}, {T("VM"), A_VA + 12, AF_ODARK}, {T("VN"), A_VA + 13, AF_ODARK}, {T("VO"), A_VA + 14, AF_ODARK}, {T("VP"), A_VA + 15, AF_ODARK}, {T("VQ"), A_VA + 16, AF_ODARK}, {T("VR"), A_VA + 17, AF_ODARK}, {T("VS"), A_VA + 18, AF_ODARK}, {T("VT"), A_VA + 19, AF_ODARK}, {T("VU"), A_VA + 20, AF_ODARK}, {T("VV"), A_VA + 21, AF_ODARK}, {T("VW"), A_VA + 22, AF_ODARK}, {T("VX"), A_VA + 23, AF_ODARK}, {T("VY"), A_VA + 24, AF_ODARK}, {T("VZ"), A_VA + 25, AF_ODARK}, {T("VRML_URL"), A_VRML_URL, AF_ODARK | AF_NOPROG}, {T("HTDesc"), A_HTDESC, AF_NOPROG}, {T("Reason"), A_REASON, AF_PRIVATE | AF_MDARK | AF_NOPROG | AF_NOCMD | AF_GOD | AF_NODECOMP}, {T("ConnInfo"), A_CONNINFO, AF_PRIVATE | AF_MDARK | AF_NOPROG | AF_NOCMD | AF_GOD | AF_NODECOMP}, #ifdef REALITY_LVLS {T("Rlevel"), A_RLEVEL, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL}, #endif // REALITY_LVLS {nullptr, 0, 0} }; // The following 'special' attributes adopt invalid names to make them // inaccessible to softcode. A small price of this is that we must // manually upper-case them in the table. // ATTR AttrTableSpecial[] = { {T("*PASSWORD"), A_PASS, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL}, {T("*PRIVILEGES"), A_PRIVS, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL}, {T("*MONEY"), A_MONEY, AF_DARK | AF_NOPROG | AF_NOCMD | AF_INTERNAL}, {nullptr, 0, 0} }; const UTF8 *aszSpecialDBRefNames[1-NOPERM] = { T(""), T("*NOTHING*"), T("*AMBIGUOUS*"), T("*HOME*"), T("*NOPERMISSION*") }; /* --------------------------------------------------------------------------- * fwdlist_set, fwdlist_clr: Manage cached forwarding lists */ void fwdlist_set(dbref thing, FWDLIST *ifp) { // If fwdlist is null, clear. // if ( nullptr == ifp || ifp->count <= 0) { fwdlist_clr(thing); return; } // Copy input forwardlist to a correctly-sized buffer. // FWDLIST *fp = nullptr; try { fp = new FWDLIST; } catch (...) { ; // Nothing. } if (nullptr != fp) { fp->data = nullptr; try { fp->data = new dbref[ifp->count]; } catch (...) { ; // Nothing. } if (nullptr != fp->data) { for (int i = 0; i < ifp->count; i++) { fp->data[i] = ifp->data[i]; } fp->count = ifp->count; // Replace an existing forwardlist, or add a new one. // bool done; FWDLIST *xfp = fwdlist_get(thing); if (xfp) { delete [] xfp->data; delete xfp; xfp = nullptr; } mudstate.forward_lists[thing] = fp; } else { STARTLOG(LOG_PROBLEMS, "FWD", "MEM"); log_printf(T("fwdlist_set: out of memory allocating forward list data for #%d."), thing); ENDLOG; delete fp; } } else { STARTLOG(LOG_PROBLEMS, "FWD", "MEM"); log_printf(T("fwdlist_set: out of memory allocating forward list for #%d."), thing); ENDLOG; } } void fwdlist_clr(dbref thing) { // If a forwardlist exists, delete it // FWDLIST *xfp = fwdlist_get(thing); if (xfp) { delete [] xfp->data; delete xfp; xfp = nullptr; const auto it = mudstate.forward_lists.find(thing); mudstate.forward_lists.erase(it); } } /* --------------------------------------------------------------------------- * fwdlist_load: Load text into a forwardlist. */ FWDLIST *fwdlist_load(dbref player, UTF8 *atext) { FWDLIST *fp = nullptr; try { fp = new FWDLIST; } catch (...) { ; // Nothing. } if (nullptr != fp) { fp->count = 0; fp->data = nullptr; try { fp->data = new dbref[LBUF_SIZE/2]; } catch (...) { ; // Nothing. } if (nullptr != fp->data) { LBuf tp = LBuf_Src("fwdlist_load.str"); UTF8 *bp = tp.get(); mux_strncpy(tp, atext, LBUF_SIZE-1); int count = 0; do { // Skip spaces. // for (; mux_isspace(*bp); bp++) { ; // Nothing. } // Remember string. // UTF8 *dp; for (dp = bp; *bp && !mux_isspace(*bp); bp++) { ; // Nothing. } // Terminate string. // if (*bp) { *bp++ = '\0'; } if ( *dp++ == '#' && mux_isdigit(*dp)) { bool fail; dbref target = mux_atoi64(dp); if (mudstate.bStandAlone) { fail = !Good_obj(target); } else { fail = ( !Good_obj(target) || ( !God(player) && !Controls(player, target) && ( !Link_ok(target) || !could_doit(player, target, A_LLINK)))); } if (fail) { if (!mudstate.bStandAlone) { notify(player, tprintf(M_("Cannot forward to #%d: Permission denied."), target)); } } else { fp->data[count++] = target; } } } while (*bp); if (0 < count) { fp->count = count; } else { delete [] fp->data; delete fp; fp = nullptr; } } else { delete fp; fp = nullptr; } } return fp; } /* --------------------------------------------------------------------------- * fwdlist_rewrite: Generate a text string from a FWDLIST buffer. */ int fwdlist_rewrite(FWDLIST *fp, UTF8 *atext) { int count = 0; atext[0] = '\0'; if ( fp && fp->count) { UTF8 *bp = atext; ITL pContext; ItemToList_Init(&pContext, atext, &bp, '#'); for (int i = 0; i < fp->count; i++) { if ( Good_obj(fp->data[i]) && ItemToList_AddInteger(&pContext, fp->data[i])) { count++; } } ItemToList_Final(&pContext); } return count; } /* --------------------------------------------------------------------------- * fwdlist_ck: Check a list of dbref numbers to forward to for AUDIBLE */ bool fwdlist_ck(dbref player, dbref thing, int anum, UTF8 *atext) { UNUSED_PARAMETER(anum); if (mudstate.bStandAlone) { return true; } FWDLIST *fp = nullptr; if ( nullptr != atext && '\0' != atext[0]) { fp = fwdlist_load(player, atext); } // Set the cached forwardlist. // fwdlist_set(thing, fp); int count = fwdlist_rewrite(fp, atext); if (nullptr != fp) { if (nullptr != fp->data) { delete [] fp->data; } delete fp; fp = nullptr; } return ( count > 0 || nullptr == atext || '\0' == atext[0]); } FWDLIST *fwdlist_get(dbref thing) { static FWDLIST *fp = nullptr; if (mudstate.bStandAlone) { dbref aowner; int aflags; LBuf tp = LBuf_Adopt(atr_get("fwdlist_get.543", thing, A_FORWARDLIST, &aowner, &aflags)); fp = fwdlist_load(GOD, tp); } else { const auto it = mudstate.forward_lists.find(thing); if (it != mudstate.forward_lists.end()) fp = it->second; else fp = nullptr; } return fp; } // --------------------------------------------------------------------------- // Name, PureName, Moniker, s_Moniker, and s_Name: Get or set object's // various names. // const UTF8 *Name(dbref thing) { if (thing < 0) { return aszSpecialDBRefNames[-thing]; } dbref aowner; int aflags; if (!db[thing].name) { size_t len; LBuf pName = LBuf_Adopt(atr_get_LEN(thing, A_NAME, &aowner, &aflags, &len)); db[thing].name = StringCloneLen(pName, len); } return db[thing].name; } const UTF8 *PureName(dbref thing) { if (thing < 0) { return aszSpecialDBRefNames[-thing]; } dbref aowner; int aflags; UTF8 *pName, *pPureName; if (mudconf.cache_names) { if (!db[thing].purename) { size_t nName; size_t nPureName; if (!db[thing].name) { pName = atr_get_LEN(thing, A_NAME, &aowner, &aflags, &nName); db[thing].name = StringCloneLen(pName, nName); free_lbuf(pName); } else { nName = strlen(reinterpret_cast(db[thing].name)); } pName = db[thing].name; pPureName = strip_color(pName, &nPureName); if (nPureName == nName) { db[thing].purename = pName; } else { db[thing].purename = StringCloneLen(pPureName, nPureName); } } return db[thing].purename; } pName = atr_get("PureName.631", thing, A_NAME, &aowner, &aflags); pPureName = strip_color(pName); free_lbuf(pName); return pPureName; } const UTF8 *Moniker(dbref thing) { if (thing < 0) { return aszSpecialDBRefNames[-thing]; } if (db[thing].moniker) { return db[thing].moniker; } // Compare accent-stripped, ansi-stripped version of @moniker against // accent-stripped, ansi-stripped version of @name. // const UTF8 *pPureName = ConvertToAscii(PureName(thing)); UTF8 *pPureNameCopy = StringClone(pPureName); size_t nMoniker; dbref aowner; int aflags; LBuf pMoniker = LBuf_Adopt(atr_get_LEN(thing, A_MONIKER, &aowner, &aflags, &nMoniker)); const UTF8 *pPureMoniker = ConvertToAscii(strip_color(pMoniker)); const UTF8 *pReturn = nullptr; thread_local UTF8 tbuff[LBUF_SIZE]; if (strcmp(reinterpret_cast(pPureNameCopy), reinterpret_cast(pPureMoniker)) == 0) { // The stripped version of @moniker is the same as the stripped // version of @name, so (possibly cache and) use the unstripped // version of @moniker. // if (mudconf.cache_names) { if (strcmp(reinterpret_cast(pMoniker.get()), reinterpret_cast(Name(thing))) == 0) { db[thing].moniker = db[thing].name; } else { db[thing].moniker = StringCloneLen(pMoniker, nMoniker); } pReturn = db[thing].moniker; } else { memcpy(tbuff, pMoniker, nMoniker+1); pReturn = tbuff; } } else { // @moniker can't be used, so instead reflect @name (whether it // contains ANSI color and accents or not). // if (mudconf.cache_names) { db[thing].moniker = db[thing].name; pReturn = db[thing].moniker; } else { pReturn = Name(thing); } } MEMFREE(pPureNameCopy); return pReturn; } void free_Names(OBJ *p) { if (p->name) { if (mudconf.cache_names) { if (p->name == p->purename) { p->purename = nullptr; } if (p->name == p->moniker) { p->moniker = nullptr; } } MEMFREE(p->name); p->name = nullptr; } if (mudconf.cache_names) { if (p->purename) { MEMFREE(p->purename); p->purename = nullptr; } if (p->moniker) { MEMFREE(p->moniker); p->moniker = nullptr; } } } void s_Name(dbref thing, const UTF8 *s) { if (!atr_add_raw(thing, A_NAME, s)) { STARTLOG(LOG_PROBLEMS, "DB", "ATTRSYNC"); log_printf(T("s_Name(#%d): failed to persist A_NAME; leaving in-memory name unchanged."), thing); ENDLOG; return; } // The object's name is what the index buckets on (#2058). name_index_invalidate(db[thing].location); free_Names(&db[thing]); if (nullptr != s) { db[thing].name = StringClone(s); } } void free_Moniker(OBJ *p) { if (mudconf.cache_names) { if (p->name == p->moniker) { p->moniker = nullptr; } if (p->moniker) { MEMFREE(p->moniker); p->moniker = nullptr; } } } void s_Moniker(dbref thing, const UTF8 *s) { if (!atr_add_raw(thing, A_MONIKER, s)) { STARTLOG(LOG_PROBLEMS, "DB", "ATTRSYNC"); log_printf(T("s_Moniker(#%d): failed to persist A_MONIKER."), thing); ENDLOG; return; } free_Moniker(&db[thing]); } void s_Pass(dbref thing, const UTF8 *s) { if (!atr_add_raw(thing, A_PASS, s)) { STARTLOG(LOG_PROBLEMS, "DB", "ATTRSYNC"); log_printf(T("s_Pass(#%d): failed to persist A_PASS."), thing); ENDLOG; } } /* --------------------------------------------------------------------------- * do_attrib: Manage user-named attributes. */ void do_attribute ( dbref executor, dbref caller, dbref enactor, int eval, int key, int nargs, UTF8 *aname, UTF8 *value, const UTF8 *cargs[], int ncargs ) { UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(nargs); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); // Look up the user-named attribute we want to play with. // size_t nName; bool bValid = false; ATTR *va = nullptr; UTF8 *pName = MakeCanonicalAttributeName(aname, &nName, &bValid); if (bValid) { va = reinterpret_cast(vattr_find_LEN(pName, nName)); } if (nullptr == va) { notify(executor, M_("No such user-named attribute.")); return; } int f; UTF8 *sp; ATTR *va2; bool negate, success; size_t nCased; UTF8 *pCased; switch (key) { case ATTRIB_ACCESS: { // Modify access to user-named attribute // pCased = mux_strupr(value, nCased); string_token st(pCased, T(" ")); sp = st.parse(); success = false; while (sp != nullptr) { // Check for negation. // negate = false; if (*sp == '!') { negate = true; sp++; } // Set or clear the appropriate bit. // if (search_nametab(executor, attraccess_nametab, sp, &f)) { success = true; if (negate) { va->flags &= ~f; } else { va->flags |= f; } } else { notify(executor, tprintf(M_("Unknown permission: %s."), sp)); } // Get the next token. // sp = st.parse(); } if (success && !Quiet(executor)) { notify(executor, M_("Attribute access changed.")); } } break; case ATTRIB_RENAME: { // Save the old name for use later. // UTF8 OldName[SBUF_SIZE]; UTF8 *pOldName = OldName; safe_sb_str(pName, OldName, &pOldName); *pOldName = '\0'; size_t nOldName = pOldName - OldName; pName = MakeCanonicalAttributeName(value, &nName, &bValid); if (!bValid) { notify(executor, M_("Attribute rename failed.")); return; } // Make sure the canonical target name doesn't already exist, // unless it resolves to the same attribute. // va2 = atr_str(pName); if ( va2 && va2 != va) { notify(executor, M_("An attribute with that name already exists.")); return; } if (vattr_rename_LEN(OldName, nOldName, pName, nName) == nullptr) { notify(executor, M_("Attribute rename failed.")); } else { notify(executor, M_("Attribute renamed.")); } } break; case ATTRIB_DELETE: // Remove the attribute. // vattr_delete_LEN(pName, nName); notify(executor, M_("Attribute deleted.")); break; } } /* --------------------------------------------------------------------------- * do_fixdb: Directly edit database fields */ void do_fixdb ( dbref executor, dbref caller, dbref enactor, int eval, int key, int nargs, UTF8 *arg1, UTF8 *arg2, const UTF8 *cargs[], int ncargs ) { UNUSED_PARAMETER(caller); UNUSED_PARAMETER(enactor); UNUSED_PARAMETER(eval); UNUSED_PARAMETER(nargs); UNUSED_PARAMETER(cargs); UNUSED_PARAMETER(ncargs); init_match(executor, arg1, NOTYPE); match_everything(0); dbref thing = noisy_match_result(); if (thing == NOTHING) { return; } dbref res = NOTHING; switch (key) { case FIXDB_OWNER: case FIXDB_LOC: case FIXDB_CON: case FIXDB_EXITS: case FIXDB_NEXT: init_match(executor, arg2, NOTYPE); match_everything(0); res = noisy_match_result(); break; case FIXDB_PENNIES: res = mux_atoi64(arg2); break; } UTF8 *pValidName; switch (key) { case FIXDB_OWNER: s_Owner(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Owner set to #%d"), res)); break; case FIXDB_LOC: s_Location(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Location set to #%d"), res)); break; case FIXDB_CON: s_Contents(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Contents set to #%d"), res)); break; case FIXDB_EXITS: s_Exits(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Exits set to #%d"), res)); break; case FIXDB_NEXT: s_Next(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Next set to #%d"), res)); break; case FIXDB_PENNIES: s_Pennies(thing, res); if (!Quiet(executor)) notify(executor, tprintf(M_("Pennies set to %d"), res)); break; case FIXDB_NAME: if (isPlayer(thing)) { if (!ValidatePlayerName(arg2)) { notify(executor, M_("That’s not a good name for a player.")); return; } bool bAlias = false; pValidName = arg2; if ( lookup_player_name(pValidName, bAlias) != NOTHING || bAlias) { notify(executor, M_("That name is already in use or is an alias.")); return; } STARTLOG(LOG_SECURITY, "SEC", "CNAME"); log_name(thing), log_text(T(" renamed to ")); log_text(pValidName); ENDLOG; if (Suspect(executor)) { raw_broadcast(WIZARD, M_("[Suspect] %s renamed to %s"), Name(thing), pValidName); } delete_player_name(thing, Name(thing), false); s_Name(thing, pValidName); add_player_name(thing, pValidName, false); } else { size_t nTmp; bool bValid; pValidName = MakeCanonicalObjectName(arg2, &nTmp, &bValid, 0); if (!bValid) { notify(executor, M_("That is not a reasonable name.")); return; } s_Name(thing, pValidName); } if (!Quiet(executor)) { notify(executor, tprintf(M_("Name set to %s"), pValidName)); } break; } } // MakeCanonicalAttributeName // // See stringutil.cpp for valid characters used here.. // // We truncate the attribute name to a length of SBUF_SIZE-1, if // necessary, but we will validate the remaining characters anyway. // UTF8 *MakeCanonicalAttributeName(const UTF8 *pName_arg, size_t *pnName, bool *pbValid) { thread_local UTF8 Buffer[SBUF_SIZE]; const UTF8 *pName = pName_arg; if ( nullptr == pName || !mux_isattrnameinitial(pName)) { *pnName = 0; *pbValid = false; return nullptr; } size_t nLeft = SBUF_SIZE-1; UTF8 *p = Buffer; size_t n; while ( '\0' != *pName && (n = utf8_FirstByte[static_cast(*pName)]) < UTF8_CONTINUE && n <= nLeft) { if (!mux_isattrname(pName)) { *pnName = 0; *pbValid = false; return Buffer; } for (size_t j = 1; j < n; j++) { if ( '\0' == pName[j] || UTF8_CONTINUE != utf8_FirstByte[static_cast(pName[j])]) { *pnName = 0; *pbValid = false; return Buffer; } } bool bXor; const string_desc *qDesc = mux_toupper(pName, bXor); size_t m = n; if (nullptr == qDesc) { if (m > nLeft) { break; } for (size_t j = 0; j < m; j++) { *p++ = pName[j]; } } else { m = qDesc->n_bytes; const UTF8 *q = qDesc->p; if ( bXor && m != n) { *pnName = 0; *pbValid = false; return Buffer; } if (m > nLeft) { break; } if (bXor) { for (size_t j = 0; j < m; j++) { *p++ = pName[j] ^ *q++; } } else { for (size_t j = 0; j < m; j++) { *p++ = *q++; } } } nLeft -= m; pName += n; } *p = '\0'; // Continue to validate remaining characters even though // we aren't going to use them. This helps to ensure that // softcode will run in the future if we increase the // size of SBUF_SIZE. // while ('\0' != *pName) { if ( UTF8_CONTINUE <= utf8_FirstByte[static_cast(*pName)] || !mux_isattrname(pName)) { *pnName = 0; *pbValid = false; return Buffer; } size_t n = utf8_FirstByte[static_cast(*pName)]; if (n < 1 || n >= UTF8_CONTINUE) { *pnName = 0; *pbValid = false; return Buffer; } for (size_t i = 1; i < n; i++) { if ( '\0' == pName[i] || UTF8_CONTINUE != utf8_FirstByte[static_cast(pName[i])]) { *pnName = 0; *pbValid = false; return Buffer; } } pName += n; } if (IsRestricted(pName_arg, mudconf.attr_name_charset)) { *pnName = 0; *pbValid = false; return Buffer; } // Length of truncated result. // *pnName = p - Buffer; *pbValid = true; return Buffer; } // MakeCanonicalAttributeCommand // UTF8 *MakeCanonicalAttributeCommand(const UTF8 *pName, size_t *pnName, bool *pbValid) { if (nullptr == pName) { *pnName = 0; *pbValid = false; return nullptr; } thread_local UTF8 Buffer[SBUF_SIZE]; size_t nLeft = SBUF_SIZE-2; UTF8 *p = Buffer; size_t n; *p++ = '@'; while ( '\0' != *pName && (n = utf8_FirstByte[static_cast(*pName)]) < UTF8_CONTINUE) { if (n > nLeft) { break; } for (size_t j = 1; j < n; j++) { if ( '\0' == pName[j] || UTF8_CONTINUE != utf8_FirstByte[static_cast(pName[j])]) { *pnName = 0; *pbValid = false; *p = '\0'; return Buffer; } } bool bXor; const string_desc *qDesc = mux_tolower(pName, bXor); size_t m = n; if (nullptr == qDesc) { if (m > nLeft) { break; } for (size_t j = 0; j < m; j++) { *p++ = pName[j]; } } else { m = qDesc->n_bytes; const UTF8 *q = qDesc->p; if ( bXor && m != n) { *pnName = 0; *pbValid = false; *p = '\0'; return Buffer; } if (m > nLeft) { break; } if (bXor) { for (size_t j = 0; j < m; j++) { *p++ = pName[j] ^ *q++; } } else { for (size_t j = 0; j < m; j++) { *p++ = *q++; } } } nLeft -= m; pName += n; } *p = '\0'; if ( '\0' != *pName && UTF8_CONTINUE <= utf8_FirstByte[static_cast(*pName)]) { *pnName = 0; *pbValid = false; return Buffer; } // Length of result. // *pnName = p - Buffer; // Is the result valid? // *pbValid = (*pnName > 1); // Pointer to result // return Buffer; } /* --------------------------------------------------------------------------- * init_attrtab: Initialize the attribute hash tables. */ void init_attrtab(void) { ATTR *a; for (a = AttrTable; a->number; a++) { size_t nLen; bool bValid; const UTF8 *buff = MakeCanonicalAttributeName(a->name, &nLen, &bValid); if (!bValid) { continue; } anum_extend(a->number); anum_set(a->number, a); vector v(buff, buff + nLen); mudstate.builtin_attribute_names.insert(make_pair(v, a)); } // We specifically allow the '*' character at server // initialization because it's part of the A_PASS attribute // name. // for (a = AttrTableSpecial; a->number; a++) { anum_extend(a->number); anum_set(a->number, a); const UTF8* buff = a->name; const size_t n = strlen(reinterpret_cast(buff)); vector v(buff, buff+n); mudstate.builtin_attribute_names.insert(make_pair(v, a)); } } /* --------------------------------------------------------------------------- * atr_str: Look up an attribute by name. */ ATTR *atr_str(const UTF8 *s) { // Make attribute name canonical. // size_t nBuffer; bool bValid; UTF8 *buff = MakeCanonicalAttributeName(s, &nBuffer, &bValid); if (!bValid) { return nullptr; } // Look for a predefined attribute. // const vector v(buff, buff + nBuffer); const auto it = mudstate.builtin_attribute_names.find(v); if (it != mudstate.builtin_attribute_names.end()) { return it->second; } // Nope, look for a user attribute. // return vattr_find_LEN(buff, nBuffer); } int GrowFiftyPercent(int x, int low, int high) { if (x < 0) { x = 0; } // Calcuate 150% of x clipping goal at INT_MAX. // int half = x >> 1; int goal; if (INT_MAX - half <= x) { goal = INT_MAX; } else { goal = x + half; } // Clip result between requested boundaries. // if (goal < low) { goal = low; } else if (high < goal) { goal = high; } return goal; } /* --------------------------------------------------------------------------- * anum_extend: Grow the attr num lookup table. */ ATTR **anum_table = nullptr; int anum_alc_top = -1; void anum_extend(int newtop) { if (newtop <= anum_alc_top) { return; } int delta; if (mudstate.bStandAlone) { delta = 1000; } else { delta = mudconf.init_size; } int h = GrowFiftyPercent(anum_alc_top, delta, INT_MAX); if (newtop < h) { newtop = h; } int i; ATTR **anum_table2 = static_cast(MEMALLOC((newtop + 1) * sizeof(ATTR *))); if (nullptr != anum_table2) { for (i = anum_alc_top + 1; i <= newtop; i++) { anum_table2[i] = nullptr; } if (nullptr != anum_table) { for (i = 0; i <= anum_alc_top; i++) { anum_table2[i] = anum_table[i]; } MEMFREE(anum_table); } anum_table = anum_table2; } else { mux_assert(anum_table2); } anum_alc_top = newtop; } // -------------------------------------------------------------------------- // atr_num: Look up an attribute by number. // ATTR *atr_num(int anum) { if ( anum < 0 || anum_alc_top < anum) { return nullptr; } return anum_get(anum); } static void SetupThrottle(dbref executor) { CLinearTimeAbsolute tNow; CLinearTimeDelta ltdHour; ltdHour.SetSeconds(60*60); tNow.GetUTC(); db[executor].tThrottleExpired = tNow + ltdHour; s_ThAttrib(executor, mudconf.vattr_per_hour); s_ThMail(executor, mudconf.mail_per_hour); s_ThRefs(executor, mudconf.references_per_hour); s_ThEmail(executor, mudconf.email_per_hour); } static void SetupGlobalThrottle(void) { CLinearTimeAbsolute tNow; CLinearTimeDelta ltdHour; ltdHour.SetSeconds(60*60); tNow.GetUTC(); mudstate.tThrottleExpired = tNow + ltdHour; mudstate.pcreates_this_hour = mudconf.pcreate_per_hour; } bool ThrottlePlayerCreate(void) { if (0 < mudstate.pcreates_this_hour) { mudstate.pcreates_this_hour--; return false; } CLinearTimeAbsolute tNow; tNow.GetUTC(); if (mudstate.tThrottleExpired <= tNow) { SetupGlobalThrottle(); return false; } return true; } bool ThrottleAttributeNames(dbref executor) { if (0 < ThAttrib(executor)) { s_ThAttrib(executor, ThAttrib(executor)-1); return false; } CLinearTimeAbsolute tNow; tNow.GetUTC(); if (db[executor].tThrottleExpired <= tNow) { SetupThrottle(executor); return false; } return true; } bool ThrottleMail(dbref executor) { if (0 < ThMail(executor)) { s_ThMail(executor, ThMail(executor)-1); return false; } CLinearTimeAbsolute tNow; tNow.GetUTC(); if (db[executor].tThrottleExpired <= tNow) { SetupThrottle(executor); return false; } return true; } bool ThrottleEmail(dbref executor) { if (0 < ThEmail(executor)) { s_ThEmail(executor, ThEmail(executor)-1); return false; } CLinearTimeAbsolute tNow; tNow.GetUTC(); if (db[executor].tThrottleExpired <= tNow) { SetupThrottle(executor); return false; } return true; } bool ThrottleReferences(dbref executor) { if (0 < ThRefs(executor)) { s_ThRefs(executor, ThRefs(executor)-1); return false; } CLinearTimeAbsolute tNow; tNow.GetUTC(); if (db[executor].tThrottleExpired <= tNow) { SetupThrottle(executor); return false; } return true; } /* --------------------------------------------------------------------------- * mkattr: Lookup attribute by name, creating if needed. */ int mkattr(dbref executor, const UTF8 *buff) { ATTR *ap = atr_str(buff); if (!ap) { // Unknown attribute name, create a new one. // size_t nName; bool bValid; UTF8 *pName = MakeCanonicalAttributeName(buff, &nName, &bValid); ATTR *va; if (bValid) { if ( !Wizard(executor) && ThrottleAttributeNames(executor)) { return -1; } int aflags = mudconf.vattr_flags; if (pName[0] == '_') { // An attribute that begins with an underline is // hidden from mortals and only changeable by // WIZARDs. // aflags |= AF_MDARK | AF_WIZARD; } va = vattr_alloc_LEN(pName, nName, aflags); if (va && va->number) { return va->number; } } return -1; } else if (ap->number) { return ap->number; } return -1; } /* --------------------------------------------------------------------------- * al_decode: Fetch an attribute number from an alist. */ static int al_decode(unsigned char **app) { unsigned char *ap = *app; int atrnum = 0, atrshft = 0; for (;;) { int ch = *ap++; int bits = ch & 0x7F; if (atrshft > 0) atrnum |= (bits << atrshft); else atrnum = bits; if ((ch & 0x80) == 0) { *app = ap; return atrnum; } atrshft += 7; } } /* --------------------------------------------------------------------------- * Commer: check if an object has any $-commands in its attributes. */ bool Commer(dbref thing) { if (mudstate.bfNoCommands.IsSet(thing)) { // We already know that there are no commands on this thing. // return false; } else if (mudstate.bfCommands.IsSet(thing)) { // We already know that there are definitely commands on this thing. // return true; } bool bFoundListens = false; atr_push(); LBuf buff = LBuf_Src("Commer"); unsigned char *as; for (int atr = atr_head(thing, &as); atr; atr = atr_next(&as)) { ATTR *ap = atr_num(atr); if ( !ap || (ap->flags & AF_NOPROG)) { continue; } int aflags; dbref aowner; atr_get_str(buff, thing, atr, &aowner, &aflags); if (aflags & AF_NOPROG) { continue; } if ( AMATCH_CMD != buff[0] && AMATCH_LISTEN != buff[0]) { continue; } // Search for unescaped ':' // UTF8 *s = find_pattern_delimiter(buff + 1); if (!s) { continue; } if (AMATCH_CMD == buff[0]) { atr_pop(); mudstate.bfCommands.Set(thing); if (bFoundListens) { mudstate.bfListens.Set(thing); mudstate.bfNoListens.Clear(thing); } return true; } else // AMATCH_LISTEN == buff[0] { bFoundListens = true; } } atr_pop(); mudstate.bfNoCommands.Set(thing); if (bFoundListens) { mudstate.bfListens.Set(thing); mudstate.bfNoListens.Clear(thing); } else { mudstate.bfNoListens.Set(thing); mudstate.bfListens.Clear(thing); } return false; } // Collect attribute numbers for an object from storage. // static void collect_attrnums_from_storage(dbref thing, vector& attrnums) { attrnums.clear(); if (!g_pSQLiteBackend) { return; } // atr_head/atr_next land here, and $-command matching reaches it once per // object in scope on every typed command. Answering with a GetAll() query // every time made that one SQLite round trip per object per command -- // ~67% of $-dispatch was inside SQLite and its file locking (#2077). // // Only the storage half is cached; the pending overlay below still runs on // every lookup, because it changes as the write queue drains. // if (cache_lookup_attrnum_list(thing, attrnums)) { cache_collect_pending_attrnums(thing, attrnums); return; } if (!g_pSQLiteBackend->GetAll( static_cast(thing), [&attrnums](unsigned int attrnum, const UTF8 *, size_t, int, int) { // Skip legacy internal packed attribute list. // if ( attrnum != static_cast(A_LIST) && attrnum != 0U) { attrnums.push_back(static_cast(attrnum)); } })) { Log.tinyprintf(T("collect_attrnums_from_storage: failed to enumerate attrs for #%d" ENDLINE), thing); attrnums.clear(); return; } // Store the storage half BEFORE the overlay is applied -- the overlay is // transient and must not be cached with it (#2077). // cache_store_attrnum_list(thing, attrnums); // Include dirty non-tombstone cache / write-queue puts, and drop // tombstoned attrs that SQLite has not yet seen deleted (#1045). // cache_collect_pending_attrnums(thing, attrnums); } static inline void makekey(dbref thing, int atr, Aname *abuff) { abuff->object = thing; abuff->attrnum = atr; return; } /* --------------------------------------------------------------------------- * atr_encode: Encode an attribute string. */ static const UTF8 *atr_encode(const UTF8 *iattr, dbref thing, dbref owner, int flags, int atr) { UNUSED_PARAMETER(atr); // If using the default owner and flags (almost all attributes will), // just store the string. // if (((owner == Owner(thing)) || (owner == NOTHING)) && !flags) return iattr; // Encode owner and flags into the attribute text. // if (owner == NOTHING) owner = Owner(thing); return tprintf(T("%c%d:%d:%s"), ATR_INFO_CHAR, owner, flags, iattr); } // atr_decode_flags_owner: Decode the owner and flags (if present) and // return a pointer to the attribute text. // const UTF8 *atr_decode_flags_owner(const UTF8 *iattr, dbref *owner, int *flags) { // See if the first char of the attribute is the special character // *flags = 0; if (*iattr != ATR_INFO_CHAR) { return iattr; } // It has the special character, crack the attr apart. // const UTF8 *cp = iattr + 1; // Get the attribute owner // bool neg = false; if (*cp == '-') { neg = true; cp++; } int tmp_owner = 0; unsigned int ch = *cp; while (mux_isdigit(ch)) { cp++; tmp_owner = 10*tmp_owner + (ch-'0'); ch = *cp; } if (neg) { tmp_owner = -tmp_owner; } // If delimiter is not ':', just return attribute // if (*cp++ != ':') { return iattr; } // Get the attribute flags. // int tmp_flags = 0; ch = *cp; while (mux_isdigit(ch)) { cp++; tmp_flags = 10*tmp_flags + (ch-'0'); ch = *cp; } // If delimiter is not ':', just return attribute. // if (*cp++ != ':') { return iattr; } // Get the attribute text. // if (tmp_owner != NOTHING) { *owner = tmp_owner; } *flags = tmp_flags; return cp; } // --------------------------------------------------------------------------- // atr_decode: Decode an attribute string. // static void atr_decode_LEN(const UTF8 *iattr, size_t nLen, UTF8 *oattr, dbref thing, dbref *owner, int *flags, size_t *pLen) { // Default the owner // *owner = Owner(thing); // Parse for owner and flags // const UTF8 *cp = atr_decode_flags_owner(iattr, owner, flags); // Get the attribute text. // *pLen = nLen - (cp - iattr); if (oattr) { memcpy(oattr, cp, (*pLen) + 1); } } /* --------------------------------------------------------------------------- * atr_clr: clear an attribute in the list. */ bool atr_clr(dbref thing, int atr) { Aname okey; makekey(thing, atr, &okey); if (!cache_del(&okey)) { Log.tinyprintf(T("atr_clr(#%d/%d): SQLite delete failed, not applying side effects." ENDLINE), thing, atr); return false; } // Increment AFTER successful deletion. The in-memory counter // survives the SQLite row removal, so a subsequent recreate // of the same attr gets a higher counter. attr_mod_count_inc(thing, atr); if ( atr == A_EXITVARDEST && isExit(thing)) { route_invalidate(); } switch (atr) { case A_STARTUP: s_Flags(thing, FLAG_WORD1, db[thing].fs.word[FLAG_WORD1] & ~HAS_STARTUP); break; case A_DAILY: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] & ~HAS_DAILY); break; case A_FORWARDLIST: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] & ~HAS_FWDLIST); if (!mudstate.bStandAlone) { // We should clear the hashtable, too. // fwdlist_clr(thing); } break; case A_LISTEN: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] & ~HAS_LISTEN); break; case A_TIMEOUT: desc_reload(thing); break; case A_QUEUEMAX: pcache_reload(thing); break; default: // Since this could overwrite an existing ^-Command or $-Command, we // longer assert that the object has one. // mudstate.bfListens.Clear(thing); mudstate.bfCommands.Clear(thing); break; } return true; } /* --------------------------------------------------------------------------- * atr_add_raw, atr_add: add attribute of type atr to list */ bool atr_add_raw_LEN(dbref thing, int atr, const UTF8 *szValue, size_t nValue) { if ( !szValue || '\0' == szValue[0]) { return atr_clr(thing, atr); } if (nValue > LBUF_SIZE-1) { nValue = LBUF_SIZE-1; } Aname okey; makekey(thing, atr, &okey); // SQLite mode: decode any packed owner/flags prefix from the value // before storing in separate columns. If no prefix is present (the // common case), defaults apply. // dbref raw_owner = Owner(thing); int raw_flags = 0; const UTF8 *clean = atr_decode_flags_owner(szValue, &raw_owner, &raw_flags); size_t clean_len = nValue - static_cast(clean - szValue); // Normalize user text to NFC for consistent storage. // Legacy internal packed attribute list is deprecated and ignored. // NFC never expands the string, so clean_len is a safe bound. // if (atr == A_LIST) { return true; } // Enforce per-object attribute count limit. // Wizards bypass the limit. A vlimit of 0 means unlimited. // const bool bypass_vlimit = Good_obj(raw_owner) && Wizard(raw_owner); if ( 0 < mudconf.vlimit && !bypass_vlimit) { // Only check the limit for new attributes, not updates. // size_t existing_len; dbref existing_owner; int existing_flags; if (!cache_get(&okey, &existing_len, &existing_owner, &existing_flags)) { int count = cache_count(thing); if (count >= mudconf.vlimit) { return false; } } } LBuf nfc_buf = LBuf_Src("atr_add_raw"); if ( clean_len > 0 && !utf8_is_nfc(clean, clean_len)) { size_t nNfc; utf8_normalize_nfc(clean, clean_len, nfc_buf, LBUF_SIZE - 1, &nNfc); nfc_buf[nNfc] = '\0'; clean = nfc_buf; clean_len = nNfc; } if (!cache_put(&okey, clean, clean_len + 1, raw_owner, raw_flags)) { Log.tinyprintf(T("atr_add_raw_LEN(#%d/%d): SQLite write failed, not applying side effects." ENDLINE), thing, atr); return false; } // Increment AFTER successful write. attr_mod_count_inc(thing, atr); if ( atr == A_EXITVARDEST && isExit(thing)) { route_invalidate(); } switch (atr) { case A_STARTUP: s_Flags(thing, FLAG_WORD1, db[thing].fs.word[FLAG_WORD1] | HAS_STARTUP); break; case A_DAILY: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] | HAS_DAILY); break; case A_FORWARDLIST: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] | HAS_FWDLIST); break; case A_LISTEN: s_Flags(thing, FLAG_WORD2, db[thing].fs.word[FLAG_WORD2] | HAS_LISTEN); break; case A_TIMEOUT: desc_reload(thing); break; case A_QUEUEMAX: pcache_reload(thing); break; } return true; } bool atr_add_raw(dbref thing, int atr, const UTF8 *szValue) { return atr_add_raw_LEN(thing, atr, szValue, szValue ? strlen(reinterpret_cast(szValue)) : 0); } void atr_add(dbref thing, int atr, const UTF8 *buff, dbref owner, int flags) { set_modified(thing); if (!buff || !*buff) { atr_clr(thing, atr); } else { switch (buff[0]) { case AMATCH_LISTEN: // Since this could be a ^-Command, we no longer assert that the // object has none. // mudstate.bfNoListens.Clear(thing); break; case AMATCH_CMD: // Since this could be a $-Command, we no longer assert that the // object has none. // mudstate.bfNoCommands.Clear(thing); break; } // Since this could overwrite an existing ^-Command or $-Command, we // longer assert that the object has one. // mudstate.bfListens.Clear(thing); mudstate.bfCommands.Clear(thing); const UTF8 *tbuff = atr_encode(buff, thing, owner, flags, atr); if (!atr_add_raw(thing, atr, tbuff)) { STARTLOG(LOG_PROBLEMS, "DB", "ATTRSYNC"); log_printf(T("atr_add(#%d, %d): failed to persist attribute."), thing, atr); ENDLOG; } } } void atr_set_flags(dbref thing, int atr, dbref flags) { dbref aowner; int aflags; LBuf buff = LBuf_Adopt(atr_get("atr_set_flags.2212", thing, atr, &aowner, &aflags)); atr_add(thing, atr, buff, aowner, flags); } /* --------------------------------------------------------------------------- * get_atr,atr_get_raw, atr_get_str, atr_get: Get an attribute from the database. */ int get_atr(const UTF8 *name) { ATTR *ap = atr_str(name); if (!ap) return 0; if (!(ap->number)) return -1; return ap->number; } // In SQLite mode, the cache stores clean text with separate owner/flags. // These statics carry the owner/flags from cache_get back to callers. // static dbref cache_last_owner = NOTHING; static int cache_last_flags = 0; const UTF8 *atr_get_raw_LEN(dbref thing, int atr, size_t *pLen) { Aname okey; makekey(thing, atr, &okey); size_t nLen; const UTF8 *a = cache_get(&okey, &nLen, &cache_last_owner, &cache_last_flags); if (a) { if (0 == nLen) { Log.tinyprintf(T("atr_get_raw_LEN: invalid zero-length value for #%d/%d" ENDLINE), thing, atr); nLen = 0; } else { nLen--; } } else { nLen = 0; } *pLen = nLen; return a; } const UTF8 *atr_get_raw(dbref thing, int atr) { size_t Len; return atr_get_raw_LEN(thing, atr, &Len); } UTF8 *atr_get_str_LEN(UTF8 *s, dbref thing, int atr, dbref *owner, int *flags, size_t *pLen) { const UTF8 *buff = atr_get_raw_LEN(thing, atr, pLen); if (!buff) { *owner = Owner(thing); *flags = 0; *pLen = 0; *s = '\0'; } else { // SQLite mode: owner/flags are separate columns, already retrieved // by cache_get via atr_get_raw_LEN. Just copy the clean text. // *owner = (cache_last_owner == NOTHING) ? Owner(thing) : cache_last_owner; *flags = cache_last_flags; memcpy(s, buff, (*pLen) + 1); } return s; } UTF8 *atr_get_str(UTF8 *s, dbref thing, int atr, dbref *owner, int *flags) { size_t nLen; return atr_get_str_LEN(s, thing, atr, owner, flags, &nLen); } UTF8 *atr_get_LEN(dbref thing, int atr, dbref *owner, int *flags, size_t *pLen) { UTF8 *buff = alloc_lbuf("atr_get_LEN"); return atr_get_str_LEN(buff, thing, atr, owner, flags, pLen); } UTF8 *atr_get_real(const UTF8 *tag, dbref thing, int atr, dbref *owner, int *flags, const UTF8 *file, const int line) { size_t nLen; UTF8 *buff = pool_alloc_lbuf(tag, file, line); return atr_get_str_LEN(buff, thing, atr, owner, flags, &nLen); } bool atr_get_info(dbref thing, int atr, dbref *owner, int *flags) { size_t nLen; const UTF8 *buff = atr_get_raw_LEN(thing, atr, &nLen); if (!buff) { *owner = Owner(thing); *flags = 0; return false; } *owner = (cache_last_owner == NOTHING) ? Owner(thing) : cache_last_owner; *flags = cache_last_flags; return true; } UTF8 *atr_pget_str_LEN(UTF8 *s, dbref thing, int atr, dbref *owner, int *flags, size_t *pLen) { dbref parent; int lev; ATTR *ap; const UTF8 *buff; ITER_PARENTS(thing, parent, lev) { buff = atr_get_raw_LEN(parent, atr, pLen); if (buff && *buff) { *owner = (cache_last_owner == NOTHING) ? Owner(thing) : cache_last_owner; *flags = cache_last_flags; memcpy(s, buff, (*pLen) + 1); if ( lev == 0 || !(*flags & AF_PRIVATE)) { return s; } } if ( lev == 0 && Good_obj(Parent(parent))) { ap = atr_num(atr); if (!ap || ap->flags & AF_PRIVATE) { break; } } } *owner = Owner(thing); *flags = 0; *s = '\0'; *pLen = 0; return s; } UTF8 *atr_pget_str(UTF8 *s, dbref thing, int atr, dbref *owner, int *flags) { size_t nLen; return atr_pget_str_LEN(s, thing, atr, owner, flags, &nLen); } UTF8 *atr_pget_LEN(dbref thing, int atr, dbref *owner, int *flags, size_t *pLen) { UTF8 *buff = alloc_lbuf("atr_pget"); return atr_pget_str_LEN(buff, thing, atr, owner, flags, pLen); } UTF8 *atr_pget_real(dbref thing, int atr, dbref *owner, int *flags, const UTF8 *file, const int line) { size_t nLen; UTF8 *buff = pool_alloc_lbuf(T("atr_pget"), file, line); return atr_pget_str_LEN(buff, thing, atr, owner, flags, &nLen); } bool atr_pget_info(dbref thing, int atr, dbref *owner, int *flags) { dbref parent; int lev; ATTR *ap; ITER_PARENTS(thing, parent, lev) { size_t nLen; const UTF8 *buff = atr_get_raw_LEN(parent, atr, &nLen); if (buff && *buff) { *owner = (cache_last_owner == NOTHING) ? Owner(thing) : cache_last_owner; *flags = cache_last_flags; if ((lev == 0) || !(*flags & AF_PRIVATE)) { return true; } } if ((lev == 0) && Good_obj(Parent(parent))) { ap = atr_num(atr); if (!ap || ap->flags & AF_PRIVATE) break; } } *owner = Owner(thing); *flags = 0; return false; } /* --------------------------------------------------------------------------- * atr_free: Reset all attributes of an object. */ void atr_free(dbref thing) { atr_push(); unsigned char *as; for (int atr = atr_head(thing, &as); atr; atr = atr_next(&as)) { atr_clr(thing, atr); } atr_pop(); mudstate.bfCommands.Clear(thing); mudstate.bfNoCommands.Set(thing); mudstate.bfListens.Clear(thing); mudstate.bfNoListens.Set(thing); } /* --------------------------------------------------------------------------- * atr_cpy: Copy all attributes from one object to another. */ void atr_cpy(dbref dest, dbref source, bool bInternal) { dbref owner = Owner(dest); atr_push(); unsigned char *as; for (int atr = atr_head(source, &as); atr; atr = atr_next(&as)) { int aflags; dbref aowner; LBuf buf = LBuf_Adopt(atr_get("atr_cpy.2480", source, atr, &aowner, &aflags)); if (!(aflags & AF_LOCK)) { // Change owner. // aowner = owner; } ATTR *at = atr_num(atr); if ( atr && at) { if ( !(at->flags & (AF_INTERNAL|AF_NOCLONE)) && ( bInternal || God(owner) || ( !God(dest) && !(aflags & AF_LOCK) && ( ( Controls(owner, dest) && !(at->flags & (AF_WIZARD|AF_GOD)) && !(aflags & (AF_WIZARD|AF_GOD))) || ( Wizard(owner) && !(at->flags & AF_GOD)))))) { // Only set attrs that owner has perm to set. // atr_add(dest, atr, buf, aowner, aflags); } } } atr_pop(); } /* --------------------------------------------------------------------------- * atr_chown: Change the ownership of the attributes of an object to the * current owner if they are not locked. */ void atr_chown(dbref obj) { dbref owner = Owner(obj); atr_push(); unsigned char *as; for (int atr = atr_head(obj, &as); atr; atr = atr_next(&as)) { int aflags; dbref aowner; LBuf buf = LBuf_Adopt(atr_get("atr_chown.2529", obj, atr, &aowner, &aflags)); if ( aowner != owner && !(aflags & AF_LOCK)) { atr_add(obj, atr, buf, owner, aflags); } } atr_pop(); } /* --------------------------------------------------------------------------- * atr_next: Return next attribute in attribute list. */ int atr_next(UTF8 **attrp) { if ( !attrp || !*attrp) { return 0; } if (mudstate.attr_iter_ctx.pos >= mudstate.attr_iter_ctx.attrs.size()) { *attrp = nullptr; return 0; } int atr = mudstate.attr_iter_ctx.attrs[mudstate.attr_iter_ctx.pos++]; if (mudstate.attr_iter_ctx.pos >= mudstate.attr_iter_ctx.attrs.size()) { *attrp = nullptr; } else { *attrp = reinterpret_cast( static_cast(mudstate.attr_iter_ctx.pos + 1)); } return atr; } /* --------------------------------------------------------------------------- * atr_push, atr_pop: Push and pop attr lists. */ void atr_push(void) { mudstate.attr_iter_stack.push_back(std::move(mudstate.attr_iter_ctx)); mudstate.attr_iter_ctx = {}; mudstate.attr_iter_ctx.pos = 0; } void atr_pop(void) { if (!mudstate.attr_iter_stack.empty()) { mudstate.attr_iter_ctx = std::move(mudstate.attr_iter_stack.back()); mudstate.attr_iter_stack.pop_back(); } else { mudstate.attr_iter_ctx.attrs.clear(); mudstate.attr_iter_ctx.pos = 0; } } /* --------------------------------------------------------------------------- * atr_head: Returns the head of the attr list for object 'thing' */ int atr_head(dbref thing, unsigned char **attrp) { collect_attrnums_from_storage(thing, mudstate.attr_iter_ctx.attrs); mudstate.attr_iter_ctx.pos = 0; if (mudstate.attr_iter_ctx.attrs.empty()) { *attrp = nullptr; return 0; } *attrp = reinterpret_cast(static_cast(1)); return atr_next(reinterpret_cast(attrp)); } attr_info::attr_info(void) { m_object = NOTHING; m_attr = nullptr; m_bValid = false; m_aowner = NOTHING; m_aflags = 0; m_bHaveInfo = false; } attr_info::attr_info(dbref object, ATTR *attr) { m_aowner = NOTHING; m_aflags = 0; m_bHaveInfo = false; m_object = object; m_attr = attr; m_bValid = (Good_obj(object) && (nullptr != attr)); } attr_info::attr_info(dbref executor, const UTF8 *pTarget, bool bCreate, bool bDefaultMe) { m_object = NOTHING; m_attr = nullptr; m_bValid = false; m_aowner = NOTHING; m_aflags = 0; m_bHaveInfo = false; if (isEmpty(pTarget)) { return; } const UTF8 *pAttrName = nullptr; bool bHaveObject = parse_thing_slash(executor, pTarget, &pAttrName, &m_object); if (!bHaveObject) { if (bDefaultMe) { m_object = executor; pAttrName = pTarget; } else { m_object = match_thing(executor, pTarget); return; } } if (!isEmpty(pAttrName)) { if (bCreate) { m_attr = atr_num(mkattr(executor, pAttrName)); } else { m_attr = atr_str(pAttrName); } m_bValid = (m_attr != nullptr); } } bool attr_info::get_info(bool bParent) { if (!m_bValid) { return false; } bool bHasAttr = false; if (bParent) { bHasAttr = atr_pget_info(m_object, m_attr->number, &m_aowner, &m_aflags); } else { bHasAttr = atr_get_info(m_object, m_attr->number, &m_aowner, &m_aflags); } m_bHaveInfo = true; return bHasAttr; } /* --------------------------------------------------------------------------- * db_grow: Extend the struct database. */ #define SIZE_HACK 1 // So mistaken refs to #-1 won't die. static void initialize_objects(dbref first, dbref last) { dbref thing; for (thing = first; thing < last; thing++) { s_Owner(thing, GOD); s_Flags(thing, FLAG_WORD1, (TYPE_GARBAGE | GOING)); s_Powers(thing, 0); s_Powers2(thing, 0); s_Location(thing, NOTHING); s_Contents(thing, NOTHING); s_Exits(thing, NOTHING); s_Link(thing, NOTHING); s_Next(thing, NOTHING); s_Zone(thing, NOTHING); s_Parent(thing, NOTHING); db[thing].cpu_time_used.Set100ns(0); db[thing].tThrottleExpired.Set100ns(0); s_ThAttrib(thing, 0); s_ThMail(thing, 0); s_ThRefs(thing, 0); db[thing].name = nullptr; db[thing].purename = nullptr; db[thing].moniker = nullptr; } } void db_grow(dbref newtop) { mudstate.bfCommands.Resize(newtop); mudstate.bfNoCommands.Resize(newtop); mudstate.bfListens.Resize(newtop); mudstate.bfNoListens.Resize(newtop); int delta; if (mudstate.bStandAlone) { delta = 1000; } else { delta = mudconf.init_size; } // Determine what to do based on requested size, current top and size. // Make sure we grow in reasonable-sized chunks to prevent frequent // reallocations of the db array. // // If requested size is smaller than the current db size, ignore it. // if (newtop <= mudstate.db_top) { return; } // If requested size is greater than the current db size but smaller // than the amount of space we have allocated, raise the db size and // initialize the new area. // if (newtop <= mudstate.db_size) { initialize_objects(mudstate.db_top, newtop); mudstate.db_top = newtop; return; } // Grow by a minimum of delta objects // int newsize = newtop; int nMinimumGrowth = mudstate.db_size + delta; if (newtop <= nMinimumGrowth) { newsize = nMinimumGrowth; } // Enforce minimum database size // if (newsize < mudstate.min_size) { newsize = mudstate.min_size; } // Grow the db array // // NOTE: There is always one copy of 'db' around that isn't freed even // just before the process terminates. We rely (quite safely) on the OS // to reclaim the memory. // OBJ *newdb = static_cast(MEMALLOC((newsize + SIZE_HACK) * sizeof(OBJ))); mux_assert(newdb); if (db) { // An old struct database exists. Copy it to the new buffer. // db -= SIZE_HACK; memcpy(newdb, db, (mudstate.db_top + SIZE_HACK) * sizeof(OBJ)); MEMFREE(db); } else { // Creating a brand new struct database. Fill in the 'reserved' area // in case it is referenced. // db = newdb; initialize_objects(0, SIZE_HACK); } db = newdb + SIZE_HACK; newdb = nullptr; initialize_objects(mudstate.db_top, newtop); mudstate.db_top = newtop; mudstate.db_size = newsize; // Grow the db mark buffer. // int marksize = (newsize + 7) >> 3; MARKBUF *newmarkbuf = static_cast(MEMALLOC(marksize)); mux_assert(newmarkbuf); memset(newmarkbuf, 0, marksize); if (mudstate.markbits) { marksize = (newtop + 7) >> 3; memcpy(newmarkbuf, mudstate.markbits, marksize); MEMFREE(mudstate.markbits); } mudstate.markbits = newmarkbuf; } void db_free(void) { // Every container the index was built from is about to cease to exist. // This is the one point all eight teardown paths share, and it is the // only invalidation that does not depend on a particular load path // running db_validate_refs() afterwards (#2058). // name_index_invalidate_all(); #ifdef SELFCHECK delete_all_player_names(); for (dbref thing = 0; thing < mudstate.db_top; thing++) { free_Names(&db[thing]); } #endif if (db != nullptr) { db -= SIZE_HACK; char *cp = reinterpret_cast(db); MEMFREE(cp); cp = nullptr; db = nullptr; } mudstate.db_top = 0; mudstate.db_size = 0; mudstate.freelist = NOTHING; } bool db_make_minimal(void) { CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); auto cleanup_bootstrap_failure = [&sqldb]() { if (!sqldb.Begin()) { // If a previous transaction is still open, rollback and retry so // cleanup remains atomic. // sqldb.Rollback(); if (!sqldb.Begin()) { Log.WriteString(T("db_make_minimal: SQLite cleanup transaction begin failed.\n")); db_free(); mudstate.attr_next = A_USER_START; mudstate.record_players = 0; return; } } if ( !sqldb.ClearAttributes() || !sqldb.ClearObjectTable() || !sqldb.ClearAttrNames() || !sqldb.PutMeta("attr_next", A_USER_START) || !sqldb.PutMeta("db_top", 0) || !sqldb.PutMeta("record_players", 0) || !sqldb.Commit()) { sqldb.Rollback(); } db_free(); mudstate.attr_next = A_USER_START; mudstate.record_players = 0; }; db_free(); db_grow(1); s_Name(0, T("Limbo")); if (nullptr == db[0].name) { Log.WriteString(T("db_make_minimal: failed to persist Limbo name.\n")); cleanup_bootstrap_failure(); return false; } s_Flags(0, FLAG_WORD1, TYPE_ROOM); s_Flags(0, FLAG_WORD2, 0); s_Flags(0, FLAG_WORD3, 0); s_Powers(0, 0); s_Powers2(0, 0); s_Location(0, NOTHING); s_Exits(0, NOTHING); s_Link(0, NOTHING); s_Parent(0, NOTHING); s_Zone(0, NOTHING); if (!atr_add_raw(0, A_MONEY, T("0"))) { Log.WriteString(T("db_make_minimal: failed to persist Limbo pennies.\n")); cleanup_bootstrap_failure(); return false; } s_Owner(0, 1); // Insert Limbo (#0) into SQLite before creating Wizard. // create_player -> create_obj will insert #1 via its own path. // CSQLiteDB::ObjectRecord rec; rec.dbref_val = 0; rec.location = db[0].location; rec.contents = db[0].contents; rec.exits = db[0].exits; rec.next = db[0].next; rec.link = db[0].link; rec.owner = db[0].owner; rec.parent = db[0].parent; rec.zone = db[0].zone; rec.pennies = 0; rec.flags1 = db[0].fs.word[FLAG_WORD1]; rec.flags2 = db[0].fs.word[FLAG_WORD2]; rec.flags3 = db[0].fs.word[FLAG_WORD3]; rec.powers1 = db[0].powers; rec.powers2 = db[0].powers2; bool bPersisted = false; if (!sqldb.Begin()) { Log.WriteString(T("db_make_minimal: failed to begin SQLite transaction for Limbo (#0).\n")); } else if (!sqldb.InsertObject(rec)) { Log.WriteString(T("db_make_minimal: failed to insert Limbo (#0) into SQLite.\n")); sqldb.Rollback(); } else if (!sqldb.PutMeta("db_top", mudstate.db_top)) { Log.WriteString(T("db_make_minimal: failed to persist db_top after Limbo insert.\n")); sqldb.Rollback(); } else if (!sqldb.Commit()) { Log.WriteString(T("db_make_minimal: failed to commit Limbo insert.\n")); sqldb.Rollback(); } else { bPersisted = true; } if (!bPersisted) { Log.WriteString(T("db_make_minimal: attempting full SQLite runtime resync.\n")); if (!sqlite_sync_runtime()) { Log.WriteString(T("db_make_minimal: SQLite runtime resync failed.\n")); cleanup_bootstrap_failure(); return false; } } // should be #1 // load_player_names(); const UTF8 *pmsg; dbref obj = create_player(T("Wizard"), T("potrzebie"), NOTHING, false, &pmsg); if (obj == NOTHING) { Log.WriteString(T("db_make_minimal: failed to create Wizard player.\n")); cleanup_bootstrap_failure(); return false; } const UTF8 *pWizardPass = atr_get_raw(obj, A_PASS); if ( nullptr == pWizardPass || '\0' == pWizardPass[0]) { Log.WriteString(T("db_make_minimal: failed to persist Wizard password.\n")); cleanup_bootstrap_failure(); return false; } s_Flags(obj, FLAG_WORD1, Flags(obj) | WIZARD); s_Powers(obj, 0); s_Powers2(obj, 0); s_Pennies(obj, 1000); // Manually link to Limbo, just in case // s_Location(obj, 0); s_Next(obj, NOTHING); s_Contents(0, obj); s_Link(obj, 0); // Authoritative post-bootstrap refresh ensures Wizard setup and db_top // are durably reflected in SQLite before declaring minimal boot success. // if (!sqlite_sync_runtime()) { Log.WriteString(T("db_make_minimal: post-bootstrap sqlite_sync_runtime failed.\n")); cleanup_bootstrap_failure(); return false; } return true; } int64_t creation_seconds(dbref obj) { if (!Good_obj(obj)) { return 0; } const UTF8 *pCreated = atr_get_raw(obj, A_CREATED); if (nullptr == pCreated) { return 0; } CLinearTimeAbsolute lta; if (!lta.SetString(pCreated)) { return 0; } lta.Local2UTC(); return lta.ReturnSeconds(); } dbref parse_dbref(const UTF8 *s) { // Skip leading spaces. // while (mux_isspace(*s)) { s++; } const UTF8 *p = s; if (mux_isdigit(*p)) { // Parse numeric portion. // p++; while (mux_isdigit(*p)) { p++; } if ( '\0' == *p || mux_isspace(*p)) { // Parse trailing spaces. // while (mux_isspace(*p)) { p++; } if ('\0' == *p) { // Full-width parse; dbref is int-ranged (#1402). // int64_t x = mux_atoi64(s); if (x < 0 || x > INT_MAX) { return NOTHING; } return static_cast(x); } } else if (':' == *p) { // Parse objid format: dbref:timestamp // int64_t x = mux_atoi64(s); if (x < 0 || x > INT_MAX) { return NOTHING; } p++; const UTF8 *pTimestamp = p; if (!mux_isdigit(*p)) { return NOTHING; } p++; while (mux_isdigit(*p)) { p++; } // Parse trailing spaces. // while (mux_isspace(*p)) { p++; } if ('\0' != *p) { return NOTHING; } dbref obj = static_cast(x); if (!Good_obj(obj)) { return NOTHING; } int64_t csecs = creation_seconds(obj); if ( 0 == csecs || csecs != mux_atoi64(pTimestamp)) { return NOTHING; } return obj; } } return NOTHING; } // putref, putstring, getref, getstring_noalloc and their static tables // have moved to dbutil.cpp (libmux.so). void free_boolexp(BOOLEXP *b) { if (b == TRUE_BOOLEXP) return; switch (b->type) { case BOOLEXP_AND: case BOOLEXP_OR: free_boolexp(b->sub1); free_boolexp(b->sub2); free_bool(b); break; case BOOLEXP_NOT: case BOOLEXP_CARRY: case BOOLEXP_IS: case BOOLEXP_OWNER: case BOOLEXP_INDIR: free_boolexp(b->sub1); free_bool(b); break; case BOOLEXP_CONST: free_bool(b); break; case BOOLEXP_ATR: case BOOLEXP_EVAL: MEMFREE(b->sub1); b->sub1 = nullptr; free_bool(b); break; } } static BOOLEXP *dup_bool(BOOLEXP *b) { if (b == TRUE_BOOLEXP) return (TRUE_BOOLEXP); BOOLEXP *r = alloc_bool("dup_bool"); switch (r->type = b->type) { case BOOLEXP_AND: case BOOLEXP_OR: r->sub2 = dup_bool(b->sub2); case BOOLEXP_NOT: case BOOLEXP_CARRY: case BOOLEXP_IS: case BOOLEXP_OWNER: case BOOLEXP_INDIR: r->sub1 = dup_bool(b->sub1); case BOOLEXP_CONST: r->thing = b->thing; break; case BOOLEXP_EVAL: case BOOLEXP_ATR: r->thing = b->thing; r->sub1 = reinterpret_cast(StringClone(reinterpret_cast(b->sub1))); break; default: Log.WriteString(T("Bad bool type!" ENDLINE)); return TRUE_BOOLEXP; } return (r); } int init_dbfile(const UTF8 *indb) { if (mudstate.bStandAlone) { Log.tinyprintf(T("Opening SQLite (from %s)" ENDLINE), indb); } int cc = cache_init(indb); if (cc != HF_OPEN_STATUS_ERROR) { if (mudstate.bStandAlone) { Log.tinyprintf(T("Done opening SQLite." ENDLINE)); } else { STARTLOG(LOG_ALWAYS, "INI", "LOAD"); log_text(T("Using SQLite storage backend.")); ENDLOG; } db_free(); } return cc; } // check_zone - checks back through a zone tree for control. // bool check_zone_handler(dbref player, dbref thing, bool bPlayerCheck) { mudstate.zone_nest_num++; if ( !mudconf.have_zones || !Good_obj(Zone(thing)) || mudstate.zone_nest_num >= mudconf.zone_nest_lim || isPlayer(thing) != bPlayerCheck) { mudstate.zone_nest_num = 0; return false; } // If the zone doesn't have an enterlock, DON'T allow control. // if ( atr_get_raw(Zone(thing), A_LENTER) && could_doit(player, Zone(thing), A_LENTER)) { mudstate.zone_nest_num = 0; return true; } else if (thing == Zone(thing)) { return false; } return check_zone_handler(player, Zone(thing), false); } // This function releases: // // 1. comsys resources associated with an object. // 2. @mail resources associated with an object. // void ReleaseAllResources(dbref obj) { do_comdisconnect(obj); do_clearcom(obj, obj, obj, 0, 0); do_channelnuke(obj); del_comsys(obj); do_mail_clear(obj, nullptr); do_mail_purge(obj); malias_cleanup(obj); } int ReplaceFile(UTF8 *old_name, UTF8 *new_name) { std::error_code ec; std::filesystem::rename( std::filesystem::u8path(reinterpret_cast(old_name)), std::filesystem::u8path(reinterpret_cast(new_name)), ec); if (ec) { Log.tinyprintf(T("rename %s to %s fails: %s (%d)" ENDLINE), old_name, new_name, ec.message().c_str(), ec.value()); return -1; } return 0; } void RemoveFile(const UTF8 *name) { std::error_code ec; std::filesystem::remove( std::filesystem::u8path(reinterpret_cast(name)), ec); } #define SQLITE_WRITABLE() (!mudstate.bSQLiteLoading) static void sqlite_log_object_update_failure(const UTF8 *field, dbref obj, int val) { Log.tinyprintf(T("SQLite object update failed: field=%s obj=#%d val=%d" ENDLINE), field, obj, val); } void s_Location(dbref t, dbref n) { // Both containers' exact-name indices are now stale (#2058). Read the // old one before overwriting it. name_index_invalidate(db[t].location); name_index_invalidate(n); db[t].location = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateLocation(t, n)) { sqlite_log_object_update_failure(T("location"), t, n); } } } void s_Zone(dbref t, dbref n) { db[t].zone = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateZone(t, n)) { sqlite_log_object_update_failure(T("zone"), t, n); } } } void s_Contents(dbref t, dbref n) { name_index_invalidate(t); // (#2058) db[t].contents = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateContents(t, n)) { sqlite_log_object_update_failure(T("contents"), t, n); } } } void s_Exits(dbref t, dbref n) { db[t].exits = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateExits(t, n)) { sqlite_log_object_update_failure(T("exits"), t, n); } } } void s_Next(dbref t, dbref n) { // t's successor changed, so the list t sits in has been re-ordered or // spliced; that container's index is stale (#2058). name_index_invalidate(db[t].location); db[t].next = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateNext(t, n)) { sqlite_log_object_update_failure(T("next"), t, n); } } } void s_Link(dbref t, dbref n) { db[t].link = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateLink(t, n)) { sqlite_log_object_update_failure(T("link"), t, n); } } } void s_Owner(dbref t, dbref n) { db[t].owner = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateOwner(t, n)) { sqlite_log_object_update_failure(T("owner"), t, n); } } } void s_Parent(dbref t, dbref n) { db[t].parent = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateParent(t, n)) { sqlite_log_object_update_failure(T("parent"), t, n); } } } void s_Flags(dbref t, int f, FLAG n) { db[t].fs.word[f] = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdateFlags(t, db[t].fs.word[FLAG_WORD1], db[t].fs.word[FLAG_WORD2], db[t].fs.word[FLAG_WORD3])) { sqlite_log_object_update_failure(T("flags"), t, static_cast(db[t].fs.word[f])); } } } void s_Powers(dbref t, POWER n) { db[t].powers = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdatePowers(t, db[t].powers, db[t].powers2)) { sqlite_log_object_update_failure(T("powers"), t, db[t].powers); } } } void s_Powers2(dbref t, POWER n) { db[t].powers2 = n; if (SQLITE_WRITABLE()) { if (!g_pSQLiteBackend->GetDB().UpdatePowers(t, db[t].powers, db[t].powers2)) { sqlite_log_object_update_failure(T("powers2"), t, db[t].powers2); } } } void s_Home(dbref t, dbref n) { s_Link(t, n); } void s_Dropto(dbref t, dbref n) { s_Location(t, n); } // Bulk sync objects, attrnames, and related metadata in one transaction. // Called after db_read and failover paths to keep SQLite metadata in // lockstep with runtime. // bool sqlite_sync_runtime(void) { CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); if (!sqldb.Begin()) { return false; } if ( !sqldb.ClearObjectTable() || !sqldb.ClearAttrNames()) { sqldb.Rollback(); return false; } for (dbref i = 0; i < mudstate.db_top; i++) { CSQLiteDB::ObjectRecord rec; rec.dbref_val = i; rec.location = db[i].location; rec.contents = db[i].contents; rec.exits = db[i].exits; rec.next = db[i].next; rec.link = db[i].link; rec.owner = db[i].owner; rec.parent = db[i].parent; rec.zone = db[i].zone; rec.pennies = Pennies(i); rec.flags1 = db[i].fs.word[FLAG_WORD1]; rec.flags2 = db[i].fs.word[FLAG_WORD2]; rec.flags3 = db[i].fs.word[FLAG_WORD3]; rec.powers1 = db[i].powers; rec.powers2 = db[i].powers2; if (!sqldb.InsertObject(rec)) { sqldb.Rollback(); return false; } } for (int iAttr = A_USER_START; iAttr <= anum_alc_top; iAttr++) { ATTR *vp = static_cast(anum_get(iAttr)); if ( vp != nullptr && !(vp->flags & AF_DELETED)) { if (!sqldb.PutAttrName(vp->number, reinterpret_cast(vp->name), vp->flags)) { sqldb.Rollback(); return false; } } } if ( !sqldb.PutMeta("attr_next", mudstate.attr_next) || !sqldb.PutMeta("db_top", mudstate.db_top) || !sqldb.PutMeta("record_players", mudstate.record_players)) { sqldb.Rollback(); return false; } if (!sqldb.Commit()) { sqldb.Rollback(); return false; } return true; } // db_validate_refs — clamp object field dbrefs to the legal range after a load // (#810). db_read()/sqlite_load_game() store the dbref *values* (location, // contents, exits, next, link, owner, parent, zone) straight from the DB. The // object indices are validated (#806), but the field values are not, and a // corrupt/malicious DB can carry wild values that crash at use: the chain // pointers (contents/exits/next) are walked by DOLIST, which only guards // against NOTHING and self-loops, so a wild `next` -> db[wild].next is an OOB // read; location/parent/zone/owner index db[] in many places too. For a valid // database every field is already in range, so this is a no-op there; only a // corrupt database is repaired (wild -> NOTHING; a wild owner defaults to GOD; // link and location additionally permit the HOME sentinel — for an exit, // location holds the destination, and @link exit=home stores HOME there). // void db_validate_refs(void) { const dbref top = mudstate.db_top; auto objref = [top](dbref r) -> dbref { return (NOTHING == r || (0 <= r && r < top)) ? r : NOTHING; }; int nFixed = 0; // The loop below rewrites location/contents/next directly rather than // through the setters, so it bypasses their index hooks (#2058). name_index_invalidate_all(); for (dbref i = 0; i < top; i++) { dbref b; b = db[i].location; db[i].location = (HOME == b) ? HOME : objref(b); if (db[i].location != b) nFixed++; b = db[i].contents; db[i].contents = objref(b); if (db[i].contents != b) nFixed++; b = db[i].exits; db[i].exits = objref(b); if (db[i].exits != b) nFixed++; b = db[i].next; db[i].next = objref(b); if (db[i].next != b) nFixed++; b = db[i].parent; db[i].parent = objref(b); if (db[i].parent != b) nFixed++; b = db[i].zone; db[i].zone = objref(b); if (db[i].zone != b) nFixed++; b = db[i].link; db[i].link = (HOME == b) ? HOME : objref(b); if (db[i].link != b) nFixed++; b = db[i].owner; db[i].owner = (0 <= b && b < top) ? b : GOD; if (db[i].owner != b) nFixed++; } if (0 < nFixed) { STARTLOG(LOG_STARTUP, "DB", "REFS") log_printf(T("db_validate_refs: clamped %d out-of-range object reference(s) from the loaded database."), nFixed); ENDLOG } } // Load game state from SQLite (warm start). // Returns: // 1 if SQLite had data and we loaded successfully, // 0 if SQLite has no game data (cold start needed), // -1 if SQLite had data but load failed. // int sqlite_load_game(void) { CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB(); // Check if SQLite has data by reading db_top. // int db_top_val = 0; if (!sqldb.GetMeta("db_top", &db_top_val) || db_top_val <= 0) { return 0; } int attr_next_val = A_USER_START; sqldb.GetMeta("attr_next", &attr_next_val); int record_players_val = 0; sqldb.GetMeta("record_players", &record_players_val); mudstate.record_players = record_players_val; // Suppress write-through for the entire load. // mudstate.bSQLiteLoading = true; // Load attribute names first (before objects, since objects // reference attribute names for Name, etc.) // int max_attrnum_loaded = A_USER_START - 1; if (!sqldb.LoadAllAttrNames( [&max_attrnum_loaded](int attrnum, const char *name, int flags) { // Validate before anum_extend()/anum_set()/max tracking (#808). A // negative attrnum from a tampered SQLite DB would OOB-write // anum_table; a huge one would make anum_extend allocate a giant // table and seed attr_next enormous (runtime OOM). User attribute // numbers are always in [A_USER_START, 0x01000000]; skip anything // else so a corrupt row degrades gracefully. // if ( attrnum < A_USER_START || attrnum > A_USER_MAX) { return; } anum_extend(attrnum); vattr_define_LEN( reinterpret_cast(name), strlen(name), attrnum, flags); if (attrnum > max_attrnum_loaded) { max_attrnum_loaded = attrnum; } })) { mudstate.bSQLiteLoading = false; return -1; } if (attr_next_val <= max_attrnum_loaded) { attr_next_val = max_attrnum_loaded + 1; if (!sqldb.PutMeta("attr_next", attr_next_val)) { Log.tinyprintf(T("sqlite_load_game: failed to persist corrected attr_next=%d" ENDLINE), attr_next_val); } } mudstate.attr_next = attr_next_val; // Load all objects from the objects table first, then size db[] // from the actual max dbref to avoid stale metadata overruns. // vector objects; if (!sqldb.LoadAllObjects( [&objects](const CSQLiteDB::ObjectRecord &rec) { objects.push_back(rec); })) { mudstate.bSQLiteLoading = false; return -1; } dbref max_dbref = -1; for (const auto& rec : objects) { if (rec.dbref_val > max_dbref) { max_dbref = rec.dbref_val; } } dbref expected_top = db_top_val; if (max_dbref >= 0) { dbref needed = max_dbref + 1; if (needed > expected_top) { expected_top = needed; } } if (expected_top <= 0) { mudstate.bSQLiteLoading = false; return -1; } vector seen(static_cast(expected_top), false); for (const auto& rec : objects) { dbref i = rec.dbref_val; if ( i < 0 || i >= expected_top) { mudstate.bSQLiteLoading = false; return -1; } if (seen[static_cast(i)]) { mudstate.bSQLiteLoading = false; return -1; } seen[static_cast(i)] = true; } for (dbref i = 0; i < expected_top; i++) { if (!seen[static_cast(i)]) { Log.tinyprintf(T("sqlite_load_game: missing object row #%d (db_top=%d)." ENDLINE), i, db_top_val); mudstate.bSQLiteLoading = false; return -1; } } if (expected_top != db_top_val) { if (!sqldb.PutMeta("db_top", expected_top)) { Log.tinyprintf(T("sqlite_load_game: failed to persist corrected db_top=%d" ENDLINE), expected_top); } db_top_val = expected_top; } // Grow db[] to the right size. // db_grow(expected_top); // The loop below writes location/contents/next directly, bypassing the // setters' index hooks (#2058). // name_index_invalidate_all(); for (const auto& rec : objects) { dbref i = rec.dbref_val; if ( i < 0 || i >= mudstate.db_top) { mudstate.bSQLiteLoading = false; return -1; } db[i].location = rec.location; db[i].contents = rec.contents; db[i].exits = rec.exits; db[i].next = rec.next; db[i].link = rec.link; db[i].owner = rec.owner; db[i].parent = rec.parent; db[i].zone = rec.zone; db[i].fs.word[FLAG_WORD1] = rec.flags1; db[i].fs.word[FLAG_WORD2] = rec.flags2; db[i].fs.word[FLAG_WORD3] = rec.flags3; db[i].powers = rec.powers1; db[i].powers2 = rec.powers2; // Clear CONNECTED flag — nobody is connected at startup. // if (isPlayer(i)) { db[i].fs.word[FLAG_WORD2] &= ~CONNECTED; } } mudstate.bSQLiteLoading = false; // Load player names. // load_player_names(); STARTLOG(LOG_STARTUP, "INI", "LOAD"); log_printf(T("Loaded %d objects from SQLite."), db_top_val); ENDLOG; return 1; }