/**
* \file destroy.c
*
* \brief Destroying objects and consistency checking.
*
* 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().
*
*
* 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.
*
*/
#include "config.h"
#include <ctype.h>
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "copyrite.h"
#include "conf.h"
#include "mushdb.h"
#include "match.h"
#include "externs.h"
#include "log.h"
#include "game.h"
#include "extmail.h"
#include "malias.h"
#include "attrib.h"
#include "dbdefs.h"
#include "flags.h"
#include "lock.h"
#include "confmagic.h"
dbref first_free = NOTHING; /**< Object at top of free list */
static dbref what_to_destroy(dbref player, char *name, int confirm);
static void pre_destroy(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);
static void check_zones(void);
static int attribute_owner_helper
(dbref player, dbref thing, dbref parent, char const *pattern, ATTR *atr,
void *args);
extern void remove_all_obj_chan(dbref thing);
extern void chan_chownall(dbref old, dbref new);
extern struct db_stat_info current_state;
/** Mark an object */
#define SetMarked(x) Type(x) |= TYPE_MARKED
/** Unmark an object */
#define ClearMarked(x) Type(x) &= ~TYPE_MARKED
/* 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.
*/
/** Determine what object to destroy and if we're allowed.
* Do all matching and permissions checking. Returns the object to be
* destroyed if all the permissions checks are successful, otherwise
* return NOTHING.
*/
static dbref
what_to_destroy(dbref player, char *name, int confirm)
{
dbref thing;
thing = noisy_match_result(player, name, NOTYPE, MAT_EVERYTHING);
if (thing == NOTHING)
return NOTHING;
if (IsGarbage(thing)) {
notify(player, T("Destroying that again is hardly necessary."));
return NOTHING;
}
if (God(thing)) {
notify(player, T("Destroying God would be blasphemous."));
return NOTHING;
}
/* To destroy, you must either:
* 1. Control it
* 2. Control its source or destination if it's an exit
* 3. Be dealing with a dest-ok thing and pass its lock/destroy
*/
if (!controls(player, thing) &&
!(IsExit(thing) &&
(controls(player, Destination(thing)) ||
controls(player, Source(thing)))) &&
!(DestOk(thing) && eval_lock(player, thing, Destroy_Lock))) {
notify(player, T("Permission denied."));
return NOTHING;
}
if (thing == PLAYER_START || thing == MASTER_ROOM || thing == BASE_ROOM ||
thing == DEFAULT_HOME || God(thing)) {
notify(player, T("That is too special to be destroyed."));
return NOTHING;
}
if (REALLY_SAFE) {
if (Safe(thing) && !DestOk(thing)) {
notify(player,
T
("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, T("That object is marked SAFE. Use @nuke to destroy it."));
return NOTHING;
}
}
/* check to make sure there's no accidental destruction */
if (!confirm && !Owns(player, thing) && !DestOk(thing)) {
notify(player,
T("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 (!IsPlayer(player)) {
notify(player, T("Programs don't kill people; people kill people!"));
return NOTHING;
}
/* The only player a player can own() is themselves...
* If they somehow manage to own() another player, they can't
* nuke that one either...which seems like a good plan, although
* the error message is a bit confusing. -DTC
*/
if (!Wizard(player)) {
notify(player, T("Sorry, no suicide allowed."));
return NOTHING;
}
/* Already checked for God(thing), so use Wizard() */
if (Wizard(thing) && !God(player)) {
notify(player, T("Even you can't do that!"));
return NOTHING;
}
if (Connected(thing)) {
notify(player,
T("How gruesome. You may not destroy players who are connected."));
return NOTHING;
}
if (!confirm) {
notify(player, T("You must use @nuke to destroy a player."));
return NOTHING;
}
break;
case TYPE_THING:
if (!confirm && Wizard(thing)) {
notify(player,
T("That object is set WIZARD. You must use @nuke to destroy it."));
return NOTHING;
}
break;
case TYPE_ROOM:
break;
case TYPE_EXIT:
break;
}
return thing;
}
/** User interface to destroy an object.
* \verbatim
* This is the top-level function for @destroy.
* \endverbatim
* \param player the enactor.
* \param name name of object to destroy.
* \param confirm if 1, called with /override (or nuke).
*/
void
do_destroy(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, T("Destroyed."));
return;
}
/* Present informative messages. */
if (!REALLY_SAFE && Safe(thing))
notify(player,
T
("Warning: Target is set SAFE, but scheduling for destruction anyway."));
switch (Typeof(thing)) {
case TYPE_ROOM:
/* wait until dbck */
notify_except(Contents(thing), NOTHING,
T("The room shakes and begins to crumble."), 0);
if (Owns(player, thing))
notify_format(player,
T("You will be rewarded shortly for %s."),
object_header(player, thing));
else {
notify_format(player,
T
("The wrecking ball is on its way for %s's %s and its exits."),
Name(Owner(thing)), object_header(player, thing));
notify_format(Owner(thing),
T("%s has scheduled your room %s to be destroyed."),
Name(player), object_header(Owner(thing), thing));
}
break;
case TYPE_PLAYER:
/* wait until dbck */
notify_format(player,
(DESTROY_POSSESSIONS ?
(REALLY_SAFE ?
T
("%s and all their (non-SAFE) objects are scheduled to be destroyed.")
:
T
("%s and all their objects are scheduled to be destroyed."))
: T("%s is scheduled to be destroyed.")),
object_header(player, thing));
break;
case TYPE_THING:
if (!Owns(player, thing)) {
notify_format(player, T("%s's %s is scheduled to be destroyed."),
Name(Owner(thing)), object_header(player, thing));
if (!DestOk(thing))
notify_format(Owner(thing),
T("%s has scheduled your %s for destruction."),
Name(player), object_header(Owner(thing), thing));
} else {
notify_format(player, T("%s is scheduled to be destroyed."),
object_header(player, thing));
}
break;
case TYPE_EXIT:
if (!Owns(player, thing)) {
notify_format(Owner(thing),
T("%s has scheduled your %s for destruction."),
Name(player), object_header(Owner(thing), thing));
notify_format(player,
T("%s's %s is scheduled to be destroyed."),
Name(Owner(thing)), object_header(player, thing));
} else
notify_format(player,
T("%s is scheduled to be destroyed."),
object_header(player, thing));
break;
default:
do_log(LT_ERR, NOTHING, NOTHING, T("Surprising type in do_destroy."));
return;
}
pre_destroy(player, thing);
return;
}
/** Spare an object from slated destruction.
* \verbatim
* This is the top-level function for @undestroy.
* Not undestroy, quite--it's actually 'remove it from its status as about
* to be destroyed.'
* \endverbatim
* \param player the enactor.
* \param name name of object to be spared.
*/
void
do_undestroy(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, T("Alas, your efforts of mercy are in vain."));
return;
}
if (undestroy(player, thing)) {
notify_format(Owner(thing),
T("Your %s has been spared from destruction."),
object_header(Owner(thing), thing));
if (player != Owner(thing)) {
notify_format(player,
T("%s's %s has been spared from destruction."),
Name(Owner(thing)), object_header(player, thing));
}
} else {
notify(player, T("That can't be undestroyed."));
}
}
/* Section II: Functions that manage the actual work of destroying
* Objects.
*/
/* Schedule something to be destroyed, run @adestroy, etc. */
static void
pre_destroy(dbref player, dbref thing)
{
dbref tmp;
if (Going(thing) || IsGarbage(thing)) {
/* we've already covered this thing. No need to do so again. */
return;
}
set_flag_internal(thing, "GOING");
clear_flag_internal(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_format(Owner(thing),
T("%s has scheduled your %s for destruction."),
Name(player), object_header(Owner(thing), thing));
}
}
break;
default:
do_log(LT_ERR, NOTHING, NOTHING, T("Surprising type in pre_destroy."));
return;
}
if (ADESTROY_ATTR)
did_it(player, thing, NULL, NULL, NULL, NULL, "ADESTROY", NOTHING);
return;
}
/** Spare an object from destruction.
* Not undestroy, quite--it's actually 'remove it from its status as about
* to be destroyed.' This is the internal function used in hardcode.
* \param player the enactor.
* \param thing dbref of object to be spared.
* \return 1 successful undestruction.
* \return 0 thing is not a valid object to undestroy.
*/
int
undestroy(dbref player, dbref thing)
{
dbref tmp;
if (!Going(thing) || IsGarbage(thing)) {
return 0;
}
clear_flag_internal(thing, "GOING");
clear_flag_internal(thing, "GOING_TWICE");
if (!Halted(thing))
(void) queue_attribute_noparent(thing, "STARTUP", thing);
/* undestroy owner, if need be. */
if (Going(Owner(thing))) {
if (Owner(thing) != player) {
notify_format(player,
T("%s has been spared from destruction."),
object_header(player, Owner(thing)));
notify_format(Owner(thing),
T("You have been spared from destruction by %s."),
Name(player));
} else {
notify(player, T("You have been spared from destruction."));
}
(void) 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) &&
!(IsExit(tmp) && !Owns(thing, Source(tmp)) && Going(Source(tmp)))) {
(void) undestroy(player, tmp);
}
}
break;
case TYPE_THING:
break;
case TYPE_EXIT:
/* undestroy containing room. */
if (Going(Source(thing))) {
(void) undestroy(player, Source(thing));
notify_format(player,
T("The room %s has been spared from destruction."),
object_header(player, Source(thing)));
if (Owner(Source(thing)) != player) {
notify_format(Owner(Source(thing)),
T("The room %s has been spared from destruction by %s."),
object_header(Owner(Source(thing)), Source(thing)),
Name(player));
}
}
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) {
(void) undestroy(player, tmp);
}
}
break;
default:
do_log(LT_ERR, NOTHING, NOTHING, T("Surprising type in un_destroy."));
return 0;
}
return 1;
}
/* 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.
*/
static void
free_object(dbref thing)
{
dbref i, loc;
if (!GoodObject(thing))
return;
local_data_free(thing);
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, T("Unknown type on #%d in free_object."),
thing);
return;
}
change_quota(Owner(thing), QUOTA_COST);
do_halt(thing, "", thing);
/* The equivalent of an @drain/any/all: */
dequeue_semaphores(thing, NULL, INT_MAX, 1, 1);
/* 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) = DEFAULT_HOME;
break;
case TYPE_EXIT:
/* Huh. An exit that claims to be from here, but wasn't linked
* in properly. */
do_rawlog(LT_ERR,
T("ERROR: Exit %s leading from invalid room #%d destroyed."),
unparse_object(GOD, i), 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,
T("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), 0);
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);
List(thing) = NULL;
/* don't eat name otherwise examine will crash */
free_locks(Locks(thing));
Locks(thing) = NULL;
s_Pennies(thing, 0);
Owner(thing) = GOD;
Parent(thing) = NOTHING;
Zone(thing) = NOTHING;
remove_all_obj_chan(thing);
switch (Typeof(thing)) {
/* Make absolutely sure we are removed from Location's content or
exit list. If we are in a room we own and destroy_possessions
is yes, this can happen, causing much ickyness: All garbage
items would be in DEFAULT_HOME. */
case TYPE_PLAYER:
case TYPE_THING:
loc = Location(thing);
if (GoodObject(loc))
Contents(loc) = remove_first(Contents(loc), thing);
if (Typeof(thing) == TYPE_THING)
current_state.things--;
else
current_state.players--;
break;
case TYPE_EXIT: /* This probably won't be needed, but lets make sure */
loc = Source(thing);
if (GoodObject(loc))
Exits(loc) = remove_first(Exits(loc), thing);
current_state.exits--;
break;
case TYPE_ROOM:
current_state.rooms--;
break;
default:
/* Do nothing for rooms. */
break;
}
Type(thing) = TYPE_GARBAGE;
destroy_flag_bitmask(Flags(thing));
Flags(thing) = NULL;
destroy_flag_bitmask(Powers(thing));
Powers(thing) = NULL;
Location(thing) = NOTHING;
set_name(thing, "Garbage");
Exits(thing) = NOTHING;
Home(thing) = NOTHING;
clear_objdata(thing);
Next(thing) = first_free;
first_free = thing;
current_state.garbage++;
#ifdef HAS_ASSERT
assert(IsGarbage(thing));
#endif
}
static void
empty_contents(dbref thing)
{
/* Destroy any exits they may be carrying, send everything else home. */
dbref first;
dbref rest;
dbref target;
notify_except(Contents(thing), NOTHING,
T
("The floor disappears under your feet, you fall through NOTHINGness and then:"),
0);
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:
/* Make sure the home is a reasonable object. */
if (!GoodObject(Home(first)) || IsExit(Home(first)) ||
Home(first) == thing)
Home(first) = DEFAULT_HOME;
target = Home(first);
/* If home isn't a good place to send it, send it to DEFAULT_HOME. */
if (!GoodObject(target) || recursive_member(target, first, 0))
target = DEFAULT_HOME;
if (target != NOTHING) {
/* Use enter_room() on everything so that AENTER and such
* are all triggered properly. */
enter_room(first, target, 0);
}
break;
}
first = rest;
}
}
static void
clear_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);
}
/* Remove object from any following chains */
clear_followers(thing, 0);
clear_following(thing, 0);
/* give player money back */
giveto(Owner(thing), (a = Pennies(thing)));
empty_contents(thing);
clear_flag_internal(thing, "PUPPET");
if (!Quiet(thing) && !Quiet(Owner(thing)))
notify_format(Owner(thing),
T("You get your %d %s deposit back for %s."),
a, ((a == 1) ? MONEY : MONIES),
object_header(Owner(thing), thing));
}
static void
clear_player(dbref thing)
{
dbref i;
ATTR *atemp;
char alias[BUFFER_LEN + 1];
/* Clear out mail. */
do_mail_clear(thing, NULL);
do_mail_purge(thing);
malias_cleanup(thing);
/* Chown any chat channels they own to God */
chan_chownall(thing, GOD);
/* Clear out names from the player list */
delete_player(thing, NULL);
if ((atemp = atr_get_noparent(thing, "ALIAS")) != NULL) {
strcpy(alias, atr_value(atemp));
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, 0);
} else {
free_object(i);
}
}
}
}
static void
clear_room(dbref thing)
{
dbref first, rest;
/* give player money back */
giveto(Owner(thing), ROOM_COST);
empty_contents(thing);
/* Remove exits */
first = Exits(thing);
Source(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 (IsExit(first)) {
free_object(first);
}
first = rest;
}
}
static void
clear_exit(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.
* \return a garbage object or NOTHING.
*/
dbref
free_get(void)
{
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 (!IsGarbage(newobj)) {
static int nrecur = 0;
dbref temp;
if (nrecur++ == 20) {
first_free = NOTHING;
report();
do_rawlog(LT_ERR, T("ERROR: Removed free list and continued\n"));
return (NOTHING);
}
report();
do_rawlog(LT_TRACE, T("ERROR: Object #%d should not be free\n"), newobj);
do_rawlog(LT_TRACE, T("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(void)
{
dbref thing;
first_free = NOTHING;
for (thing = 0; thing < db_top; thing++) {
if (IsGarbage(thing)) {
Next(thing) = first_free;
first_free = thing;
}
}
}
/** Destroy all the objects we said we would destroy later. */
void
purge(void)
{
dbref thing;
for (thing = 0; thing < db_top; thing++) {
if (IsGarbage(thing)) {
continue;
} else if (Going(thing)) {
if (Going_Twice(thing)) {
free_object(thing);
} else {
set_flag_internal(thing, "GOING_TWICE");
}
} else {
continue;
}
}
}
/** Destroy objects slated for destruction.
* \verbatim
* This is the top-level function for @purge.
* \endverbatim
* \param player the enactor.
*/
void
do_purge(dbref player)
{
if (Wizard(player)) {
purge();
notify(player, T("Purge complete."));
} else
notify(player, T("Sorry, you are a mortal."));
}
/* Section III: dbck() and related functions. */
/** The complete db checkup.
*/
void
dbck(void)
{
check_fields();
check_contents();
check_locations();
check_connected_rooms();
check_zones();
local_dbck();
}
/* Do sanity checks on non-destroyed objects. */
static void
check_fields(void)
{
dbref thing;
for (thing = 0; thing < db_top; thing++) {
if (IsGarbage(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) || !IsGarbage(next)) && (next != NOTHING)) {
do_rawlog(LT_ERR, T("ERROR: Invalid next pointer #%d from object %s"),
next, unparse_object(GOD, thing));
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) && IsGarbage(zone))
Zone(thing) = NOTHING;
parent = Parent(thing);
if (GoodObject(parent) && IsGarbage(parent))
Parent(thing) = NOTHING;
owner = Owner(thing);
if (!GoodObject(owner) || IsGarbage(owner) || !IsPlayer(owner)) {
do_rawlog(LT_ERR, T("ERROR: Invalid object owner on %s(%d)"),
Name(thing), thing);
report();
Owner(thing) = GOD;
}
next = Next(thing);
if ((!GoodObject(next) || IsGarbage(next)) && (next != NOTHING)) {
do_rawlog(LT_ERR, T("ERROR: Invalid next pointer #%d from object %s"),
next, unparse_object(GOD, thing));
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) || IsGarbage(home) || IsExit(home))
Home(thing) = DEFAULT_HOME;
if (!GoodObject(loc) || IsGarbage(loc) || IsExit(loc))
enter_room(thing, Home(thing), 0);
break;
case TYPE_EXIT:
if (Contents(thing) != NOTHING) {
/* Eww.. Exits can't have contents. Bad news */
Contents(thing) = NOTHING;
do_rawlog(LT_ERR,
T("ERROR: Exit %s has a contents list. Wiping it out."),
unparse_object(GOD, thing));
}
if (!GoodObject(loc)
&& !((loc == NOTHING) || (loc == AMBIGUOUS) || (loc == HOME))) {
/* Bad news. We're linked to a really impossible object.
* Relink to our source
*/
Destination(thing) = Source(thing);
do_rawlog(LT_ERR,
T
("ERROR: Exit %s leading to invalid room #%d relinked to its source room."),
unparse_object(GOD, thing), home);
} else if (GoodObject(loc) && IsGarbage(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);
do_rawlog(LT_ERR,
T
("ERROR: Exit %s leading to garbage room #%d relinked to its source room."),
unparse_object(GOD, thing), home);
}
/* This must come last */
if (!GoodObject(home) || !IsRoom(home)) {
/* If our source is destroyed, just destroy the exit. */
do_rawlog(LT_ERR,
T
("ERROR: Exit %s leading from invalid room #%d destroyed."),
unparse_object(GOD, thing), home);
free_object(thing);
}
break;
case TYPE_ROOM:
if (GoodObject(home) && IsGarbage(home)) {
/* Eww. Destroyed exit. This isn't supposed to happen. */
do_log(LT_ERR, NOTHING, NOTHING,
T("Found a destroyed exit #%d in room #%d"), home, thing);
}
if (GoodObject(loc) && (IsGarbage(loc) || IsExit(loc))) {
/* Just remove a dropto. */
Location(thing) = NOTHING;
}
break;
}
/* Check attribute ownership. If the attribute is owned by
* an invalid dbref, change its ownership to God.
*/
if (!IsGarbage(thing))
atr_iter_get(GOD, thing, "**", 0, attribute_owner_helper, NULL);
}
}
}
static int
attribute_owner_helper(dbref player __attribute__ ((__unused__)),
dbref thing __attribute__ ((__unused__)),
dbref parent __attribute__ ((__unused__)),
char const *pattern
__attribute__ ((__unused__)), ATTR *atr, void *args
__attribute__ ((__unused__)))
{
if (!GoodObject(AL_CREATOR(atr)))
AL_CREATOR(atr) = GOD;
return 0;
}
static void
check_connected_rooms(void)
{
mark_connected(BASE_ROOM);
check_connected_marks();
}
static void
mark_connected(dbref loc)
{
dbref thing;
if (!GoodObject(loc) || !IsRoom(loc) || Marked(loc))
return;
SetMarked(loc);
/* recursively trace */
for (thing = Exits(loc); thing != NOTHING; thing = Next(thing))
mark_connected(Destination(thing));
}
static void
check_connected_marks(void)
{
dbref loc;
for (loc = 0; loc < db_top; loc++)
if (!IsGarbage(loc) && Marked(loc))
ClearMarked(loc);
else if (IsRoom(loc)) {
if (!Name(loc)) {
do_log(LT_ERR, NOTHING, NOTHING, T("ERROR: no name for room #%d."),
loc);
set_name(loc, "XXXX");
}
if (!Going(loc) && !Floating(loc) && !NoWarnable(loc) &&
(!EXITS_CONNECT_ROOMS || (Exits(loc) == NOTHING))) {
notify_format(Owner(loc), T("You own a disconnected room, %s"),
object_header(Owner(loc), loc));
}
}
}
/* Warn about objects without @lock/zone used as zones */
static void
check_zones(void)
{
dbref n, zone = NOTHING, tmp;
int zone_depth;
for (n = 0; n < db_top; n++) {
if (IsGarbage(n))
continue;
zone = Zone(n);
if (!GoodObject(zone))
continue;
if (ZONE_CONTROL_ZMP && !IsPlayer(zone))
continue;
if (zone != n) /* Objects can be zoned to themselves */
for (zone_depth = MAX_ZONES, tmp = Zone(zone);
zone_depth-- && GoodObject(tmp); tmp = Zone(tmp)) {
if (tmp == n) {
notify_format(Owner(n),
T("You own an object in a circular zone chain: %s"),
object_header(Owner(n), n));
break;
}
if (tmp == Zone(tmp)) /* Object zoned to itself */
break;
}
if (Marked(zone))
continue;
if (getlock(zone, Zone_Lock) == TRUE_BOOLEXP)
SetMarked(zone);
}
for (n = 0; n < db_top; n++) {
if (!IsGarbage(n) && Marked(n)) {
ClearMarked(n);
notify_format(Owner(n),
T
("You own an object without a @lock/zone being used as a zone: %s"),
object_header(Owner(n), n));
}
}
}
/** In this macro, field must be an lvalue whose evaluation has
* no side effects and results in a dbref to be checked.
* All hell will break loose if this is not so.
*/
#define CHECK(field) \
if ((field) != NOTHING) { \
if (!GoodObject(field) || IsGarbage(field)) { \
do_rawlog(LT_ERR, "Bad reference #%d from %s severed.", \
(field), unparse_object(GOD, thing)); \
(field) = NOTHING; \
} else if (IsRoom(field)) { \
do_rawlog(LT_ERR, "Reference to room #%d from %s severed.", \
(field), unparse_object(GOD, thing)); \
(field) = NOTHING; \
} else if (Marked(field)) { \
do_rawlog(LT_ERR, "Multiple references to %s. Reference from #%d severed.", \
unparse_object(GOD, (field)), thing); \
(field) = NOTHING; \
} else { \
SetMarked(field); \
mark_contents(field); \
} \
}
/* An auxiliary function for check_contents. */
static void
mark_contents(dbref thing)
{
if (!GoodObject(thing) || IsGarbage(thing))
return;
SetMarked(thing);
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, T("Bad object type found for %s in mark_contents"),
unparse_object(GOD, thing));
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(void)
{
dbref thing;
for (thing = 0; thing < db_top; thing++) {
if (IsRoom(thing)) {
mark_contents(thing);
}
}
for (thing = 0; thing < db_top; thing++) {
if (!IsRoom(thing) && !IsGarbage(thing) && !Marked(thing)) {
do_rawlog(LT_ERR, T("Object %s not pointed to by anything."),
unparse_object(GOD, thing));
notify_format(Owner(thing),
T("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 DEFAULT_HOME, 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)) &&
!IsGarbage(Location(thing)) && Marked(Location(thing))) {
PUSH(thing, Contents(Location(thing)));
} else if (GoodObject(Home(thing)) &&
!IsGarbage(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(DEFAULT_HOME));
Location(thing) = DEFAULT_HOME;
}
enter_room(thing, Location(thing), 0);
/* If we've managed to reconnect it, then we've reconnected
* its contents. */
mark_contents(Contents(thing));
notify_format(Owner(thing), T("It was moved to %s."),
object_header(Owner(thing), Location(thing)));
do_rawlog(LT_ERR, T("Moved to %s."),
unparse_object(GOD, Location(thing)));
break;
case TYPE_EXIT:
if (GoodObject(Source(thing)) && IsRoom(Source(thing))) {
PUSH(thing, Exits(Source(thing)));
notify_format(Owner(thing), T("It was moved to %s."),
object_header(Owner(thing), Source(thing)));
do_rawlog(LT_ERR, T("Moved to %s."),
unparse_object(GOD, Source(thing)));
} else {
/* Just destroy the exit. */
Source(thing) = NOTHING;
notify(Owner(thing), T("It was destroyed."));
do_rawlog(LT_ERR, T("Orphaned exit destroyed."));
free_object(thing);
}
break;
case TYPE_ROOM:
/* We should never get here. */
do_log(LT_ERR, NOTHING, NOTHING, T("Disconnected room. So what?"));
break;
default:
do_log(LT_ERR, NOTHING, NOTHING,
T("Surprising type on #%d found in check_cycles."), thing);
break;
}
}
}
for (thing = 0; thing < db_top; thing++) {
if (!IsGarbage(thing))
ClearMarked(thing);
}
}
/* 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(void)
{
dbref thing;
dbref loc;
for (loc = 0; loc < db_top; loc++) {
if (!IsExit(loc)) {
for (thing = Contents(loc); thing != NOTHING; thing = Next(thing)) {
if (!Mobile(thing)) {
do_rawlog(LT_ERR,
T
("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,
T("Incorrect location on object %s. Reset to #%d."),
unparse_object(GOD, thing), loc);
Location(thing) = loc;
}
SetMarked(thing);
}
}
if (IsRoom(loc)) {
for (thing = Exits(loc); thing != NOTHING; thing = Next(thing)) {
if (!IsExit(thing)) {
do_rawlog(LT_ERR,
T("ERROR: Exits of room %d corrupt at object %d cleared"),
loc, thing);
/* Remove this from the list and start over. */
Exits(loc) = remove_first(Exits(loc), thing);
thing = Exits(loc);
continue;
} else if (Source(thing) != loc) {
do_rawlog(LT_ERR,
T("Incorrect source on exit %s. Reset to #%d."),
unparse_object(GOD, thing), loc);
}
}
}
}
for (thing = 0; thing < db_top; thing++)
if (!IsGarbage(thing) && Marked(thing))
ClearMarked(thing);
else if (Mobile(thing)) {
do_rawlog(LT_ERR, T("ERROR DBCK: Moved object %d"), thing);
moveto(thing, DEFAULT_HOME);
}
}
/** Database checkup, user interface.
* \verbatim
* This is the top-level function for @dbck. Automatic checks should
* call dbck(), not this.
* \endverbatim
* \param player the enactor.
*/
void
do_dbck(dbref player)
{
if (!Wizard(player)) {
notify(player, T("Silly mortal chicks are for kids!"));
return;
}
notify(player, T("GAME: Performing database consistency check."));
do_log(LT_WIZ, player, NOTHING, T("DBCK done."));
dbck();
notify(player, T("GAME: Database consistency check complete."));
}