tinymux/mux/modules/engine/object.cpp

2312 lines
61 KiB
C++
Raw Permalink Normal View History

/*! \file object.cpp
* \brief Low-level object manipulation routines.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "sqlite_backend.h"
#include "engine_api.h"
// #1185: Also require zero Powers/Powers2 so freelist recycle cannot inherit
// privilege bits from a corrupt or incompletely-reclaimed garbage slot.
//
#define IS_CLEAN(i) (isGarbage(i) && Going(i) && \
((i) >= 0) && ((i) < mudstate.db_top) && \
(Location(i) == NOTHING) && \
(Contents(i) == NOTHING) && (Exits(i) == NOTHING) && \
(Next(i) == NOTHING) && (Owner(i) == GOD) && \
(Powers(i) == 0) && (Powers2(i) == 0))
static int check_type;
// Best-effort refresh of SQLite attributes from the in-memory attribute cache.
// This intentionally avoids clearing the entire attribute table because the
// cache can be incomplete.
//
static bool sqlite_refresh_cached_attributes_in_sqlite(void)
{
struct AttrRow
{
dbref obj;
int attrnum;
std::vector<UTF8> value;
dbref owner;
int flags;
};
// Use only in-memory cache entries as source of truth here. This avoids
// pulling values from SQLite while we are trying to repair SQLite state.
//
std::vector<AttrRow> snapshot;
snapshot.reserve(mudstate.attribute_lru_cache_map.size());
for (const auto& kv : mudstate.attribute_lru_cache_map)
{
const Aname& key = kv.first;
const auto& entry = kv.second;
if ( key.attrnum <= 0
|| key.attrnum == A_LIST
|| key.object < 0
|| key.object >= mudstate.db_top
|| isGarbage(key.object))
{
continue;
}
ATTR *pAttr = atr_num(key.attrnum);
if ( nullptr == pAttr
|| (pAttr->flags & AF_DELETED))
{
continue;
}
if (entry.data.empty())
{
Log.tinyprintf(T("sqlite_refresh_cached_attributes_in_sqlite: invalid empty cache entry #%d/%d" ENDLINE),
key.object, key.attrnum);
return false;
}
AttrRow row;
row.obj = key.object;
row.attrnum = key.attrnum;
row.owner = entry.attr_owner;
row.flags = entry.attr_flags;
row.value = entry.data;
snapshot.push_back(std::move(row));
}
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
if (!sqldb.Begin())
{
return false;
}
for (const auto& row : snapshot)
{
if (!sqldb.PutAttribute(row.obj,
row.attrnum,
row.value.empty() ? nullptr : row.value.data(),
row.value.size(),
row.owner,
row.flags))
{
sqldb.Rollback();
return false;
}
}
if (!sqldb.Commit())
{
sqldb.Rollback();
return false;
}
return true;
}
// Force a newly-created object slot back to a reusable clean shape even
// if backend attr deletes fail during teardown.
//
static void force_reclaim_failed_create_slot(dbref obj)
{
if ( obj < 0
|| obj >= mudstate.db_top)
{
return;
}
if (db[obj].name)
{
if (mudconf.cache_names)
{
if (db[obj].name == db[obj].purename)
{
db[obj].purename = nullptr;
}
if (db[obj].name == db[obj].moniker)
{
db[obj].moniker = nullptr;
}
}
MEMFREE(db[obj].name);
db[obj].name = nullptr;
}
if (mudconf.cache_names)
{
if (db[obj].purename)
{
MEMFREE(db[obj].purename);
db[obj].purename = nullptr;
}
if (db[obj].moniker)
{
MEMFREE(db[obj].moniker);
db[obj].moniker = nullptr;
}
}
perf(engine): index exact names per container (#2058) match_list() compared the query against every object in scope and could not stop at the first hit, because ambiguity has to be detected. Name resolution was 66.8% of attrread and 74.8% of attrwrite once the storage costs were out of the attribute path (#2073, #2084). THE INDEX IS A FILTER, NOT THE DECIDER. string_compare() folds Unicode case through mux_tolower(), whose output is not a byte-for-byte map; reimplementing that to build a hash key would be an approximation that silently disagrees with the walk on some name. So the bucket key is an ASCII normalisation only, every candidate it yields is still confirmed with the real string_compare(), and any name carrying a byte >= 0x80 is not bucketed -- it goes on a must-scan list that suppresses the short-circuit for that container. A wrong key costs a fallback, never a wrong answer. Skipping the remaining walk on a hit is sound because an exact match scores CON_COMPLETE|CON_LOCAL (0x09 minimum) while a partial tops out at CON_LOCAL|CON_TYPE|CON_LOCK (0x07): exact always outranks partial. Only used when md.check_keys is false. With it set, promote_match() evaluates could_doit() per candidate -- softcode -- so skipping the walk would skip those evaluations. Exit and movement matching, the only check_keys users, keep the existing behaviour exactly. INVALIDATION. s_Location/s_Contents/s_Next/s_Name carry hooks, which covers the 45 call sites that mutate contents lists through them. Three sites bypass the setters and write the fields directly; grepping for those found them, and they invalidate too: * db.cpp objref() fixup loop and the SQLite load path -- whole-index clear. * object.cpp force_reclaim_failed_create_slot(), which turns an object into GARBAGE while clearing its fields. That one is in normal operation and would have left a container's index naming a destroyed object. MEASURED. Per lookup at 1000 objects, the tax that scaled with scope is gone -- name lookup now costs what a dbref lookup costs: lookup master with index by dbref (ctrl) 26.7us 30.0us name at head 53.3us 26.7us name at middle 60.0us 23.3us name at tail 66.7us 26.7us Per command, two runs, with three non-resolving controls flat: phase before run1 run2 attrwrite 143us 46us 41us attrread 160us 51us 55us dispatch (ctrl) 31us 30us 32us softcode_arith 27us 28us 26us Behaviour verified identical to master across fifteen cases: exact match, case-insensitivity, partial-match fallthrough, space compression, true ambiguity (two objects sharing a name still give #-2), rename, movement and destruction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:04:37 -06:00
// This clears location/contents/next directly rather than through the
// setters, so it bypasses their index hooks (#2058).
name_index_invalidate(db[obj].location);
db[obj].fs.word[FLAG_WORD1] = TYPE_GARBAGE | GOING;
db[obj].fs.word[FLAG_WORD2] = 0;
db[obj].fs.word[FLAG_WORD3] = 0;
db[obj].powers = 0;
db[obj].powers2 = 0;
db[obj].location = NOTHING;
db[obj].contents = NOTHING;
db[obj].exits = NOTHING;
db[obj].next = NOTHING;
db[obj].owner = GOD;
db[obj].parent = NOTHING;
db[obj].zone = NOTHING;
db[obj].cpu_time_used.Set100ns(0);
db[obj].tThrottleExpired.Set100ns(0);
s_ThAttrib(obj, 0);
s_ThMail(obj, 0);
s_ThRefs(obj, 0);
mudstate.bfListens.Clear(obj);
mudstate.bfCommands.Clear(obj);
mudstate.bfNoListens.Clear(obj);
mudstate.bfNoCommands.Clear(obj);
// Link into freelist directly without write-through.
//
db[obj].link = mudstate.freelist;
mudstate.freelist = obj;
}
// Best-effort backend cleanup for objects that failed creation and were
// reclaimed locally.
//
static void reconcile_failed_create_backend(dbref obj)
{
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
bool bCleaned = false;
if (sqldb.Begin())
{
CSQLiteDB::ObjectRecord rec;
rec.dbref_val = obj;
rec.location = db[obj].location;
rec.contents = db[obj].contents;
rec.exits = db[obj].exits;
rec.next = db[obj].next;
rec.link = db[obj].link;
rec.owner = db[obj].owner;
rec.parent = db[obj].parent;
rec.zone = db[obj].zone;
rec.pennies = Pennies(obj);
rec.flags1 = db[obj].fs.word[FLAG_WORD1];
rec.flags2 = db[obj].fs.word[FLAG_WORD2];
rec.flags3 = db[obj].fs.word[FLAG_WORD3];
rec.powers1 = db[obj].powers;
rec.powers2 = db[obj].powers2;
// Phase 1: collect attr counters before delete (no mutation).
auto collected = attr_mod_count_collect_object(obj);
if ( sqldb.DeleteObject(obj)
&& sqldb.DelAllAttributes(obj)
&& sqldb.InsertObject(rec)
&& sqldb.PutMeta("db_top", mudstate.db_top)
&& sqldb.Commit())
{
// Phase 2: apply increments only after successful commit.
attr_mod_count_apply_increments(collected);
bCleaned = true;
}
else
{
sqldb.Rollback();
// No in-memory mutation — rollback is clean.
}
}
if (!bCleaned)
{
bool bAttrsCleared = false;
if (sqldb.Begin())
{
// Phase 1: collect before delete (no mutation).
auto collected2 = attr_mod_count_collect_object(obj);
if (sqldb.DelAllAttributes(obj) && sqldb.Commit())
{
// Phase 2: apply only after commit.
attr_mod_count_apply_increments(collected2);
bAttrsCleared = true;
}
else
{
sqldb.Rollback();
}
}
STARTLOG(LOG_ALWAYS, "DB", "OBJSYNC");
if (!bAttrsCleared)
{
log_text(T("create_obj failed-cleanup fallback: attribute cleanup incomplete; attempting cached attribute refresh + sqlite_sync_runtime."));
}
else
{
log_text(T("create_obj failed-cleanup fallback: attempting cached attribute refresh + sqlite_sync_runtime."));
}
ENDLOG;
bool bRecovered = sqlite_refresh_cached_attributes_in_sqlite();
bRecovered = bRecovered && sqlite_sync_runtime();
if (!bRecovered)
{
// #1179: Never nuclear-clear SQLite (all attrs/objects/meta)
// after a create-failure reconcile miss. That used to wipe the
// durable store while RAM kept running — crash/restart = mass
// data loss. Leave SQLite alone and log loudly so an admin can
// force a controlled resync / dump from live memory.
//
STARTLOG(LOG_ALWAYS, "DB", "OBJSYNC");
log_text(T("create_obj failed-cleanup: cached refresh/sync failed; "));
log_text(T("SQLite left unchanged (no nuclear clear). "));
log_text(T("In-memory DB may diverge until a full sqlite_sync_runtime "));
log_text(T("or dump succeeds. Investigate backend errors."));
ENDLOG;
}
}
}
/*
* ---------------------------------------------------------------------------
* * Log_pointer_err, Log_header_err, Log_simple_damage: Write errors to the
* * log file.
*/
static void Log_pointer_err(dbref prior, dbref obj, dbref loc, dbref ref,
const UTF8 *reftype, const UTF8 *errtype)
{
STARTLOG(LOG_PROBLEMS, "OBJ", "DAMAG");
log_type_and_name(obj);
if (loc != NOTHING)
{
log_text(T(" in "));
log_type_and_name(loc);
}
log_text(T(": "));
if (prior == NOTHING)
{
log_text(reftype);
}
else
{
log_text(T("Next pointer"));
}
log_text(T(" "));
log_type_and_name(ref);
log_text(T(" "));
log_text(errtype);
ENDLOG;
}
static void Log_header_err(dbref obj, dbref loc, dbref val, bool is_object,
const UTF8 *valtype, const UTF8 *errtype)
{
STARTLOG(LOG_PROBLEMS, "OBJ", "DAMAG");
log_type_and_name(obj);
if (loc != NOTHING)
{
log_text(T(" in "));
log_type_and_name(loc);
}
log_text(T(": "));
log_text(valtype);
log_text(T(" "));
if (is_object)
{
log_type_and_name(val);
}
else
{
log_number(val);
}
log_text(T(" "));
log_text(errtype);
ENDLOG;
}
static void Log_simple_err(dbref obj, dbref loc, const UTF8 *errtype)
{
STARTLOG(LOG_PROBLEMS, "OBJ", "DAMAG");
log_type_and_name(obj);
if (loc != NOTHING)
{
log_text(T(" in "));
log_type_and_name(loc);
}
log_text(T(": "));
log_text(errtype);
ENDLOG;
}
/*
* ---------------------------------------------------------------------------
* * start_home, default_home, can_set_home, new_home, clone_home:
* * Routines for validating and determining homes.
*/
dbref start_home(void)
{
if (mudconf.start_home != NOTHING)
{
return mudconf.start_home;
}
return mudconf.start_room;
}
dbref default_home(void)
{
if (mudconf.default_home != NOTHING)
{
return mudconf.default_home;
}
if (mudconf.start_home != NOTHING)
{
return mudconf.start_home;
}
return mudconf.start_room;
}
bool can_set_home(dbref player, dbref thing, dbref home)
{
if ( !Good_obj(player)
|| !Good_obj(home)
|| thing == home)
{
return false;
}
switch (Typeof(home))
{
case TYPE_PLAYER:
case TYPE_ROOM:
case TYPE_THING:
if (Going(home))
{
return false;
}
if ( Controls(player, home)
|| Abode(home))
{
return true;
}
}
return false;
}
dbref new_home(dbref player)
{
dbref loc = Location(player);
if (can_set_home(Owner(player), player, loc))
{
return loc;
}
loc = Home(Owner(player));
if (can_set_home(Owner(player), player, loc))
{
return loc;
}
return default_home();
}
dbref clone_home(dbref player, dbref thing)
{
dbref loc = Home(thing);
if (can_set_home(Owner(player), player, loc))
{
return loc;
}
return new_home(player);
}
2007-09-14 23:35:36 -07:00
// Manage A_NEWOBJS stored on each player recording the newest object of
// each type created. Data is ROOM, THING, EXIT, PLAYER, and the Latest
//
static void update_newobjects(dbref player, dbref object_num, int object_type)
{
int aowner;
int aflags;
LBuf newobject_string = LBuf_Adopt(atr_get("update_newobjs.189", player, A_NEWOBJS,
&aowner, &aflags));
2007-09-14 23:35:36 -07:00
// This leaves room for Room, Thing, Exit, and Player.
//
int i;
dbref object_list[4];
if ( nullptr == newobject_string.get()
2007-09-14 23:35:36 -07:00
|| '\0' == newobject_string[0])
{
2007-09-14 23:35:36 -07:00
for (i = 0; i < 4; i++)
{
object_list[i] = -1;
}
}
else
{
string_token st(newobject_string, T(" "));
2007-09-14 23:35:36 -07:00
UTF8* ptr;
for ( ptr = st.parse(), i = 0;
2018-10-03 17:54:51 +00:00
nullptr != ptr && i < 4;
ptr = st.parse(), i++)
2007-09-14 23:35:36 -07:00
{
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
object_list[i] = mux_atoi64(ptr);
}
}
2007-09-14 23:35:36 -07:00
switch (object_type)
{
2007-09-14 23:35:36 -07:00
case TYPE_ROOM:
object_list[0] = object_num;
break;
case TYPE_THING:
object_list[1] = object_num;
break;
case TYPE_EXIT:
object_list[2] = object_num;
break;
case TYPE_PLAYER:
object_list[3] = object_num;
break;
}
UTF8 tbuf[SBUF_SIZE];
mux_sprintf(tbuf, SBUF_SIZE, T("%d %d %d %d %d"), object_list[0],
object_list[1], object_list[2], object_list[3], object_num);
atr_add_raw(player, A_NEWOBJS, tbuf);
}
// Handle @acreate on master room objects
//
static void ProcessMasterRoomACreate(dbref creator, dbref thing)
{
int nxargs = 2;
const UTF8 *xargs[2];
switch (Typeof(thing))
{
case TYPE_ROOM:
xargs[1] = T("ROOM");
break;
case TYPE_EXIT:
xargs[1] = T("EXIT");
break;
case TYPE_PLAYER:
xargs[1] = T("PLAYER");
break;
case TYPE_THING:
xargs[1] = T("THING");
break;
default:
xargs[1] = S_("#-1");
break;
}
dbref master_room_obj = -1;
DOLIST(master_room_obj, Contents(mudconf.master_room))
{
if (thing == master_room_obj)
{
break;
}
if (Controls(master_room_obj, thing))
{
int aowner, aflags;
LBuf act = LBuf_Adopt(atr_pget(master_room_obj, A_ACREATE, &aowner, &aflags));
if ('\0' != act[0])
{
CLinearTimeAbsolute lta;
UTF8 buf[SBUF_SIZE];
mux_sprintf(buf, SBUF_SIZE, T("#%d"), thing);
xargs[0] = buf;
wait_que(master_room_obj, creator, creator,
AttrTrace(aflags, 0), false, lta, NOTHING, 0, act, nxargs,
(const UTF8 **) xargs, mudstate.global_regs);
}
}
}
}
/*
* ---------------------------------------------------------------------------
* * create_obj: Create an object of the indicated type IF the player can
* * afford it.
*/
dbref create_obj(dbref player, int objtype, const UTF8 *name, int cost)
{
dbref obj, owner;
dbref target_parent = NOTHING;
int quota = 0, value = 0;
size_t nValidName;
FLAGSET f;
UTF8 *buff;
const UTF8 *pValidName;
const UTF8 *tname;
bool okname = false, self_owned = false, require_inherit = false;
bool bRequiredAttrWritesOk = true;
switch (objtype)
{
case TYPE_ROOM:
cost = mudconf.digcost;
quota = mudconf.room_quota;
f = mudconf.room_flags;
target_parent = mudconf.room_parent;
pValidName = MakeCanonicalObjectName(name, &nValidName, &okname, mudconf.room_name_charset);
tname = T("a room");
break;
case TYPE_THING:
if (cost < mudconf.createmin)
cost = mudconf.createmin;
if (cost > mudconf.createmax)
cost = mudconf.createmax;
quota = mudconf.thing_quota;
f = mudconf.thing_flags;
target_parent = mudconf.thing_parent;
value = OBJECT_ENDOWMENT(cost);
pValidName = MakeCanonicalObjectName(name, &nValidName, &okname, mudconf.thing_name_charset);
tname = T("a thing");
break;
case TYPE_EXIT:
cost = mudconf.opencost;
quota = mudconf.exit_quota;
f = mudconf.exit_flags;
target_parent = mudconf.exit_parent;
pValidName = MakeCanonicalExitName(name, &nValidName, &okname);
tname = T("an exit");
break;
case TYPE_PLAYER:
if (cost)
{
cost = mudconf.robotcost;
quota = mudconf.player_quota;
f = mudconf.robot_flags;
value = 0;
tname = T("a robot");
require_inherit = true;
}
else
{
cost = 0;
quota = 0;
f = mudconf.player_flags;
value = mudconf.paystart;
quota = mudconf.start_quota;
self_owned = true;
tname = T("a player");
}
target_parent = mudconf.player_parent;
buff = munge_space(name);
pValidName = name;
if (!badname_check(buff))
{
notify(player, M_("That name is not allowed."));
free_lbuf(buff);
return NOTHING;
}
if (*buff)
{
okname = ValidatePlayerName(buff);
if (!okname)
{
notify(player, M_("Thats a silly name for a player."));
free_lbuf(buff);
return NOTHING;
}
}
if (okname)
{
bool bAlias = false;
okname = (lookup_player_name(buff, bAlias) == NOTHING);
if (!okname)
{
notify(player, tprintf(M_("The name %s is already taken."), name));
free_lbuf(buff);
return NOTHING;
}
}
free_lbuf(buff);
break;
default:
LOG_SIMPLE(LOG_BUGS, "BUG", "OTYPE", tprintf(T("Bad object type in create_obj: %d."), objtype));
return NOTHING;
}
if (!okname)
{
notify(player, tprintf(M_("Thats a silly name for %s!"), tname));
return NOTHING;
}
if (!self_owned)
{
if (!Good_obj(player))
{
return NOTHING;
}
owner = Owner(player);
if (!Good_obj(owner))
{
return NOTHING;
}
}
else
{
owner = NOTHING;
}
if (require_inherit)
{
if (!Inherits(player))
{
notify(player, NOPERM_MESSAGE);
return NOTHING;
}
}
// Make sure the creator can pay for the object.
//
if ((player != NOTHING) && !canpayfees(player, player, cost, quota))
{
return NOTHING;
}
// Get the first object from the freelist. If the object is not clean,
// discard the remainder of the freelist and go get a completely new object.
//
obj = NOTHING;
if (mudstate.freelist != NOTHING)
{
obj = mudstate.freelist;
if (IS_CLEAN(obj))
{
mudstate.freelist = Link(obj);
}
else
{
LOG_SIMPLE(LOG_PROBLEMS, "FRL", "DAMAG", tprintf(T("Freelist damaged, bad object #%d."), obj));
obj = NOTHING;
mudstate.freelist = NOTHING;
}
}
if (obj == NOTHING)
{
obj = mudstate.db_top;
db_grow(mudstate.db_top + 1);
}
atr_free(obj); // just in case.
// Set things up according to the object type.
//
s_Location(obj, NOTHING);
s_Contents(obj, NOTHING);
s_Exits(obj, NOTHING);
s_Next(obj, NOTHING);
s_Link(obj, NOTHING);
if (Good_obj(target_parent))
{
s_Parent(obj, target_parent);
}
else
{
s_Parent(obj, NOTHING);
}
if (mudconf.autozone && player != NOTHING)
{
s_Zone(obj, Zone(player));
}
else
{
s_Zone(obj, NOTHING);
}
f.word[FLAG_WORD1] |= objtype;
db[obj].fs = f;
s_Owner(obj, (self_owned ? obj : owner));
// #1185: Unconditionally clear powers on create. destroy_obj zeros them for
// normal reclaim, but freelist/corrupt-load paths must not carry privilege.
//
s_Powers(obj, 0);
s_Powers2(obj, 0);
s_Pennies(obj, value);
if (Pennies(obj) != value)
{
bRequiredAttrWritesOk = false;
}
Unmark(obj);
buff = munge_space(pValidName);
s_Name(obj, buff);
if (nullptr == db[obj].name)
{
bRequiredAttrWritesOk = false;
}
2018-10-03 17:54:51 +00:00
free_lbuf(buff); buff = nullptr;
db[obj].cpu_time_used.Set100ns(0);
db[obj].tThrottleExpired.Set100ns(0);
s_ThAttrib(obj, 0);
s_ThMail(obj, 0);
s_ThRefs(obj, 0);
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
const UTF8 *datestr = ltaNow.ReturnDateString(7);
bRequiredAttrWritesOk = bRequiredAttrWritesOk && atr_add_raw(obj, A_CREATED, datestr);
bRequiredAttrWritesOk = bRequiredAttrWritesOk && atr_add_raw(obj, A_MODIFIED, datestr);
if (objtype == TYPE_PLAYER)
{
bRequiredAttrWritesOk = bRequiredAttrWritesOk && atr_add_raw(obj, A_LAST, datestr);
UTF8 *buff2 = alloc_sbuf("create_obj.quota");
mux_ltoa(quota, buff2);
bRequiredAttrWritesOk = bRequiredAttrWritesOk && atr_add_raw(obj, A_QUOTA, buff2);
bRequiredAttrWritesOk = bRequiredAttrWritesOk && atr_add_raw(obj, A_RQUOTA, buff2);
free_sbuf(buff2);
s_Zone(obj, NOTHING);
}
if (!bRequiredAttrWritesOk)
{
STARTLOG(LOG_ALWAYS, "DB", "OBJSYNC");
log_text(T("create_obj required attribute persistence failed; refusing object creation."));
ENDLOG;
destroy_obj(obj);
if (!IS_CLEAN(obj))
{
force_reclaim_failed_create_slot(obj);
}
else
{
s_Link(obj, mudstate.freelist);
mudstate.freelist = obj;
}
reconcile_failed_create_backend(obj);
return NOTHING;
}
CSQLiteDB::ObjectRecord rec;
rec.dbref_val = obj;
rec.location = db[obj].location;
rec.contents = db[obj].contents;
rec.exits = db[obj].exits;
rec.next = db[obj].next;
rec.link = db[obj].link;
rec.owner = db[obj].owner;
rec.parent = db[obj].parent;
rec.zone = db[obj].zone;
rec.pennies = Pennies(obj);
rec.flags1 = db[obj].fs.word[FLAG_WORD1];
rec.flags2 = db[obj].fs.word[FLAG_WORD2];
rec.flags3 = db[obj].fs.word[FLAG_WORD3];
rec.powers1 = db[obj].powers;
rec.powers2 = db[obj].powers2;
CSQLiteDB &sqldb = g_pSQLiteBackend->GetDB();
bool bPersisted = false;
if (!sqldb.Begin())
{
STARTLOG(LOG_PROBLEMS, "DB", "OBJSYNC");
log_text(T("Begin transaction failed in create_obj."));
ENDLOG;
}
else if (!sqldb.InsertObject(rec))
{
STARTLOG(LOG_PROBLEMS, "DB", "OBJSYNC");
log_text(T("InsertObject failed in create_obj."));
ENDLOG;
sqldb.Rollback();
}
else if (!sqldb.PutMeta("db_top", mudstate.db_top))
{
STARTLOG(LOG_PROBLEMS, "DB", "OBJSYNC");
log_text(T("PutMeta(db_top) failed in create_obj."));
ENDLOG;
sqldb.Rollback();
}
else if (!sqldb.Commit())
{
STARTLOG(LOG_PROBLEMS, "DB", "OBJSYNC");
log_text(T("Commit failed in create_obj."));
ENDLOG;
sqldb.Rollback();
}
else
{
bPersisted = true;
}
if (!bPersisted)
{
STARTLOG(LOG_ALWAYS, "DB", "OBJSYNC");
log_text(T("create_obj persistence failed; attempting SQLite metadata resync."));
ENDLOG;
if (!sqlite_sync_runtime())
{
STARTLOG(LOG_ALWAYS, "DB", "OBJSYNC");
log_text(T("create_obj recovery sqlite_sync_runtime() failed."));
ENDLOG;
// Fail closed: don't hand out an object we couldn't make durable.
//
destroy_obj(obj);
if (!IS_CLEAN(obj))
{
force_reclaim_failed_create_slot(obj);
}
else
{
s_Link(obj, mudstate.freelist);
mudstate.freelist = obj;
}
reconcile_failed_create_backend(obj);
return NOTHING;
}
}
// Side effects are only applied after object durability is guaranteed.
//
if (objtype == TYPE_PLAYER)
{
add_player_name(obj, Name(obj), false);
}
if (Good_obj(player))
{
update_newobjects(player, obj, objtype);
ProcessMasterRoomACreate(player, obj);
}
return obj;
}
/*
* ---------------------------------------------------------------------------
* * destroy_obj: Destroy an object. Assumes it has already been removed from
* * all lists and has no contents or exits.
*/
static void destroy_bad_obj(dbref obj)
{
if (!mudstate.bStandAlone)
{
halt_que(NOTHING, obj);
nfy_que(obj, 0, NFY_DRAIN, 0);
fwdlist_clr(obj);
ReleaseAllResources(obj);
}
atr_free(obj);
pcache_delete(obj);
2018-10-03 17:54:51 +00:00
s_Name(obj, nullptr);
s_Flags(obj, FLAG_WORD1, (TYPE_GARBAGE | GOING));
s_Flags(obj, FLAG_WORD2, 0);
s_Flags(obj, FLAG_WORD3, 0);
s_Powers(obj, 0);
s_Powers2(obj, 0);
s_Location(obj, NOTHING);
s_Contents(obj, NOTHING);
s_Exits(obj, NOTHING);
s_Next(obj, NOTHING);
s_Link(obj, NOTHING);
s_Owner(obj, GOD);
s_Pennies(obj, 0);
s_Parent(obj, NOTHING);
s_Zone(obj, NOTHING);
}
void destroy_obj(dbref obj)
{
if (!Good_obj(obj))
{
if ( (obj >= 0)
&& (obj < mudstate.db_top))
{
destroy_bad_obj(obj);
}
return;
}
// Validate the owner.
//
dbref owner = Owner(obj);
bool good_owner = Good_owner(owner);
// Halt any pending commands (waiting or semaphore).
//
if (!mudstate.bStandAlone)
{
if ( halt_que(NOTHING, obj) > 0
&& good_owner
&& !Quiet(obj)
&& !Quiet(owner))
{
notify(owner, M_("Halted."));
}
nfy_que(obj, 0, NFY_DRAIN, 0);
// Remove forwardlists.
//
fwdlist_clr(obj);
}
// Compensate the owner for the object.
//
if ( good_owner
&& owner != obj)
{
int val = 0;
int quota = 0;
switch (Typeof(obj))
{
case TYPE_ROOM:
val = mudconf.digcost;
quota = mudconf.room_quota;
break;
case TYPE_THING:
val = OBJECT_DEPOSIT(Pennies(obj));
quota = mudconf.thing_quota;
break;
case TYPE_EXIT:
if (Location(obj) == NOTHING)
{
val = mudconf.opencost;
}
else
{
val = mudconf.opencost + mudconf.linkcost;
}
quota = mudconf.exit_quota;
break;
case TYPE_PLAYER:
if (Robot(obj))
{
val = mudconf.robotcost;
}
else
{
val = 0;
}
quota = mudconf.player_quota;
break;
}
if (val)
{
giveto(owner, val);
if ( !Quiet(owner)
&& !Quiet(obj))
{
notify(owner,
tprintf(M_("You get back your %d %s deposit for %s(#%d)."),
val, mudconf.one_coin, Moniker(obj), obj));
}
}
if ( mudconf.quotas
&& quota)
{
add_quota(owner, quota);
}
}
if (!mudstate.bStandAlone)
{
ReleaseAllResources(obj);
}
atr_free(obj);
// #1180: Drop pcache before TYPE_GARBAGE so s_Pennies cannot no-op
// on !Good_obj, and freelist recycle cannot inherit QueueMax/queue.
//
pcache_delete(obj);
2018-10-03 17:54:51 +00:00
s_Name(obj, nullptr);
s_Flags(obj, FLAG_WORD1, (TYPE_GARBAGE | GOING));
s_Flags(obj, FLAG_WORD2, 0);
s_Flags(obj, FLAG_WORD3, 0);
s_Powers(obj, 0);
s_Powers2(obj, 0);
s_Location(obj, NOTHING);
s_Contents(obj, NOTHING);
s_Exits(obj, NOTHING);
s_Next(obj, NOTHING);
s_Link(obj, NOTHING);
s_Owner(obj, GOD);
s_Pennies(obj, 0);
s_Parent(obj, NOTHING);
s_Zone(obj, NOTHING);
local_data_free(obj);
2008-01-29 12:16:54 -08:00
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->data_free(obj);
p = p->pNext;
}
}
/*
* ---------------------------------------------------------------------------
* * make_freelist: Build a freelist
*/
static void make_freelist(void)
{
dbref i;
mudstate.freelist = NOTHING;
DO_WHOLE_DB_BACKWARDS(i)
{
if (IS_CLEAN(i))
{
s_Link(i, mudstate.freelist);
mudstate.freelist = i;
}
}
}
/*
* ---------------------------------------------------------------------------
* * divest_object: Get rid of KEY contents of object.
*/
void divest_object(dbref thing)
{
dbref curr, temp;
SAFE_DOLIST(curr, temp, Contents(thing))
{
if ( !Controls(thing, curr)
&& Has_location(curr)
&& Key(curr))
{
if ( !Good_obj(Home(curr))
|| Going(Home(curr)))
{
s_Home(curr, new_home(curr));
}
move_via_generic(curr, HOME, NOTHING, 0);
}
}
}
/*
* ---------------------------------------------------------------------------
* * empty_obj, purge_going: Get rid of GOING objects in the db.
*/
void empty_obj(dbref obj)
{
dbref targ, next;
// Send the contents home
//
SAFE_DOLIST(targ, next, Contents(obj))
{
if (!Has_location(targ))
{
Log_simple_err(targ, obj,
T("Funny object type in contents list of GOING location. Flush terminated."));
break;
}
else if (Location(targ) != obj)
{
Log_header_err(targ, obj, Location(targ), true,
T("Location"),
T("indicates object really in another location during cleanup of GOING location. Flush terminated."));
break;
}
else
{
s_Location(targ, NOTHING);
s_Next(targ, NOTHING);
if ( !Good_obj(Home(targ))
|| Going(Home(targ))
|| Home(targ) == obj)
{
s_Home(targ, new_home(targ));
}
move_via_generic(targ, HOME, NOTHING, 0);
divest_object(targ);
}
}
// Destroy the exits.
//
SAFE_DOLIST(targ, next, Exits(obj))
{
if (!isExit(targ) && !isGarbage(targ))
{
Log_simple_err(targ, obj, T("Funny object type in exit list of GOING location. Flush terminated."));
break;
}
else if (Exits(targ) != obj)
{
Log_header_err(targ, obj, Exits(targ), true, T("Location"),
T("indicates exit really in another location during cleanup of GOING location. Flush terminated."));
break;
}
else
{
destroy_obj(targ);
}
}
}
/*
* ---------------------------------------------------------------------------
* * destroy_exit, destroy_thing, destroy_player
*/
void destroy_exit(dbref exit)
{
dbref loc = Exits(exit);
s_Exits(loc, remove_first(Exits(loc), exit));
destroy_obj(exit);
}
void destroy_thing(dbref thing)
{
move_via_generic(thing, NOTHING, Owner(thing), 0);
empty_obj(thing);
destroy_obj(thing);
}
void destroy_player(dbref player, dbref victim)
{
// Bye bye...
//
boot_off(victim, T("You have been destroyed!"));
halt_que(victim, NOTHING);
int count = chown_all(victim, player, player, CHOWN_NOZONE);
// Remove the name from the name hash table.
//
delete_player_name(victim, Name(victim), false);
dbref aowner;
int aflags;
LBuf buf = LBuf_Adopt(atr_pget(victim, A_ALIAS, &aowner, &aflags));
delete_player_name(victim, buf, true);
// Purge mailbox and orphan sent mail before the dbref is recycled.
//
mail_destroy_player(victim);
move_via_generic(victim, NOTHING, player, 0);
destroy_obj(victim);
fix(nls): plural morphology baked into msgids, and a fragment that could not be translated (#1717) Found while translating mail (#1419). The tell was a line wrong in English before translation is even involved: MAIL: 1 messages in folder 0 [unnamed] (1 unread, 0 cleared). MN_() exists for this (#1622) and #1655 converted the @mail/stats family; this set was missed. A translator cannot work around it -- the msgid has no msgid_plural, so there is no second form for a catalogue to supply, and es must pick one wording for all counts while ru and pl need three and can express none of them. The fix is in the C++, not the .po. ## Converted, 11 sites mail.cpp, mail_mod.cpp mailbox is full; URGENT MAIL urgent messages walkdb.cpp universe contains; objects @chowned; objects marked object.cpp objects @chowned to you wiz.cpp You toaded ... objects @chowned command.cpp at most N players logged in The mail module duplicates are converted alongside the engine's so the two implementations do not drift (#1614). ## One was a different bug than the issue described walkdb.cpp built its message as M_("%d objects %smarked") with %s = "" or "un". That is fragment assembly, not a missing plural: a translator handed that %s cannot use it, because the prefix is English morphology rather than a word. Split into two whole sentences, each pluralised -- the #1575 / #1588 rule. Now renders "1 object marked" / "8 objects unmarked" instead of a %s a catalogue has no way to fill. ## Two deliberately NOT converted MAIL: %d messages in folder %d [%s] (%d unread, %d cleared). %d objects = %d rooms, %d exits, %d things, %d players. (%d garbage) MN_(s, p, n) selects one form from one n. Four independent counts are four independent plural decisions, and in Russian each noun needs its own form. These want rewording, which is a design call rather than a mechanical conversion, so they are left and recorded rather than half-fixed. ## Catalogue fallout .pot 18 -> 27 plural entries; all three catalogues msgmerge'd. xx is `complete` policy, so its nine new entries had to be filled and unfuzzied or the guard fails -- which it duly did, first try. msgmerge had copied the old singular-only text into BOTH forms, so msgstr[0] said "messages"; each form is now prefixed from its own msgid. es and ko received real translations for all nine. es.po 190 -> 198/976 (20.3%) ko.po 229 -> 236/976 (24.2%) ## Verified end to end, at n=1 and n>1 en 1 object marked 8 objects marked es 1 objeto marcado 8 objetos marcados ko 사물 1개를 표시했습니다 사물 8개를 표시했습니다 xx [xx] 1 object marked [xx] 8 objects marked Korean renders identically for both, which is correct: it declares nplurals=1, so the catalogue's own rule is being honoured rather than English's imposed on it. The unconverted multi-count line still reads "1 messages", as documented. make test: Smoke 1561 x3, tests/nls 7, ko 4, plural 43. TESTEXIT=0. Refs #1419, #1575, #1588, #1614, #1622, #1655, #1702. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:29:08 -06:00
notify_quiet(player, tprintf(MN_("(%d object @chowned to you)",
"(%d objects @chowned to you)", count),
count));
}
static void purge_going(void)
{
dbref i;
DO_WHOLE_DB(i)
{
if (!Going(i))
{
continue;
}
if (Indestructible(i))
{
s_Flags(i, FLAG_WORD1, db[i].fs.word[FLAG_WORD1] & ~GOING);
continue;
}
const UTF8 *p;
switch (Typeof(i))
{
case TYPE_PLAYER:
p = atr_get_raw(i, A_DESTROYER);
if (!p)
{
STARTLOG(LOG_PROBLEMS, "OBJ", "DAMAG");
log_type_and_name(i);
dbref loc = Location(i);
if (loc != NOTHING)
{
log_text(T(" in "));
log_type_and_name(loc);
}
log_text(T("GOING object doesnt remember its destroyer. GOING reset."));
ENDLOG;
s_Flags(i, FLAG_WORD1, db[i].fs.word[FLAG_WORD1] & ~GOING);
}
else
{
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
dbref player = static_cast<dbref>(mux_atoi64(p));
// #1183: A_DESTROYER is wizard-writable and may be stale or
// corrupt; never chown inventory to a non-owner dbref.
//
if (!Good_owner(player))
{
STARTLOG(LOG_PROBLEMS, "OBJ", "DAMAG");
log_type_and_name(i);
log_text(T("GOING player has invalid destroyer #"));
log_number(player);
log_text(T("; falling back to GOD."));
ENDLOG;
player = GOD;
}
destroy_player(player, i);
}
break;
case TYPE_ROOM:
// Room scheduled for destruction... do it.
//
empty_obj(i);
destroy_obj(i);
break;
case TYPE_THING:
destroy_thing(i);
break;
case TYPE_EXIT:
destroy_exit(i);
break;
case TYPE_GARBAGE:
break;
default:
// Something else... How did this happen?
//
Log_simple_err(i, NOTHING,
T("GOING object with unexpected type. Destroyed."));
destroy_obj(i);
}
}
}
// ---------------------------------------------------------------------------
// check_dead_refs: Look for references to GOING or illegal objects.
//
static void check_pennies(dbref thing, int limit, const UTF8 *qual)
{
if (Going(thing))
{
return;
}
int j = Pennies(thing);
if (j)
{
if (isRoom(thing) || isExit(thing))
{
Log_header_err(thing, NOTHING, j, false, qual, T("is strange. Reset."));
s_Pennies(thing, 0);
}
else if (j < 0)
{
Log_header_err(thing, NOTHING, j, false, qual, T("is negative."));
}
else if (limit < j)
{
Log_header_err(thing, NOTHING, j, false, qual, T("is excessive."));
}
}
else
{
2007-09-14 23:35:36 -07:00
if (isPlayer(thing) || isThing(thing))
{
Log_header_err(thing, NOTHING, j, false, qual, T("is zero."));
}
}
}
static void check_dead_refs(void)
{
dbref targ, owner, i, j;
int aflags;
FWDLIST *fp;
bool dirty;
DO_WHOLE_DB(i)
{
// Check the owner.
//
owner = Owner(i);
if (!Good_obj(owner))
{
if (isPlayer(i))
{
Log_header_err(i, NOTHING, owner, true, T("Owner"),
T("is invalid. Set to player."));
owner = i;
}
else
{
Log_header_err(i, NOTHING, owner, true, T("Owner"),
T("is invalid. Set to GOD."));
owner = GOD;
}
s_Owner(i, owner);
if (!mudstate.bStandAlone)
{
halt_que(NOTHING, i);
}
s_Halted(i);
}
else if (check_type & DBCK_FULL)
{
if (Going(owner))
{
if (isPlayer(i))
{
Log_header_err(i, NOTHING, owner, true,
T("Owner"), T("is set GOING. Set to player."));
owner = i;
}
else
{
Log_header_err(i, NOTHING, owner, true,
T("Owner"), T("is set GOING. Set to GOD."));
owner = GOD;
}
s_Owner(i, owner);
if (!mudstate.bStandAlone)
{
halt_que(NOTHING, i);
}
s_Halted(i);
}
else if (!OwnsOthers(owner))
{
if (isPlayer(i))
{
Log_header_err(i, NOTHING, owner, true,
T("Owner"), T("is not a valid owner type. Set to player."));
owner = i;
}
else
{
Log_header_err(i, NOTHING, owner, true,
T("Owner"), T("is not a valid owner type. Set to GOD."));
owner = GOD;
}
s_Owner(i, owner);
}
}
// Check the parent
//
targ = Parent(i);
if (Good_obj(targ))
{
if (Going(targ))
{
s_Parent(i, NOTHING);
if (!mudstate.bStandAlone)
{
if ( !Quiet(i)
&& !Quiet(owner))
{
notify(owner, tprintf(M_("Parent cleared on %s(#%d)"),
Moniker(i), i));
}
}
else
{
Log_header_err(i, Location(i), targ, true, T("Parent"),
T("is invalid. Cleared."));
}
}
}
else if (targ != NOTHING)
{
Log_header_err(i, Location(i), targ, true,
T("Parent"), T("is invalid. Cleared."));
s_Parent(i, NOTHING);
}
// Check the zone.
//
targ = Zone(i);
if (Good_obj(targ))
{
if (Going(targ))
{
s_Zone(i, NOTHING);
if (!mudstate.bStandAlone)
{
owner = Owner(i);
if ( !Quiet(i)
&& !Quiet(owner))
{
notify(owner, tprintf(M_("Zone cleared on %s(#%d)"),
Moniker(i), i));
}
}
else
{
Log_header_err(i, Location(i), targ, true, T("Zone"),
T("is invalid. Cleared."));
}
}
}
else if (targ != NOTHING)
{
Log_header_err(i, Location(i), targ, true, T("Zone"),
T("is invalid. Cleared."));
s_Zone(i, NOTHING);
}
// Check forwardlist
//
fp = fwdlist_get(i);
dirty = false;
if (fp)
{
for (j = 0; j < fp->count; j++)
{
targ = fp->data[j];
if ( Good_obj(targ)
&& Going(targ))
{
fp->data[j] = NOTHING;
dirty = true;
}
else if ( !Good_obj(targ)
&& targ != NOTHING)
{
fp->data[j] = NOTHING;
dirty = true;
}
}
}
if (dirty)
{
LBuf lbuf_str = LBuf_Src("purge_going");
(void)fwdlist_rewrite(fp, lbuf_str);
atr_get_info(i, A_FORWARDLIST, &owner, &aflags);
atr_add(i, A_FORWARDLIST, lbuf_str, owner, aflags);
}
if (check_type & DBCK_FULL)
{
// Check for wizards
//
if (RealWizard(i))
{
if (isPlayer(i))
{
Log_simple_err(i, NOTHING, T("Player is a WIZARD."));
}
if (!Wizard(Owner(i)))
{
Log_header_err(i, NOTHING, Owner(i), true,
T("Owner"), T("of a WIZARD object is not a wizard"));
}
}
}
switch (Typeof(i))
{
case TYPE_PLAYER:
// Check home.
//
targ = Home(i);
if ( !Good_obj(targ)
|| !Has_contents(targ))
{
Log_simple_err(i, Location(i), T("Bad home. Reset."));
s_Home(i, default_home());
}
// Check the location.
//
targ = Location(i);
if ( !Good_obj(targ)
|| !Has_contents(targ))
{
Log_pointer_err(NOTHING, i, NOTHING, targ, T("Location"),
T("is invalid. Moved to home."));
move_object(i, Home(i));
}
// Check for self-referential Next().
//
if (Next(i) == i)
{
Log_simple_err(i, NOTHING,
T("Next points to self. Next cleared."));
s_Next(i, NOTHING);
}
if (check_type & DBCK_FULL)
{
// Check wealth.
//
targ = mudconf.paylimit;
check_pennies(i, targ, T("Wealth"));
}
break;
case TYPE_THING:
// Check home.
//
targ = Home(i);
if ( !Good_obj(targ)
|| !Has_contents(targ))
{
if (!mudstate.bStandAlone)
{
if ( !Quiet(i)
&& !Quiet(owner))
{
notify(owner, tprintf(M_("Home reset on %s(#%d)"),
Moniker(i), i));
}
else
{
Log_header_err(i, Location(i), targ, true, T("Home"),
T("is invalid. Cleared."));
}
}
s_Home(i, new_home(i));
}
// Check the location.
//
targ = Location(i);
if ( !Good_obj(targ)
|| !Has_contents(targ))
{
Log_pointer_err(NOTHING, i, NOTHING, targ, T("Location"),
T("is invalid. Moved to home."));
move_object(i, HOME);
}
// Check for self-referential Next().
//
if (Next(i) == i)
{
Log_simple_err(i, NOTHING,
T("Next points to self. Next cleared."));
s_Next(i, NOTHING);
}
if (check_type & DBCK_FULL)
{
// Check value.
//
targ = OBJECT_ENDOWMENT(mudconf.createmax);
check_pennies(i, targ, T("Value"));
}
break;
case TYPE_ROOM:
// Check the dropto.
//
targ = Dropto(i);
if (Good_obj(targ))
{
if (Going(targ))
{
s_Dropto(i, NOTHING);
if (!mudstate.bStandAlone)
{
if ( !Quiet(i)
&& !Quiet(owner))
{
notify(owner, tprintf(M_("Dropto removed from %s(#%d)"),
Moniker(i), i));
}
}
else
{
Log_header_err(i, NOTHING, targ, true, T("Dropto"),
T("is invalid. Removed."));
}
}
}
else if ( targ != NOTHING
&& targ != HOME)
{
Log_header_err(i, NOTHING, targ, true, T("Dropto"),
T("is invalid. Cleared."));
s_Dropto(i, NOTHING);
}
if (check_type & DBCK_FULL)
{
// NEXT should be null.
//
if (Next(i) != NOTHING)
{
Log_header_err(i, NOTHING, Next(i), true, T("Next pointer"),
T("should be NOTHING. Reset."));
s_Next(i, NOTHING);
}
// LINK should be null.
//
if (Link(i) != NOTHING)
{
Log_header_err(i, NOTHING, Link(i), true, T("Link pointer "),
T("should be NOTHING. Reset."));
s_Link(i, NOTHING);
}
// Check value.
//
check_pennies(i, 1, T("Value"));
}
break;
case TYPE_EXIT:
// If it points to something GOING, set it going.
//
targ = Location(i);
if (Good_obj(targ))
{
if (Going(targ))
{
s_Going(i);
}
}
else if (targ == HOME)
{
// null case, HOME is always valid.
//
}
else if (targ != NOTHING)
{
Log_header_err(i, Exits(i), targ, true, T("Destination"),
T("is invalid. Exit destroyed."));
s_Going(i);
}
else
{
if (!Has_contents(targ))
{
Log_header_err(i, Exits(i), targ, true, T("Destination"),
T("is not a valid type. Exit destroyed."));
s_Going(i);
}
}
// Check for self-referential Next().
//
if (Next(i) == i)
{
Log_simple_err(i, NOTHING,
T("Next points to self. Next cleared."));
s_Next(i, NOTHING);
}
if (check_type & DBCK_FULL)
{
// CONTENTS should be null.
//
if (Contents(i) != NOTHING)
{
Log_header_err(i, Exits(i), Contents(i), true, T("Contents"),
T("should be NOTHING. Reset."));
s_Contents(i, NOTHING);
}
// LINK should be null.
//
if (Link(i) != NOTHING)
{
Log_header_err(i, Exits(i), Link(i), true, T("Link"),
T("should be NOTHING. Reset."));
s_Link(i, NOTHING);
}
// Check value.
//
check_pennies(i, 1, T("Value"));
}
break;
case TYPE_GARBAGE:
break;
default:
// Funny object type, destroy it.
//
Log_simple_err(i, NOTHING, T("Funny object type. Destroyed."));
destroy_obj(i);
}
}
}
/*
* ---------------------------------------------------------------------------
* * check_loc_exits, check_exit_chains: Validate the exits chains
* * of objects and attempt to correct problems. The following errors are
* * found and corrected:
* * Location not in database - skip it.
* * Location GOING - skip it.
* * Location not a PLAYER, ROOM, or THING - skip it.
* * Location already visited - skip it.
2018-10-03 17:54:51 +00:00
* * Exit/next pointer not in database - nullptr it.
* * Member is not an EXIT - terminate chain.
* * Member is GOING - destroy exit.
* * Member already checked (is in another list) - terminate chain.
* * Member in another chain (recursive check) - terminate chain.
* * Location of member is not specified location - reset it.
*/
static void check_loc_exits(dbref loc)
{
if (!Good_obj(loc))
{
return;
}
// Only check players, rooms, and things that aren't GOING.
//
if ( isExit(loc)
|| Going(loc))
{
return;
}
// If marked, we've checked here already.
//
if (Marked(loc))
{
return;
}
Mark(loc);
// Check all the exits.
//
dbref temp, exitloc, dest;
dbref back = NOTHING;
dbref exit = Exits(loc);
while (exit != NOTHING)
{
exitloc = NOTHING;
dest = NOTHING;
if (Good_obj(exit))
{
exitloc = Exits(exit);
dest = Location(exit);
}
if (!Good_obj(exit))
{
// A bad pointer - terminate chain.
//
Log_pointer_err(back, loc, NOTHING, exit, T("Exit list"),
T("is invalid. List nulled."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Exits(loc, NOTHING);
}
exit = NOTHING;
}
else if (!isExit(exit))
{
// Not an exit - terminate chain.
//
Log_pointer_err(back, loc, NOTHING, exit, T("Exitlist member"),
T("is not an exit. List terminated."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Exits(loc, NOTHING);
}
exit = NOTHING;
}
else if (Going(exit))
{
// Going - silently filter out.
//
temp = Next(exit);
if (back != NOTHING)
{
s_Next(back, temp);
}
else
{
s_Exits(loc, temp);
}
destroy_obj(exit);
exit = temp;
continue;
}
else if (Marked(exit))
{
// Already in another list - terminate chain.
//
Log_pointer_err(back, loc, NOTHING, exit, T("Exitlist member"),
T("is in another exitlist. Cleared."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Exits(loc, NOTHING);
}
exit = NOTHING;
}
else if ( !Good_obj(dest)
&& dest != HOME
&& dest != NOTHING)
{
// Destination is not in the db. Null it.
//
Log_pointer_err(back, loc, NOTHING, exit, T("Destination"),
T("is invalid. Cleared."));
s_Location(exit, NOTHING);
}
else if (exitloc != loc)
{
// Exit thinks it's in another place. Check the exitlist there
// and see if it contains this exit. If it does, then our exitlist
// somehow pointed into the middle of their exitlist. If not,
// assume we own the exit.'
//
check_loc_exits(exitloc);
if (Marked(exit))
{
// It's in the other list, give it up.
//
Log_pointer_err(back, loc, NOTHING, exit, T(""),
T("is in another exitlist. List terminated."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Exits(loc, NOTHING);
}
exit = NOTHING;
}
else
{
// Not in the other list, assume in ours.
//
Log_header_err(exit, loc, exitloc, true,
T("Not on chain for location"), T("Reset."));
s_Exits(exit, loc);
}
}
if (exit != NOTHING)
{
// All OK (or all was made OK).
//
if (check_type & DBCK_FULL)
{
// Make sure exit owner owns at least one of the source or
// destination. Just warn if he doesn't.
//
temp = Owner(exit);
if ( temp != Owner(loc)
&& !( Good_dbref(dest)
&& temp == Owner(dest)))
{
Log_header_err(exit, loc, temp, true, T("Owner"),
T("does not own either the source or destination."));
}
}
Mark(exit);
back = exit;
exit = Next(exit);
}
}
return;
}
static void check_exit_chains(void)
{
dbref i;
Unmark_all(i);
DO_WHOLE_DB(i)
{
check_loc_exits(i);
}
DO_WHOLE_DB(i)
{
if ( isExit(i)
&& !Marked(i))
{
Log_simple_err(i, NOTHING, T("Disconnected exit. Destroyed."));
destroy_obj(i);
}
}
}
/*
* ---------------------------------------------------------------------------
* * check_misplaced_obj, check_loc_contents, check_contents_chains: Validate
* * the contents chains of objects and attempt to correct problems. The
* * following errors are found and corrected:
* * Location not in database - skip it.
* * Location GOING - skip it.
* * Location not a PLAYER, ROOM, or THING - skip it.
* * Location already visited - skip it.
2018-10-03 17:54:51 +00:00
* * Contents/next pointer not in database - nullptr it.
* * Member is not an PLAYER or THING - terminate chain.
* * Member is GOING - destroy exit.
* * Member already checked (is in another list) - terminate chain.
* * Member in another chain (recursive check) - terminate chain.
* * Location of member is not specified location - reset it.
*/
static void check_loc_contents(dbref);
static void check_misplaced_obj(dbref *obj, dbref back, dbref loc)
{
// Object thinks it's in another place. Check the contents list there
// and see if it contains this object. If it does, then our contents
// list somehow pointed into the middle of their contents list and we
// should truncate our list. If not, assume we own the object.
//
if (!Good_obj(*obj))
{
return;
}
loc = Location(*obj);
Unmark(*obj);
if (Good_obj(loc))
{
check_loc_contents(loc);
}
if (Marked(*obj))
{
// It's in the other list, give it up.
//
Log_pointer_err(back, loc, NOTHING, *obj, T(""),
T("is in another contents list. Cleared."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Contents(loc, NOTHING);
}
*obj = NOTHING;
}
else
{
// Not in the other list, assume in ours.
//
Log_header_err(*obj, loc, Contents(*obj), true, T("Location"),
T("is invalid. Reset."));
s_Contents(*obj, loc);
}
return;
}
static void check_loc_contents(dbref loc)
{
if (!Good_obj(loc))
{
return;
}
// Only check players, rooms, and things that aren't GOING.
//
if ( isExit(loc)
|| Going(loc))
{
return;
}
dbref back = NOTHING;
dbref obj = Contents(loc);
while (obj != NOTHING)
{
if (!Good_obj(obj))
{
// A bad pointer - terminate chain.
//
Log_pointer_err(back, loc, NOTHING, obj, T("Contents list"),
T("is invalid. Cleared."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Contents(loc, NOTHING);
}
obj = NOTHING;
}
else if (!Has_location(obj))
{
// Not a player or thing - terminate chain.
//
Log_pointer_err(back, loc, NOTHING, obj, T(""),
T("is not a player or thing. Cleared."));
if (back != NOTHING)
{
s_Next(back, NOTHING);
}
else
{
s_Contents(loc, NOTHING);
}
obj = NOTHING;
}
else if (Marked(obj))
{
// Already visited - either truncate or ignore.
//
if (Location(obj) != loc)
{
// Location wrong - either truncate or fix.
//
check_misplaced_obj(&obj, back, loc);
}
else
{
// Location right - recursive contents.
//
}
}
else if (Location(obj) != loc)
{
// Location wrong - either truncate or fix.
//
check_misplaced_obj(&obj, back, loc);
}
if (obj != NOTHING)
{
// All OK (or all was made OK).
//
if (check_type & DBCK_FULL)
{
// Check for wizard command-handlers inside non-wizard. Just
// warn if we find one.
//
if ( RealWizard(obj)
&& !Wizard(loc))
{
if (Commer(obj))
{
Log_simple_err(obj, loc,
T("Wizard command handling object inside non-wizard."));
}
}
// Check for non-wizard objects inside wizard objects.
//
if ( RealWizard(loc)
&& !Wizard(obj)
&& !Wizard(Owner(obj)))
{
Log_simple_err(obj, loc, T("Non-wizard object inside wizard."));
}
}
Mark(obj);
back = obj;
obj = Next(obj);
}
}
}
static void check_contents_chains(void)
{
dbref i;
Unmark_all(i);
DO_WHOLE_DB(i)
{
check_loc_contents(i);
}
DO_WHOLE_DB(i)
{
if ( !Going(i)
&& !Marked(i)
&& Has_location(i))
{
Log_simple_err(i, Location(i), T("Orphaned object, moved home."));
s_Location(i, NOTHING);
s_Next(i, NOTHING);
if ( !Good_obj(Home(i))
|| Going(Home(i)))
{
s_Home(i, new_home(i));
}
move_via_generic(i, HOME, NOTHING, 0);
}
}
}
/*
* ---------------------------------------------------------------------------
* * mark_place, check_floating: Look for floating rooms not set FLOATING.
*/
static void mark_place(dbref loc)
{
// If already marked, exit. Otherwise set marked.
//
if ( !Good_obj(loc)
|| Marked(loc))
{
return;
}
Mark(loc);
// Visit all places you can get to via exits from here.
//
dbref exit;
for (exit = Exits(loc); exit != NOTHING; exit = Next(exit))
{
if (Good_obj(Location(exit)))
{
mark_place(Location(exit));
}
}
}
static void check_floating(void)
{
dbref i;
// Mark everyplace you can get to via exits from the starting room.
//
Unmark_all(i);
mark_place(mudconf.start_room);
// Mark every room you can get to from a floating room.
//
DO_WHOLE_DB(i)
{
if ( isRoom(i)
&& Floating(i)
&& !Going(i))
{
mark_place(i);
}
}
// Look for unvisited rooms.
//
DO_WHOLE_DB(i)
{
if ( isRoom(i)
&& !Going(i)
&& !Marked(i))
{
if (!mudstate.bStandAlone)
{
dbref owner = Owner(i);
if (Good_owner(owner))
{
notify(owner, tprintf(M_("You own a floating room: %s(#%d)"),
Moniker(i), i));
}
}
else
{
Log_simple_err(i, NOTHING, T("Disconnected room."));
}
}
}
}
/*
* ---------------------------------------------------------------------------
* * do_dbck: Perform a database consistency check and clean up damage.
*/
void do_dbck(dbref executor, dbref caller, dbref enactor, int eval, int key)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
check_type = key;
check_dead_refs();
check_exit_chains();
check_contents_chains();
check_floating();
if ( !mudstate.bStandAlone
&& executor != NOTHING)
{
Guest.CleanUp();
}
purge_going();
make_freelist();
scheduler.Shrink();
mux_ModuleMaintenance();
#if defined(STUB_SLAVE)
{
mux_ISlaveControl *pISlaveControl = nullptr;
MUX_RESULT mr = mux_CreateInstance(CID_StubSlave, nullptr,
UseSlaveProcess, IID_ISlaveControl,
(void **)&pISlaveControl);
if (MUX_SUCCEEDED(mr) && nullptr != pISlaveControl)
{
pISlaveControl->ModuleMaintenance();
pISlaveControl->Release();
}
}
2008-01-29 12:16:54 -08:00
#endif // STUB_SLAVE
// Allow the local extensions to do data checks.
//
local_dbck();
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
2018-10-03 17:54:51 +00:00
while (nullptr != p)
{
p->pSink->dbck();
p = p->pNext;
}
if ( !mudstate.bStandAlone
&& executor != NOTHING
&& !Quiet(executor))
{
notify(executor, M_("Done."));
}
}