/* destroy.c */ #include "config.h" #include <ctype.h> #include <assert.h> #include <stdlib.h> #ifdef I_STRING #include <string.h> #else #include <strings.h> #endif #include "copyrite.h" #include "conf.h" #include "mushdb.h" #include "match.h" #include "externs.h" #include "globals.h" #include "confmagic.h" /* These lengthy comments are by Ralph Melton, December 1995. */ /* * First, a discourse on the theory of how we handle destruction. * * We want to maintain the following invariants: * 1. All destroyed objects are on the free list. (linked through the next * fields.) * 2. All objects on the free list are destroyed objects. * 3. No undestroyed object has its next, contents, location, or home * field pointing to a destroyed object. * 4. No object's zone or parent is a destroyed object. * 5. No object's owner is a destroyed object. * * For the sake of efficiency, we allow indirect locks and other locks to * refer to destroyed objects; boolexp.c had better be able to cope with * these. * * * There are three logically distinct parts to destroying an object: * * Part 1: we do all the permissions checks, check for the SAFE flag * and the override switch, and decide that yes, we are going to destroy * this object. * * Part 2: we eliminate all the links from other objects in the database * to this object. This processing may depend on the object's type. * * Part 3: (logically concurrent with part 2, and must happen together * with part 2) Remove any commands the object may have in the queue, * free all the storage associated with the object, set the name to * 'Garbage', and set this object to be a destroyed object, and put it * on the free list. This process is independent of object type. * * Note that phases 2 and 3 do not have to happen immediately after Phase 1. * To allow some delay, we set the object GOING, and then process it on * the check that happens every ten minutes. */ /* * This file has two main parts. One part is the functions for destroying * objects and getting objects off of the free list. The major public * functions here are do_destroy(), free_get(), and purge(). * * The other part is functions for checking the consistency of the * database, and repairing any inconsistencies that are found. The * major function in this group is dbck(). */ dbref first_free = NOTHING; void do_destroy _((dbref player, char *name, int confirm)); void do_undestroy _((dbref player, char *name)); dbref free_get _((void)); void fix_free_list _((void)); void purge _((void)); void do_purge _((dbref player)); void dbck _((void)); void do_dbck _((dbref player)); static dbref what_to_destroy _((dbref player, char *name, int confirm)); static void pre_destroy _((dbref player, dbref thing)); void undestroy _((dbref player, dbref thing)); static void free_object _((dbref thing)); static void empty_contents _((dbref thing)); static void clear_thing _((dbref thing)); static void clear_player _((dbref thing)); static void clear_room _((dbref thing)); static void clear_exit _((dbref thing)); static void check_fields _((void)); static void check_connected_rooms _((void)); static void mark_connected _((dbref loc)); static void check_connected_marks _((void)); static void mark_contents _((dbref loc)); static void check_contents _((void)); static void check_locations _((void)); extern void remove_all_obj_chan _((dbref thing)); extern void chan_chownall _((dbref old, dbref new)); /* Section I: do_destroy() and related functions. This section is where * the human interface of do_destroy() should largely be determined. */ /* Methinks it's time to consider human interfaces criteria for destruction * as well as to consider the invariants that need to be maintained. * * My major criteria are these (with no implied ranking, since I haven't * decided how they balance out): * * 1) It's easy to destroy things you intend to destroy. * * 2) It's easy to correct from destroying things that you don't intend * to destroy. This includes both typos and realizing that you didn't mean * to destroy that. This principle requires two sub-principles: * a) The player gets notified when something 'important' gets * marked to be destroyed--and gets told .what. is marked. * b) The actual destruction of the important thing is delayed * long enough that you can recover from it. * * 3) You can't destroy something you don't have the proper privileges to * destroy. (Obvious, but still worth writing down.) * * To try to achieve a reasonable balance between items 1) and 2), we * have the following design: * Everything is finally destroyed on the second purge after the @destroy * command is done, unless it is set !GOING in the meantime. * @destroying an object while it is set GOING destroys it immediately. * * Let me introduce a little jargon for this discussion: * pre-destroying an object == setting it GOING, running the @adestroy. * (Pre-destroying corresponds to phase 1 above.) * purging an object == actually irrevocably making it vanish. * (This corresponds to phases 2 and 3 above.) * undestroying an object == setting it !GOING, etc. * * We would also like to have an @adestroy attribute that contains * code to be executed when the object is destroyed. This is * complicated by the fact that the object is going to be * destroyed. To work around this, we run the @adestroy when the * object is pre-destroyed, not when it's actually purged. This * introduces the possibility that the adestroy may be invoked for * something that is then undestroyed. To compensate for that, we run * the @startup attribute when something is undestroyed. * * Another issue is how to run the @adestroy for objects that are * destroyed as a consequence of other objects being destroyed. For * example, when rooms are destroyed, any exits leading from those * rooms are also destroyed, and when a player is destroyed, !SAFE * objects they own may also be destroyed. * * To handle this, we do the following: * pre-destroying a room pre-destroys all its exits. * pre-destroying a player pre-destroys all the objects that will be purged * when that player is purged. * * This requires the following about undestroys: * undestroying an exit undestroys its source room. * undestroying any object requires undestroying its owner. * * But it also seems to require the following in order to make '@destroy * foo; @undestroy foo' a no-op for all foo: * undestroying a room undestroys all its exits. * undestroying a player undestroys all its GOING things. * * Now, consider this scenario: * Player A owns room #1. Player B owns exit #2, whose source is room #1. * Player B owns thing #3. Player A and player B are both pre-destroyed; * none of the objects are set SAFE. Thing #3 is then undestroyed. * * If you trace through the dependencies, you find that this involves * undestroying all the objects, including both players! Is that what * we want? It seems to me that it would be very surprising in practice. * * To reconcile this, we introduce the following compromise. * undestroying a room undestroys all exits in the room that are not owned * by a GOING player or set SAFE.. * undestroying a player undestroys all objects he owns that are not exits * in a GOING room that he does not own. * * In this way, the propagation of previous scenario would die out at exit * #2, which would stay GOING. Metaphorically, there are two 'votes' for * its destruction: the destruction of room #1, and the destruction of * player B. Undestroying player B by undestroying thing #3 removes one * of the 'votes' for exit #2's destruction, but there would still be * the vote from room #1. */ static dbref what_to_destroy(player, name, confirm) dbref player; char *name; int confirm; /* Do all matching and permissions checking. Returns the object to be * destroyed if all the permissions checks are successful, otherwise * return NOTHING. */ { dbref thing; thing = noisy_match_result(player, name, NOTYPE, MAT_EVERYTHING); if (thing == NOTHING) return NOTHING; if (Destroyed(thing)) { notify(player, "Destroying that again is hardly necessary."); return NOTHING; } /* To destroy, you must either: * 1. Own it (and control it) * 2. Be a Wizard * 3. Control its source or destination if it's an exit * 4. Be dealing with a dest-ok thing */ if (!(Owns(player, thing) && controls(player, thing)) && !Wizard(player) && !((Typeof(thing) == TYPE_EXIT) && (controls(player, Destination(thing)) || controls(player, Source(thing)))) && !DestOk(thing)) { notify(player, "Permission denied."); return NOTHING; } if (thing == PLAYER_START || thing == MASTER_ROOM || God(thing)) { notify(player, "That is too special to be destroyed."); return NOTHING; } if (REALLY_SAFE) { if (Safe(thing) && !DestOk(thing)) { notify(player, "That object is set SAFE. You must set it !SAFE before destroying it."); return NOTHING; } } else { /* REALLY_SAFE */ if (Safe(thing) && !DestOk(thing) && !confirm) { notify(player, "That object is marked SAFE. Use @nuke to destroy it."); return NOTHING; } } /* check to make sure there's no accidental destruction */ if (!confirm && Wizard(player) && !Owns(player, thing) && !DestOk(thing)) { notify(player, "That object does not belong to you. Use @nuke to destroy it."); return NOTHING; } /* what kind of thing we are destroying? */ switch (Typeof(thing)) { case TYPE_PLAYER: if (Typeof(player) != TYPE_PLAYER) { notify(player, "Programs don't kill people; people kill people!"); return NOTHING; } if (!Wizard(player) && (Owner(thing) == thing)) { notify(player, "Sorry, no suicide allowed."); return NOTHING; } if (Wizard(thing) && !God(player)) { notify(player, "Even you can't do that!"); return NOTHING; } if (God(player) && player == thing) { notify(player, "Sorry, God cannot destroy himself."); return NOTHING; } if (Connected(thing)) { notify(player, "How gruesome. You may not destroy players who are connected."); return NOTHING; } if (!confirm) { notify(player, "You must use @nuke to destroy a player."); return NOTHING; } break; case TYPE_THING: if (!confirm && Wizard(thing)) { notify(player, "That object is set WIZARD. You must use @nuke to destroy it."); return NOTHING; } if (thing == Home(player) && (Typeof(player) != TYPE_EXIT)) { notify(player, "No home-wrecking allowed! Relink yourself."); return NOTHING; } break; case TYPE_ROOM: if (thing == Home(player) && (Typeof(player) != TYPE_EXIT)) { notify(player, "No home-wrecking allowed! Relink first."); return NOTHING; } break; case TYPE_EXIT: break; } return thing; } void do_destroy(player, name, confirm) dbref player; char *name; int confirm; { dbref thing; thing = what_to_destroy(player, name, confirm); if (!GoodObject(thing)) return; /* If thing has already been marked for destruction, go ahead and * destroy immediately. */ if (Going(thing)) { free_object(thing); notify(player, "Destroyed."); return; } /* Present informative messages. */ switch (Typeof(thing)) { case TYPE_ROOM: /* wait until dbck */ notify_except(Contents(thing), NOTHING, "The room shakes and begins to crumble."); if (Owns(player, thing)) notify(player, tprintf("You will be rewarded shortly for %s.", object_header(player, thing))); else { notify(player, tprintf("The wrecking ball is on its way for %s's %s and its exits.", Name(Owner(thing)), object_header(player, thing))); notify(Owner(thing), tprintf("%s has scheduled your room %s to be destroyed.", Name(player), object_header(Owner(thing), thing))); } break; case TYPE_PLAYER: /* wait until dbck */ notify(thing, tprintf("%s has scheduled your character for destruction.", Name(player))); notify(player, tprintf("%s and all their objects are scheduled to be destroyed.", object_header(player, thing))); break; case TYPE_THING: if (!Owns(player, thing) && !DestOk(thing)) { notify(Owner(thing), tprintf("%s has scheduled your %s for destruction.", Name(player), object_header(Owner(thing), thing))); notify(player, tprintf("%s's %s is scheduled to be destroyed.", Name(Owner(thing)), object_header(player, thing))); } else if (!Owns(player, thing)) { notify(player, tprintf("%s's %s is scheduled to be destroyed.", Name(Owner(thing)), object_header(player, thing))); } else { notify(player, tprintf("%s is scheduled to be destroyed.", object_header(player, thing))); } break; case TYPE_EXIT: if (!Owns(player, thing)) { notify(Owner(thing), tprintf("%s has scheduled your %s for destruction.", Name(player), object_header(Owner(thing), thing))); notify(player, tprintf("%s's %s is scheduled to be destroyed.", Name(Owner(thing)), object_header(player, thing))); } else notify(player, tprintf("%s is scheduled to be destroyed.", object_header(player, thing))); break; default: do_log(LT_ERR, NOTHING, NOTHING, "Surprising type in do_destroy."); return; } pre_destroy(player, thing); return; } /* Not undestroy, quite--it's actually 'remove it from its status as about * to be destroyed.' */ void do_undestroy(player, name) dbref player; char *name; { dbref thing; thing = noisy_match_result(player, name, NOTYPE, MAT_EVERYTHING); if (!GoodObject(thing)) { return; } if (!controls(player, thing)) { notify(player, "Alas, your efforts of mercy are in vain."); return; } undestroy(player, thing); notify(Owner(thing), tprintf("Your %s has been spared from destruction.", object_header(Owner(thing), thing))); if (player != Owner(thing)) { notify(player, tprintf("%s has been spared from destruction.", object_header(player, thing))); } } /* Section II: Functions that manage the actual work of destroying * Objects. */ /* Schedule something to be destroyed, run @adestroy, etc. */ static void pre_destroy(player, thing) dbref player; dbref thing; { dbref tmp; if (Going(thing) || Destroyed(thing)) { /* we've already covered this thing. No need to do so again. */ return; } Flags(thing) |= GOING; Flags(thing) &= ~GOING_TWICE; /* Present informative messages, and do recursive destruction. */ switch (Typeof(thing)) { case TYPE_ROOM: DOLIST(tmp, Exits(thing)) { pre_destroy(player, tmp); } break; case TYPE_PLAYER: if (DESTROY_POSSESSIONS) { for (tmp = 0; tmp < db_top; tmp++) { if (Owner(tmp) == thing && (tmp != thing) && (!REALLY_SAFE || !Safe(thing)) ) { pre_destroy(player, tmp); } } } break; case TYPE_THING: break; case TYPE_EXIT: /* This is the only case in which we might end up destroying something * whose owner hasn't already been notified. */ if ((Owner(thing) != Owner(Source(thing))) && Going(Source(thing))) { if (!Owns(player, thing)) { notify(Owner(thing), tprintf("%s has scheduled your %s for destruction.", Name(player), object_header(Owner(thing), thing))); } } break; default: do_log(LT_ERR, NOTHING, NOTHING, "Surprising type in pre_destroy."); return; } if (ADESTROY_ATTR) did_it(player, thing, NULL, NULL, NULL, NULL, "ADESTROY", NOTHING); return; } /* Not undestroy, quite--it's actually 'remove it from its status as about * to be destroyed.' */ void undestroy(player, thing) dbref player; dbref thing; { dbref tmp; if (!Going(thing)) { return; } Flags(thing) &= ~GOING; Flags(thing) &= ~GOING_TWICE; if (Startup(thing) && !Halted(thing)) { ATTR *s; char *r; s = atr_get_noparent(thing, "STARTUP"); if (s) { r = safe_uncompress(s->value); parse_que(thing, r, thing); mush_free(r, "safe_uncompress.buff"); } } /* undestroy owner, if need be. */ if (Going(Owner(thing))) { if (Owner(thing) != player) { notify(player, tprintf("%s has been spared from destruction.", object_header(player, Owner(thing))) ); notify(Owner(thing), tprintf("You have been spared from destruction by %s.", Name(player)) ); } else { notify(player, "You have been spared from destruction."); } undestroy(player, Owner(thing)); } switch (Typeof(thing)) { case TYPE_PLAYER: if (DESTROY_POSSESSIONS) /* Undestroy all objects owned by players, except exits that are in * rooms owned by other players that are set GOING, since those will * be purged when the room is purged. */ for (tmp = 0; tmp < db_top; tmp++) { if (Owns(thing, tmp) && (tmp != thing) && !(Typeof(tmp) == TYPE_EXIT && !Owns(thing, Source(tmp)) && Going(Source(tmp)))) { undestroy(player, tmp); } } break; case TYPE_THING: break; case TYPE_EXIT: /* undestroy containing room. */ if (Going(Source(thing))) { undestroy(player, Source(thing)); notify(player, tprintf("The room %s has been spared from destruction.", object_header(player, Source(thing)))); if (Owner(Source(thing)) != player) { notify(Owner(Source(thing)), tprintf("The room %s has been spared from destruction by %s.", object_header(Owner(Source(thing)), Source(thing))) ); } } break; case TYPE_ROOM: /* undestroy exits in this room, except exits that are going to be * destroyed anyway due to a GOING player. */ DOLIST(tmp, Exits(thing)) { if (DESTROY_POSSESSIONS ? (!Going(Owner(tmp)) || Safe(tmp)) : 1) { undestroy(player, tmp); } } break; default: do_log(LT_ERR, NOTHING, NOTHING, "Surprising type in un_destroy."); return; } } static void free_object(thing) dbref thing; /* Does the real work of freeing all the memory and unlinking an object. */ /* This is going to have to be very tightly coupled with the implementation; * if the database format changes, this will likely have to change too. */ { dbref i; #ifdef LOCAL_DATA local_data_free(thing); #endif switch (Typeof(thing)) { case TYPE_THING: clear_thing(thing); break; case TYPE_PLAYER: clear_player(thing); break; case TYPE_EXIT: clear_exit(thing); break; case TYPE_ROOM: clear_room(thing); break; default: do_log(LT_ERR, NOTHING, NOTHING, "Unknown type on #%d in free_object.", thing); return; } #ifdef QUOTA change_quota(Owner(thing), QUOTA_COST); #endif /* QUOTA */ do_halt(thing, "", thing); nfy_que(thing, 2, 0); /* The equivalent of an @drain. */ /* if something is zoned or parented or linked or chained or located * to/in destroyed object, undo */ for (i = 0; i < db_top; i++) { if (Zone(i) == thing) { Zone(i) = NOTHING; } if (Parent(i) == thing) { Parent(i) = NOTHING; } if (Home(i) == thing) { switch (Typeof(i)) { case TYPE_PLAYER: case TYPE_THING: Home(i) = PLAYER_START; break; case TYPE_EXIT: /* Huh. An exit that claims to be from here, but wasn't linked * in properly. */ do_rawlog(LT_ERR, "ERROR: Exit %s leading from invalid room #%d destroyed.", real_unparse(GOD, i, 0), thing); free_object(i); break; case TYPE_ROOM: /* Hrm. It claims we're an exit from it, but we didn't agree. * Clean it up anyway. */ do_log(LT_ERR, NOTHING, NOTHING, "Found a destroyed exit #%d in room #%d", thing, i); break; } } /* The location check MUST be done AFTER the home check. */ if (Location(i) == thing) { switch (Typeof(i)) { case TYPE_PLAYER: case TYPE_THING: /* Huh. It thought it was here, but we didn't agree. */ enter_room(i, Home(i)); break; case TYPE_EXIT: /* If our destination is destroyed, then we relink to the * source room (so that the exit can't be stolen). Yes, it's * inconsistent with the treatment of exits leading from * destroyed rooms, but it's a lot better than turning exits * into nasty limbo exits. */ Destination(i) = Source(i); break; case TYPE_ROOM: /* Just remove a dropto. */ Location(i) = NOTHING; break; } } if (Next(i) == thing) { Next(i) = NOTHING; } } /* chomp chomp */ atr_free(thing); db[thing].list = NULL; /* don't eat name otherwise examine will crash */ free_locks(Locks(thing)); Locks(thing) = NULL; s_Pennies(thing, 0); Owner(thing) = GOD; db[thing].parent = NOTHING; Zone(thing) = NOTHING; Flags(thing) = TYPE_GARBAGE; #ifdef CHAT_SYSTEM remove_all_obj_chan(thing); #endif Location(thing) = NOTHING; SET(Name(thing), "Garbage"); Exits(thing) = NOTHING; Next(thing) = first_free; first_free = thing; #ifdef HAS_ASSERT assert(Destroyed(thing)); #endif } static void empty_contents(thing) dbref thing; { /* Destroy any exits they may be carrying, send everything else home. */ dbref first; dbref rest; notify_except(Contents(thing), NOTHING, "The floor disappears under your feet, you fall through NOTHINGness and then:"); first = Contents(thing); Contents(thing) = NOTHING; /* send all objects to nowhere */ DOLIST(rest, first) { Location(rest) = NOTHING; } /* now send them home */ while (first != NOTHING) { rest = Next(first); /* if home is in thing set it to limbo */ switch (Typeof(first)) { case TYPE_EXIT: /* if holding exits, destroy it */ free_object(first); break; case TYPE_THING: /* move to home */ case TYPE_PLAYER: if (Home(first) == thing || Typeof(Home(first)) == TYPE_EXIT) Home(first) = PLAYER_START; if (Home(first) != NOTHING) { PUSH(first, Contents(Home(first))); Location(first) = Home(first); /* notify players they have been moved */ if (Hearer(first)) { enter_room(first, HOME); } } break; } first = rest; } } static void clear_thing(thing) dbref thing; { dbref loc; int a; /* Remove object from room's contents */ loc = Location(thing); if (loc != NOTHING) { Contents(loc) = remove_first(Contents(loc), thing); } /* give player money back */ giveto(db[thing].owner, (a = OBJECT_DEPOSIT(Pennies(thing)))); empty_contents(thing); if (Toggles(thing) & THING_PUPPET) Toggles(thing) &= ~THING_PUPPET; if (!Quiet(thing) && !Quiet(Owner(thing))) notify(Owner(thing), tprintf("You get your %d %s deposit back for %s.", a, ((a == 1) ? MONEY : MONIES), object_header(Owner(thing), thing))); } static void clear_player(thing) dbref thing; { dbref i; ATTR *atemp; char alias[PLAYER_NAME_LIMIT]; /* Clear out mail. */ #ifdef USE_MAILER do_mail_clear(thing, NULL); do_mail_purge(thing); #endif /* Chown any chat channels they own to God */ #ifdef CHAT_SYSTEM chan_chownall(thing, GOD); #endif /* Clear out names from the player list */ delete_player(thing, NULL); if ((atemp = atr_get_noparent(thing, "ALIAS")) != NULL) { strcpy(alias, uncompress(atemp->value)); delete_player(thing, alias); } /* Do all the thing-esque manipulations. */ clear_thing(thing); /* Deal with objects owned by the player. */ for (i = 0; i < db_top; i++) { if (Owner(i) == thing && i != thing) { if (DESTROY_POSSESSIONS ? (REALLY_SAFE ? Safe(i) : 0) : 1) { chown_object(GOD, i, GOD); } else { free_object(i); } } } } static void clear_room(thing) dbref thing; { dbref first, rest; /* give player money back */ giveto(Owner(thing), ROOM_COST); empty_contents(thing); /* Remove exits */ first = Exits(thing); Exits(thing) = NOTHING; /* set destination of all exits to nothing */ DOLIST(rest, first) { Destination(rest) = NOTHING; } /* Clear all exits out of exit list */ while (first != NOTHING) { rest = Next(first); if (Typeof(first) == TYPE_EXIT) { free_object(first); } first = rest; } } static void clear_exit(thing) dbref thing; { dbref loc; loc = Source(thing); if (GoodObject(loc)) { Exits(loc) = remove_first(Exits(loc), thing); }; giveto(Owner(thing), EXIT_COST); } /* return a cleaned up object off the free list or NOTHING */ dbref free_get() { dbref newobj; if (first_free == NOTHING) return (NOTHING); newobj = first_free; first_free = Next(first_free); /* Make sure this object really should be in free list */ if (!Destroyed(newobj)) { static int nrecur = 0; dbref temp; if (nrecur++ == 20) { first_free = NOTHING; report(); do_rawlog(LT_ERR, "ERROR: Removed free list and continued\n"); return (NOTHING); } report(); do_rawlog(LT_TRACE, "ERROR: Object #%d should not be free\n", newobj); do_rawlog(LT_TRACE, "ERROR: Corrupt free list, fixing\n"); fix_free_list(); temp = free_get(); nrecur--; return (temp); } /* free object name */ SET(Name(newobj), NULL); return (newobj); } /* Build the free list with a sledgehammer. Only do this when it's actually * necessary. * Since we only do it if things are corrupted, we do not free any memory. * Presumably, this will only waste a reasonable amount of memory, since * it's only called in exceptional cases. */ void fix_free_list() { dbref thing; first_free = NOTHING; for (thing = 0; thing < db_top; thing++) { if (Destroyed(thing)) { Next(thing) = first_free; first_free = thing; } } } /* Destroy all the objects we said we would destroy later. */ void purge() { dbref thing; for (thing = 0; thing < db_top; thing++) { if (Destroyed(thing)) { continue; } else if (Going(thing)) { if (Going_Twice(thing)) { free_object(thing); } else { Flags(thing) |= GOING_TWICE; } } else { continue; } } } /* Destroy objects slated for destruction. */ void do_purge(player) dbref player; { if (Wizard(player)) { purge(); notify(player, "Purge complete."); } else notify(player, "Sorry, you are a mortal."); } /* Section III: dbck() and related functions. */ /* The regular db checkup. */ void dbck() { check_fields(); check_contents(); check_locations(); check_connected_rooms(); } /* Do sanity checks on non-destroyed objects. */ static void check_fields() { dbref thing; for (thing = 0; thing < db_top; thing++) { if (Destroyed(thing)) { /* The only relevant thing is that the Next field ought to be pointing * to a destroyed object. */ dbref next; next = Next(thing); if ((!GoodObject(next) || !Destroyed(next)) && (next != NOTHING)) { do_rawlog(LT_ERR, "ERROR: Invalid next pointer #%d from object %s", next, real_unparse(GOD, thing, 0)); Next(thing) = NOTHING; fix_free_list(); } continue; } else { /* Do sanity checks on non-destroyed objects */ dbref zone, loc, parent, home, owner, next; zone = Zone(thing); if (GoodObject(zone) && Destroyed(zone)) { Zone(thing) = NOTHING; } parent = Parent(thing); if (GoodObject(parent) && Destroyed(parent)) { db[thing].parent = NOTHING; } owner = Owner(thing); if (!GoodObject(owner) || Destroyed(owner) || (Typeof(owner) != TYPE_PLAYER)) { do_rawlog(LT_ERR, "ERROR: Invalid object owner on %s(%d)", Name(thing), thing); report(); db[thing].owner = GOD; } next = Next(thing); if ((!GoodObject(next) || Destroyed(next)) && (next != NOTHING)) { do_rawlog(LT_ERR, "ERROR: Invalid next pointer #%d from object %s", next, real_unparse(GOD, thing, 0)); Next(thing) = NOTHING; } /* This next bit has to be type-specific because of different uses * of the home and location fields. */ home = Home(thing); loc = Location(thing); switch (Typeof(thing)) { case TYPE_PLAYER: case TYPE_THING: if (GoodObject(home) && (Destroyed(home) || (Typeof(home) == TYPE_EXIT))) { Home(thing) = PLAYER_START; } if (GoodObject(loc) && (Destroyed(loc) || (Typeof(loc) == TYPE_EXIT))) { enter_room(thing, Home(thing)); } break; case TYPE_EXIT: if (GoodObject(home) && (Destroyed(home) || (Typeof(home) != TYPE_ROOM))) { /* If our source is destroyed, just destroy the exit. */ do_rawlog(LT_ERR, "ERROR: Exit %s leading from invalid room #%d destroyed.", real_unparse(GOD, thing, 0), home); free_object(thing); } if (GoodObject(loc) && Destroyed(loc)) { /* If our destination is destroyed, then we relink to the * source room (so that the exit can't be stolen). Yes, it's * inconsistent with the treatment of exits leading from * destroyed rooms, but it's a lot better than turning exits * into nasty limbo exits. */ Destination(thing) = Source(thing); } break; case TYPE_ROOM: if (GoodObject(home) && Destroyed(home)) { /* Eww. Destroyed exit. This isn't supposed to happen. */ do_log(LT_ERR, NOTHING, NOTHING, "Found a destroyed exit #%d in room #%d", home, thing); } if (GoodObject(loc) && (Destroyed(loc) || (Typeof(loc) == TYPE_EXIT))) { /* Just remove a dropto. */ Location(thing) = NOTHING; } break; } } } } static void check_connected_rooms() { mark_connected(BASE_ROOM); check_connected_marks(); } static void mark_connected(loc) dbref loc; { dbref thing; if (!GoodObject(loc) || Marked(loc) || (Typeof(loc) != TYPE_ROOM)) return; Flags(loc) |= MARKED; /* recursively trace */ for (thing = Exits(loc); thing != NOTHING; thing = Next(thing)) mark_connected(Destination(thing)); } static void check_connected_marks() { dbref loc; for (loc = 0; loc < db_top; loc++) if (Marked(loc)) Flags(loc) &= ~MARKED; else if (Typeof(loc) == TYPE_ROOM) { if (!Floating(loc) && (!EXITS_CONNECT_ROOMS || (Exits(loc) == NOTHING))) { if (Name(loc)) { notify(Owner(loc), tprintf("You own a disconnected room, %s", object_header(Owner(loc), loc))); } else do_log(LT_ERR, NOTHING, NOTHING, "ERROR: no name for room #%d.", loc); } } } /* An auxiliary function for check_contents. */ static void mark_contents(thing) dbref thing; { /* In this next macro, field must be an lvalue whose evaluation has * no side effects. All hell will break loose if this is not so. */ #define CHECK(field) \ if ((field) != NOTHING) { \ if (!GoodObject(field) || Destroyed(field)) { \ do_rawlog(LT_ERR, "Bad reference #%d from %s severed.", \ (field), real_unparse(GOD, thing, 0)); \ (field) = NOTHING; \ } else if (Typeof(field) == TYPE_ROOM) { \ do_rawlog(LT_ERR, "Reference to room #%d from %s severed.", \ (field), real_unparse(GOD, thing, 0)); \ (field) = NOTHING; \ } else if (Marked(field)) { \ do_rawlog(LT_ERR, "Multiple references to %s. Reference from #%d severed.", \ real_unparse(GOD, (field), 0), thing); \ (field) = NOTHING; \ } else { \ Flags(field) |= MARKED; \ mark_contents(field); \ } \ } if (!GoodObject(thing)) return; Flags(thing) |= MARKED; switch (Typeof(thing)) { case TYPE_ROOM: CHECK(Exits(thing)); CHECK(Contents(thing)); break; case TYPE_PLAYER: case TYPE_THING: CHECK(Contents(thing)); CHECK(Next(thing)); break; case TYPE_EXIT: CHECK(Next(thing)); break; default: do_rawlog(LT_ERR, "Bad object type found for %s in mark_contents", real_unparse(GOD, thing, 0)); break; } #undef CHECK } /* Check that for every thing, player, and exit, you can trace exactly one * path to that object from a room by following the exits field of rooms, * the next field of non-rooms, and the contents field of non-exits. */ static void check_contents() { dbref thing; for (thing = 0; thing < db_top; thing++) { if (Typeof(thing) == TYPE_ROOM) { mark_contents(thing); } } for (thing = 0; thing < db_top; thing++) { if (!Marked(thing) && Typeof(thing) != TYPE_ROOM && !Destroyed(thing)) { do_rawlog(LT_ERR, "Object %s not pointed to by anything.", real_unparse(GOD, thing, 0)); notify(Owner(thing), tprintf("You own an object %s that was \'orphaned\'.", object_header(Owner(thing), thing))); /* We try to fix this by trying to send players and things to * their current location, to their home, or to PLAYER_START, in * that order, and relinking exits to their source. */ Next(thing) = NOTHING; switch (Typeof(thing)) { case TYPE_PLAYER: case TYPE_THING: if (GoodObject(Location(thing)) && !Destroyed(Location(thing)) && Marked(Location(thing))) { PUSH(thing, Contents(Location(thing))); } else if (GoodObject(Home(thing)) && !Destroyed(Home(thing)) && Marked(Home(thing))) { Contents(Location(thing)) = remove_first(Contents(Location(thing)), thing); PUSH(thing, Contents(Home(thing))); Location(thing) = Home(thing); } else { Contents(Location(thing)) = remove_first(Contents(Location(thing)), thing); PUSH(thing, Contents(PLAYER_START)); Location(thing) = PLAYER_START; } enter_room(thing, Location(thing)); /* If we've managed to reconnect it, then we've reconnected * its contents. */ mark_contents(Contents(thing)); notify(Owner(thing), tprintf("It was moved to %s.", object_header(Owner(thing), Location(thing)))); do_rawlog(LT_ERR, "Moved to %s.", real_unparse(GOD, Location(thing), 0)); break; case TYPE_EXIT: if (GoodObject(Source(thing)) && !Destroyed(Source(thing)) && (Typeof(Source(thing)) == TYPE_ROOM)) { PUSH(thing, Exits(Home(thing))); notify(Owner(thing), tprintf("It was moved to %s.", object_header(Owner(thing), Source(thing)))); do_rawlog(LT_ERR, "Moved to %s.", real_unparse(GOD, Source(thing), 0)); } else { /* Just destroy the exit. */ Source(thing) = NOTHING; notify(Owner(thing), "It was destroyed."); do_rawlog(LT_ERR, "Orphaned exit destroyed."); free_object(thing); } break; case TYPE_ROOM: /* We should never get here. */ do_log(LT_ERR, NOTHING, NOTHING, "Disconnected room. So what?"); break; default: do_log(LT_ERR, NOTHING, NOTHING, "Surprising type on #%d found in check_cycles.", thing); break; } } } for (thing = 0; thing < db_top; thing++) { Flags(thing) &= ~MARKED; } } /* Check that every player and thing occurs in the contents list of its * location, and that every exit occurs in the exit list of its source. */ static void check_locations() { dbref thing; dbref loc; for (loc = 0; loc < db_top; loc++) { if (Typeof(loc) != TYPE_EXIT) { for (thing = Contents(loc); thing != NOTHING; thing = Next(thing)) { if (!((Typeof(thing) == TYPE_THING) || (Typeof(thing) == TYPE_PLAYER))) { do_rawlog(LT_ERR, "ERROR: Contents of object %d corrupt at object %d cleared", loc, thing); /* Remove this from the list and start over. */ Contents(loc) = remove_first(Contents(loc), thing); thing = Contents(loc); continue; } else if (Location(thing) != loc) { /* Well, it would fit here, and it can't be elsewhere because * we've done a check_contents already, so let's just put it * here. */ do_rawlog(LT_ERR, "Incorrect location on object %s. Reset to #%d.", real_unparse(GOD, thing, 0), loc); Location(thing) = loc; } Flags(thing) |= MARKED; } } if (Typeof(loc) == TYPE_ROOM) { for (thing = Exits(loc); thing != NOTHING; thing = Next(thing)) { if (Typeof(thing) != TYPE_EXIT) { do_rawlog(LT_ERR, "ERROR: Exits of room %d corrupt at object %d cleared", loc, thing); /* Remove this from the list and start over. */ Contents(loc) = remove_first(Contents(loc), thing); thing = Contents(loc); continue; } else if (Source(thing) != loc) { do_rawlog(LT_ERR, "Incorrect source on exit %s. Reset to #%d.", real_unparse(GOD, thing, 0), loc); } } } } for (thing = 0; thing < db_top; thing++) if (Marked(thing)) Flags(thing) &= ~MARKED; else if (((Typeof(thing) == TYPE_PLAYER) || (Typeof(thing) == TYPE_THING))) { do_rawlog(LT_ERR, "ERROR DBCK: Moved object %d", thing); moveto(thing, PLAYER_START); } } /* Do database checkup. */ /* N.B. Unlike before, this is only a user command. The real work is * farmed out to the dbck() routine. When a dbck is done automatically, * call dbck(), not do_dbck()! */ void do_dbck(player) dbref player; { if (!Wizard(player)) { notify(player, "Silly mortal chicks are for kids!"); return; } notify(player, "GAME: Performing database consistency check."); do_log(LT_WIZ, player, NOTHING, "DBCK done."); dbck(); notify(player, "GAME: Database consistency check complete."); }