#charset "us-ascii"

/* 
 *   Copyright (c) 2000, 2006 by Michael J. Roberts.  All Rights Reserved. 
 *   
 *   TADS 3 Library - Thing
 *   
 *   This module defines Thing, the base class for physical objects in the
 *   simulation.  We also define some utility classes that Thing uses
 *   internally.  
 */

/* include the library header */
#include "adv3.h"


/* ------------------------------------------------------------------------ */
/* 
 *   this property is defined in the 'exits' module, but declare it here
 *   in case we're not including the 'exits' module 
 */
property lookAroundShowExits;


/* ------------------------------------------------------------------------ */
/*
 *   Sense Information entry.  Thing.senseInfoTable() returns a list of
 *   these objects to provide full sensory detail on the objects within
 *   range of a sense.  
 */
class SenseInfo: object
    construct(obj, trans, obstructor, ambient)
    {
        /* set the object being described */
        self.obj = obj;

        /* remember the transparency and obstructor */
        self.trans = trans;
        self.obstructor = obstructor;

        /* 
         *   set the energy level, as seen from the point of view - adjust
         *   the level by the transparency 
         */
        self.ambient = (ambient != nil
                        ? adjustBrightness(ambient, trans)
                        : nil);
    }
    
    /* the object being sensed */
    obj = nil

    /* the transparency from the point of view to this object */
    trans = nil

    /* the obstructor that introduces a non-transparent value of trans */
    obstructor = nil

    /* the ambient sense energy level at this object */
    ambient = nil

    /* 
     *   compare this SenseInfo object's transparency to the other one;
     *   returns a number greater than zero if 'self' is more transparent,
     *   zero if they're equally transparent, or a negative number if
     *   'self' is less transparent 
     */
    compareTransTo(other) { return transparencyCompare(trans, other.trans); }

    /*
     *   Return the more transparent of two SenseInfo objects.  Either
     *   argument can be nil, in which case we'll return the non-nil one;
     *   if both are nil, we'll return nil.  If they're equal, we'll return
     *   the first one.  
     */
    selectMoreTrans(a, b)
    {
        /* if one or the other is nil, return the non-nil one */
        if (a == nil)
            return b;
        else if (b == nil)
            return a;
        else if (a.compareTransTo(b) >= 0)
            return a;
        else
            return b;
    }
;

/*
 *   Can-Touch information.  This object keeps track of whether or not a
 *   given object is able to reach out and touch another object. 
 */
class CanTouchInfo: object
    /* construct, given the touch path */
    construct(path) { touchPath = path; }
    
    /* the full reach-and-touch path from the source to the target */
    touchPath = nil

    /* 
     *   if we have calculated whether or not the source can touch the
     *   target, we'll set the property canTouch to nil or true
     *   accordingly; if this property is left undefined, this information
     *   has never been calculated 
     */
    // canTouch = nil
;

/*
 *   Given a sense information table (a LookupTable returned from
 *   Thing.senseInfoTable()), return a vector of only those objects in the
 *   table that match the given criteria.
 *   
 *   'func' is a function that takes two arguments, func(obj, info), where
 *   'obj' is a simulation object and 'info' is the corresponding
 *   SenseInfo object.  This function is invoked for each object in the
 *   sense info table; if 'func' returns true, then 'obj' is part of the
 *   list that we return.
 *   
 *   The return value is a simple vector of game objects.  (Note that
 *   SenseInfo objects are not returned - just the simulation objects.)  
 */
senseInfoTableSubset(senseTab, func)
{
    local vec;
    
    /* set up a vector for the return list */
    vec = new Vector(32);

    /* scan the table for objects matching criteria given by 'func' */
    senseTab.forEachAssoc(new function(obj, info)
    {
        /* if the function accepts this object, include it in the vector */
        if ((func)(obj, info))
            vec.append(obj);
    });

    /* return the result vector */
    return vec;
}

/*
 *   Sense calculation scratch-pad globals.  Many of the sense
 *   calculations involve recursive descents of portions of the
 *   containment tree.  In the course of these calculations, it's
 *   sometimes useful to have information about the entire operation in
 *   one of the recursive calls.  We could pass the information around as
 *   extra parameters, but that adds overhead, and performance is critical
 *   in the sense routines (because they tend to get invoked a *lot*).  To
 *   reduce the overhead, particularly for information that's not needed
 *   very often, we stuff some information into this global object rather
 *   than passing it around through parameters.
 *   
 *   Note that this object is transient because this information is useful
 *   only during the course of a single tree traversal, and so doesn't
 *   need to be saved or undone.  
 */
transient senseTmp: object
    /* 
     *   The point of view of the sense calculation.  This is the starting
     *   point of a sense traversal; it's the object that's viewing the
     *   other objects.  
     */
    pointOfView = nil

    /* post-calculation notification list */
    notifyList = static new Vector(16)
;


/* ------------------------------------------------------------------------ */
/*
 *   Command check status object.  This is an abstract object that we use
 *   in to report results from a check of various kinds.
 *   
 *   The purpose of this object is to consolidate the code for certain
 *   kinds of command checks into a single routine that can be used for
 *   different purposes - verification, selection from multiple
 *   possibilities (such as multiple paths), and command action
 *   processing.  This object encapsulates a status - success or failure -
 *   and, when the status is failure, a message giving the reason for the
 *   failure.
 */
class CheckStatus: object
    /* did the check succeed or fail? */
    isSuccess = nil

    /* 
     *   the message property or string, and parameters, for failure -
     *   this is for use with reportFailure or the like 
     */
    msgProp = nil
    msgParams = []
;

/* 
 *   Success status object.  Note that this is a single object, not a
 *   class - there's no distinct information per success indicator, so we
 *   only need this single success indicator for all uses. 
 */
checkStatusSuccess: CheckStatus
    isSuccess = true
;

/* 
 *   Failure status object.  Unlike the success indicator, this is a
 *   class, because we need to keep track of the separate failure message
 *   for each kind of failure.  
 */
class CheckStatusFailure: CheckStatus
    construct(prop, [params])
    {
        isSuccess = nil;
        msgProp = prop;
        msgParams = params;
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   Equivalent group state information.  This keeps track of a state and
 *   the number of items in that state when we're listing a group of
 *   equivalent items in different states.  
 */
class EquivalentStateInfo: object
    construct(st, obj, nameProp)
    {
        /* remember the state object and the name property to display */
        stateObj = st;
        stateNameProp = nameProp;

        /* this is the first one in this state */
        stateVec = new Vector(8);
        stateVec.append(obj);
    }

    /* add an object to the list of equivalent objects in this state */
    addEquivObj(obj) { stateVec.append(obj); }

    /* get the number of equivalent items in the same state */
    getEquivCount() { return stateVec.length(); }

    /* get the list of equivalent items in the same state */
    getEquivList() { return stateVec; }

    /* get the name to use for listing purposes */
    getName() { return stateObj.(stateNameProp)(stateVec); }

    /* the ThingState object describing the state */
    stateObj = nil

    /* the property to evaluate to get the name for listing purposes */
    stateNameProp = nil

    /* list of items in this same state */
    stateVec = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Drop Descriptor.  This is passed to the receiveDrop() method of a
 *   "drop destination" when an object is discarded via commands such as
 *   DROP or THROW.  The purpose of the descriptor is to identify the type
 *   of command being performed, so that the receiveDrop() method can
 *   generate an appropriate report message.  
 */
class DropType: object
    /*
     *   Generate the standard report message for the action.  The drop
     *   destination's receiveDrop() method can call this if the standard
     *   message is adequate to describe the result of the action.
     *   
     *   'obj' is the object being dropped, and 'dest' is the drop
     *   destination.  
     */
    // standardReport(obj, dest) { /* subclasses must override */ }

    /*
     *   Get a short report describing the action without saying where the
     *   object ended up.  This is roughly the same as the standard report,
     *   but omits any information on where the object lands, so that the
     *   caller can show a separate message explaining that part.
     *   
     *   The report must be worded such that the object being dropped is
     *   the logical antecedent for any subsequent text.  This means that
     *   callers can use a pronoun to refer back to the object dropped,
     *   allowing for more natural sequences to be constructed.  (It
     *   usually sounds stilted to repeat the full name: "You drop the box.
     *   The box falls into the chasm."  It's better if we can use a
     *   pronoun in the second sentence: "You drop the box. It falls into
     *   the chasm.")
     *   
     *   'obj' is the object being dropped, and 'dest' is the drop
     *   destination.  
     */
    // getReportPrefix(obj, dest) { return ''; }
;

/*
 *   A drop-type descriptor for the DROP command.  Since we have no need to
 *   include any varying parameters in this object, we simply provide this
 *   singleton instance.  
 */
dropTypeDrop: DropType
    standardReport(obj, dest)
    {
        /* show the default "Dropped" response */
        defaultReport(&okayDropMsg);
    }

    getReportPrefix(obj, dest)
    {
        /* return the standard "You drop the <obj>" message */
        return gActor.getActionMessageObj().droppingObjMsg(obj);
    }
;

/*
 *   A drop-type descriptor for the THROW command.  This object keeps track
 *   of the target (the object that was hit by the projectile) and the
 *   projectile's path to the target.  The projectile is simply the direct
 *   object.  
 */
class DropTypeThrow: DropType
    construct(target, path)
    {
        /* remember the target and path */
        target_ = target;
        path_ = path;
    }

    standardReport(obj, dest)
    {
        local nominalDest;
        
        /* get the nominal drop destination */
        nominalDest = dest.getNominalDropDestination();

        /* 
         *   if the actual target and the nominal destination are the same,
         *   just say that it lands on the destination; otherwise, say that
         *   it bounces off the target and falls to the nominal destination
         */
        if (target_ == nominalDest)
            mainReport(&throwFallMsg, obj, target_);
        else
            mainReport(&throwHitFallMsg, obj, target_, nominalDest);
    }

    getReportPrefix(obj, dest)
    {
        /* return the standard "The <projectile> hits the <target>" message */
        return gActor.getActionMessageObj().throwHitMsg(obj, target_);
    }

    /* the object that was hit by the projectile */
    target_ = nil

    /* the path the projectile took to reach the target */
    path_ = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Bag Affinity Info - this class keeps track of the affinity of a bag
 *   of holding for an object it might contain.  We use this class in
 *   building bag affinity lists.  
 */
class BagAffinityInfo: object
    construct(obj, bulk, aff, bag)
    {
        /* save our parameters */
        obj_ = obj;
        bulk_ = bulk;
        aff_ = aff;
        bag_ = bag;
    }

    /*
     *   Compare this item to another item, for affinity ranking purposes.
     *   Returns positive if I should rank higher than the other item,
     *   zero if we have equal ranking, negative if I rank lower than the
     *   other item.  
     */
    compareAffinityTo(other)
    {
        /* 
         *   if this object is the indirect object of 'take from', treat
         *   it as having the lowest ranking 
         */
        if (gActionIs(TakeFrom) && gIobj == obj_)
            return -1;

        /* if we have different affinities, sort according to affinity */
        if (aff_ != other.aff_)
            return aff_ - other.aff_;

        /* we have the same affinity, so put the higher bulk item first */
        if (bulk_ != other.bulk_)
            return bulk_ - other.bulk_;

        /* 
         *   We have the same affinity and same bulk; rank according to
         *   how recently the items were picked up.  Put away the oldest
         *   items first, so the lower holding (older) index has the
         *   higher ranking.  (Note that because lower holding index is
         *   the higher ranking, we return the negative of the holding
         *   index comparison.)  
         */
        return other.obj_.holdingIndex - obj_.holdingIndex;
    }

    /* 
     *   given a vector of affinities, remove the most recent item (as
     *   indicated by holdingIndex) and return the BagAffinityInfo object 
     */
    removeMostRecent(vec)
    {
        local best = vec[1];
        
        /* find the most recent item */
        foreach (local cur in vec)
        {
            /* if this is better than the best so far, remember it */
            if (cur.obj_.holdingIndex > best.obj_.holdingIndex)
                best = cur;
        }

        /* remove the best item from the vector */
        vec.removeElement(best);

        /* return the best item */
        return best;
    }

    /* the object the bag wants to contain */
    obj_ = nil

    /* the object's bulk */
    bulk_ = nil

    /* the bag that wants to contain the object */
    bag_ = nil

    /* affinity of the bag for the object */
    aff_ = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   "State" of a Thing.  This is an object abstractly describing the
 *   state of an object that can assume different states.
 *   
 *   The 'listName', 'inventoryName', and 'wornName' give the names of
 *   state as displayed in room/contents listings, inventory listings, and
 *   listings of items being worn by an actor.  This state name is
 *   displayed along with the item name (usually parenthetically after the
 *   item name, but the exact nature of the display is controlled by the
 *   language-specific part of the library).
 *   
 *   The 'listingOrder' is an integer giving the listing order of this
 *   state relative to other states of the same kind of object.  When we
 *   show a list of equivalent items in different states, we'll order the
 *   state names in ascending order of listingOrder.  
 */
class ThingState: object
    /* 
     *   The name of the state to use in ordinary room/object contents
     *   listings.  If the name is nil, no extra state information is shown
     *   in a listing for an object in this state.  (It's often desirable
     *   to leave the most ordinary state an object can be in unnamed, to
     *   avoid belaboring the obvious.  For example, a match that isn't
     *   burning would probably not want to mention "(not lit)" every time
     *   it's listed.)
     *   
     *   'lst' is a list of the objects being listed in this state.  If
     *   we're only listing a single object, this will be a list with one
     *   element giving the object being listed.  If we're listing a
     *   counted set of equivalent items all in this same state, this will
     *   be the list of items.  Everything in 'lst' will be equivalent (in
     *   the isEquivalent sense).  
     */
    listName(lst) { return nil; }

    /* 
     *   The state name to use in inventory lists.  By default, we just use
     *   the base name.  'lst' has the same meaning as in listName().  
     */
    inventoryName(lst) { return listName(lst); }

    /* 
     *   The state name to use in listings of items being worn.  By
     *   default, we just use the base name.  'lst' has the same meaning as
     *   in listName().  
     */
    wornName(lst) { return listName(lst); }

    /* the relative listing order */
    listingOrder = 0

    /*
     *   Match the name of an object in this state.  'obj' is the object
     *   to be matched; 'origTokens' and 'adjustedTokens' have the same
     *   meanings they do for Thing.matchName; and 'states' is a list of
     *   all of the possible states the object can assume.
     *   
     *   Implementation of this is always language-specific.  In most
     *   cases, this should do something along the lines of checking for
     *   the presence (in the token list) of words that only apply to
     *   other states, rejecting the match if any such words are found.
     *   For example, the ThingState object representing the unlit state
     *   of a light source might check for the presence of 'lit' as an
     *   adjective, and reject the object if it's found.  
     */
    matchName(obj, origTokens, adjustedTokens, states)
    {
        /* by default, simply match the object */
        return obj;
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Object with vocabulary.  This is the base class for any object that
 *   can define vocabulary words.  
 */
class VocabObject: object
    /*
     *   Match a name as used in a noun phrase in a player's command to
     *   this object.  The parser calls this routine to test this object
     *   for a match to a noun phrase when all of the following conditions
     *   are true:
     *   
     *   - this object is in scope;
     *   
     *   - our vocabulary matches the noun phrase, which means that ALL of
     *   the words in the player's noun phrase are associated with this
     *   object with the corresponding parts of speech.  Note the special
     *   wildcard vocabulary words: '#' as an adjective matches any number
     *   used as an adjective; '*' as a noun matches any word used as any
     *   part of speech.
     *   
     *   'origTokens' is the list of the original input words making up
     *   the noun phrase, in canonical tokenizer format.  Each element of
     *   this list is a sublist representing one token.
     *   
     *   'adjustedTokens' is the "adjusted" token list, which provides
     *   more information on how the parser is analyzing the phrase but
     *   may not contain the exact original tokens of the command.  In the
     *   adjusted list, the tokens are represented by pairs of values in
     *   the list: the first value of each pair is a string giving the
     *   adjusted token text, and the second value of the pair is a
     *   property ID giving the part of speech of the parser's
     *   interpretation of the phrase.  For example, if the noun phrase is
     *   "red book", the list might look like ['red', &adjective, 'book',
     *   &noun].
     *   
     *   The adjusted token list in some cases contains different tokens
     *   than the original input.  For example, when the command contains
     *   a spelled-out number, the parser will translate the spelled-out
     *   number to a numeral format and provide the numeral string in the
     *   adjusted token list: 'a hundred and thirty-four' will become
     *   '134' in the adjusted token list.
     *   
     *   If this object does not match the noun phrase, this routine
     *   returns nil.  If the object is a match, the routine returns
     *   'self'.  The routine can also return a different object, or even
     *   a list of objects - in this case, the parser will consider the
     *   noun phrase to have matched the returned object or objects rather
     *   than this original match.
     *   
     *   Note that it isn't necessary to check that the input tokens match
     *   our defined vocabulary words, because the parser will already
     *   have done that for us.  This routine is only called after the
     *   parser has already determined that all of the noun phrase's words
     *   match ours.  
     *   
     *   By default, we do two things.  First, we check to see if ALL of
     *   our tokens are "weak" tokens, and if they are, we indicate that
     *   we do NOT match the phrase.  Second, if we pass the "weak token"
     *   test, we'll invoke the common handling in matchNameCommon(), and
     *   return the result.
     *   
     *   In most cases, games will want to override matchNameCommon()
     *   instead of this routine.  matchNameCommon() is the common handler
     *   for both normal and disambiguation-reply matching, so overriding
     *   that one routine will take care of both kinds of matching.  Games
     *   will only need to override matchName() separately in cases where
     *   they need to differentiate normal matching and disambiguation
     *   matching.  
     */
    matchName(origTokens, adjustedTokens)
    {
        /* if we have a weak-token list, check the tokens */
    weakTest:
        if (weakTokens != nil)
        {
            local sc = languageGlobals.dictComparator;
                
            /* check to see if all of our tokens are "weak" */
            for (local i = 1, local len = adjustedTokens.length() ;
                 i <= len ; i += 2)
            {
                /* get the current token and its type */
                local tok = adjustedTokens[i];
                local typ = adjustedTokens[i+1];

                /* 
                 *   if this is a miscWord token, skip it - these aren't
                 *   vocabulary matches, so we won't find them in either
                 *   our weak or strong vocabulary lists 
                 */
                if (typ == &miscWord)
                    continue;

                /* if this token isn't in our weak list, it's not weak */
                if (weakTokens.indexWhich({x: sc.matchValues(tok, x) != 0})
                    == nil)
                {
                    /* 
                     *   It's not in the weak list, so this isn't a weak
                     *   token; therefore the phrase isn't weak, so we do
                     *   not wish to veto the match.  There's no need to
                     *   keep looking; simply break out of the weak test
                     *   entirely, since we've now passed. 
                     */
                    break weakTest;
                }
            }

            /* 
             *   If we get here, it means we got through the loop without
             *   finding any non-weak tokens.  This means the entire
             *   phrase is weak, which means that we don't match it.
             *   Return nil to indicate that this is not a match.  
             */
            return nil;
         }

        /* invoke the common handling */
        return matchNameCommon(origTokens, adjustedTokens);
    }

    /*
     *   Match a name in a disambiguation response.  This is similar to
     *   matchName(), but is called for each object in an ambiguous object
     *   list for which a disambiguation response was provided.  As with
     *   matchName(), we only call this routine for objects that match the
     *   dictionary vocabulary.
     *   
     *   This routine is separate from matchName() because a
     *   disambiguation response usually only contains a partial name.
     *   For example, the exchange might go something like this:
     *   
     *   >take box
     *.  Which box do you mean, the cardboard box, or the wood box?
     *   >cardboard
     *   
     *   Note that it is not safe to assume that the disambiguation
     *   response can be prepended to the original noun phrase to make a
     *   complete noun phrase; if this were safe, we'd simply concatenate
     *   the two strings and call matchName().  This would work for the
     *   example above, since we'd get "cardboard box" as the new noun
     *   phrase, but it wouldn't work in general.  Consider these examples:
     *   
     *   >open post office box
     *.  Which post office box do you mean, box 100, box 101, or box 102?
     *   
     *   >take jewel
     *.  Which jewel do you mean, the emerald, or the diamond?
     *   
     *   There's no general way of assembling the disambiguation response
     *   and the original noun phrase together into a new noun phrase, so
     *   rather than trying to use matchName() for both purposes, we
     *   simply use a separate routine to match the disambiguation name.
     *   
     *   Note that, when this routine is called, this object will have
     *   been previously matched with matchName(), so there is no question
     *   that this object matches the original noun phrase.  The only
     *   question is whether or not this object matches the response to
     *   the "which one do you mean" question.
     *   
     *   The return value has the same meaning as for matchName().
     *   
     *   By default, we simply invoke the common handler.  Note that games
     *   will usually want to override matchNameCommon() instead of this
     *   routine, since matchNameCommon() provides common handling for the
     *   main match and disambiguation match cases.  Games should only
     *   override this routine when they need to do something different
     *   for normal vs disambiguation matching.    
     */
    matchNameDisambig(origTokens, adjustedTokens)
    {
        /* by default, use the same common processing */
        return matchNameCommon(origTokens, adjustedTokens);
    }

    /*
     *   Common handling for the main matchName() and the disambiguation
     *   handler matchNameDisambig().  By default, we'll check with our
     *   state object if we have a state object; if not, we'll simply
     *   return 'self' to indicate that we do indeed match the given
     *   tokens.
     *   
     *   In most cases, when a game wishes to customize name matching for
     *   an object, it can simply override this routine.  This routine
     *   provides common handling for matchName() and matchNameDisambig(),
     *   so overriding this routine will take care of both the normal and
     *   disambiguation matching cases.  In cases where a game needs to
     *   customize only normal matching or only disambiguation matching,
     *   it can override one of those other routines instead.  
     */
    matchNameCommon(origTokens, adjustedTokens)
    {
        local st;
        
        /* 
         *   if we have a state, ask our state object to check for words
         *   applying only to other states 
         */
        if ((st = getState()) != nil)
            return st.matchName(self, origTokens, adjustedTokens, allStates);

        /* by default, accept the parser's determination that we match */
        return self;
    }

    /*
     *   Plural resolution order.  When a command contains a plural noun
     *   phrase, we'll sort the items that match the plural phrase in
     *   ascending order of this property value.
     *   
     *   In most cases, the plural resolution order doesn't matter.  Once
     *   in a while, though, a set of objects will be named as "first,"
     *   "second," "third," and so on; in these cases, it's nice to have
     *   the order of resolution match the nominal ordering.
     *   
     *   Note that the sorting order only applies within the matches for a
     *   particular plural phrase, not globally throughout the entire list
     *   of objects in a command.  For example, if the player types TAKE
     *   BOXES AND BOOKS, we'll sort the boxes in one group, and then we'll
     *   sort the books as a separate group - but all of the boxes will
     *   come before any of the books, regardless of the plural orders.
     *   
     *   >take books and boxes
     *.  first book: Taken.
     *.  second book: Taken.
     *.  third book: Taken.
     *.  left box: Taken.
     *.  middle box: Taken.
     *.  right box: Taken.
     */
    pluralOrder = 100

    /*
     *   Disambiguation prompt order.  When we interactively prompt for
     *   help resolving an ambiguous noun phrase, we'll put the list of
     *   ambiguous matches in ascending order of this property value.
     *   
     *   In most cases, the prompt order doesn't matter, so most objects
     *   won't have to override the default setting.  Sometimes, though, a
     *   set of objects will be identified in the game as "first",
     *   "second", "third", etc., and in these cases it's desirable to have
     *   the objects presented in the same order as the names indicate:
     *   
     *   Which door do you mean, the first door, the second door, or the
     *   third door?
     *   
     *   By default, we use the same value as our pluralOrder, since the
     *   plural order has essentially the same purpose.  
     */
    disambigPromptOrder = (pluralOrder)

    /*
     *   Intrinsic parsing likelihood.  During disambiguation, if the
     *   parser finds two objects with equivalent logicalness (as
     *   determined by the 'verify' process for the particular Action being
     *   performed), it will pick the one with the higher intrinsic
     *   likelihood value.  The default value is zero, which makes all
     *   objects equivalent by default.  Set a higher value to make the
     *   parser prefer this object in cases of ambiguity.  
     */
    vocabLikelihood = 0

    /*
     *   Filter an ambiguous noun phrase resolution list.  The parser calls
     *   this method for each object that matches an ambiguous noun phrase
     *   or an ALL phrase, to allow the object to modify the resolution
     *   list.  This method allows the object to act globally on the entire
     *   list, so that the filtering can be sensitive to the presence or
     *   absence in the list of other objects, and can affect the presence
     *   of other objects.
     *   
     *   'lst' is a list of ResolveInfo objects describing the tentative
     *   resolution of the noun phrase.  'action' is the Action object
     *   representing the command.  'whichObj' is the object role
     *   identifier of the object being resolved (DirectObject,
     *   IndirectObject, etc).  'np' is the noun phrase production that
     *   we're resolving; this is usually a subclass of NounPhraseProd.
     *   'requiredNum' is the number of objects required, when an exact
     *   quantity is specified; this is nil in cases where the quantity is
     *   unspecified, as in 'all' or an unquantified plural ("take coins").
     *   
     *   The result is a new list of ResolveInfo objects, which need not
     *   contain any of the objects of the original list, and can add new
     *   objects not in the original list, as desired.
     *   
     *   By default, we simply return the original list.  
     */
    filterResolveList(lst, action, whichObj, np, requiredNum)
    {
        /* return the original list unchanged */
        return lst;
    }

    /*
     *   Expand a pronoun list.  This is essentially complementary to
     *   filterResolveList: the function is to "unfilter" a pronoun binding
     *   that contains this object so that it restores any objects that
     *   would have been filtered out by filterResolveList from the
     *   original noun phrase binding.
     *   
     *   This routine is called whenever the parser is called upon to
     *   resolve a pronoun ("TAKE THEM").  This routine is called for each
     *   object in the "raw" pronoun binding, which is simply the list of
     *   objects that was stored by the previous command as the antecedent
     *   for the pronoun.  After this routine has been called for each
     *   object in the raw pronoun binding, the final list will be passed
     *   through filterResolveList().
     *   
     *   'lst' is the raw pronoun binding so far, which might reflect
     *   changes made by this method called on previous objects in the
     *   list.  'typ' is the pronoun type (PronounIt, PronounThem, etc)
     *   describing the pronoun phrase being resolved.  The return value is
     *   the new pronoun binding list; if this routine doesn't need to make
     *   any changes, it should simply return 'lst'.
     *   
     *   In some cases, filterResolveList chooses which of two or more
     *   possible ways to bind a noun phrase, with the binding dependent
     *   upon other conditions, such as the current action.  In these
     *   cases, it's often desirable for a subsequent pronoun reference to
     *   make the same decision again, choosing from the full set of
     *   possible bindings.  This routine facilitates that by letting the
     *   object put back objects that were filtered out, so that the
     *   filtering can once again run on the full set of possible bindings
     *   for the pronoun reference.
     *   
     *   This base implementation just returns the original list unchanged.
     *   See CollectiveGroup for an override that uses this.  
     */
    expandPronounList(typ, lst) { return lst; }

    /*
     *   Our list of "weak" tokens.  This is a token that is acceptable in
     *   our vocabulary, but which we can only use in combination with one
     *   or more "strong" tokens.  (A token is strong if it's not weak, so
     *   we need only keep track of one or the other kind.  Weak tokens
     *   are much less common than strong tokens, so it takes a lot less
     *   space if we store the weak ones instead of the strong ones.)
     *   
     *   The purpose of weak tokens is to allow players to use more words
     *   to refer to some objects without creating ambiguity.  For
     *   example, if we have a house, and a front door of the house, we
     *   might want to allow the player to call the front door "front door
     *   of house."  If we just defined the door's vocabulary thus,
     *   though, we'd create ambiguity if the player tried to refer to
     *   "house," even though this obviously doesn't create any ambiguity
     *   to a human reader.  Weak tokens fix the problem: we define
     *   "house" as a weak token for the front door, which allows the
     *   player to refer to the front door as "front door of house", but
     *   prevents the front door from matching just "house".
     *   
     *   By default, this is nil to indicate that we don't have any weak
     *   tokens to check.  If the object has weak tokens, this should be
     *   set to a list of strings giving the weak tokens.  
     */
    weakTokens = nil

    /*
     *   By default, every object can be used as the resolution of a
     *   possessive qualifier phrase (e.g., "bob" in "bob's book").  If
     *   this property is set to nil for an object, that object can never
     *   be used as a possessive.  Note that has nothing to do with
     *   establishing ownership relationships between objects; it controls
     *   only the resolution of possessive phrases during parsing to
     *   concrete game objects.  
     */
    canResolvePossessive = true

    /*
     *   Throw an appropriate parser error when this object is used in a
     *   player command as a possessive qualifier (such as when 'self' is
     *   the "bob" in "take bob's key"), and we don't own anything matching
     *   the object name that we qualify.  This is only called when 'self'
     *   is in scope.  By default, we throw the standard parser error ("Bob
     *   doesn't appear to have any such thing").  'txt' is the
     *   lower-cased, HTMLified text that of the qualified object name
     *   ("key" in "bob's key").  
     */
    throwNoMatchForPossessive(txt)
        { throw new ParseFailureException(&noMatchForPossessive, self, txt); }

    /* 
     *   Throw an appropriate parser error when this object is used in a
     *   player command to locationally qualify another object (such as
     *   when we're the box in "examine the key in the box"), and there's
     *   no object among our contents with the given name.  By default, we
     *   throw the standard parser error ("You see no key in the box").  
     */
    throwNoMatchForLocation(txt)
        { throw new ParseFailureException(&noMatchForLocation, self, txt); }

    /*
     *   Throw an appropriate parser error when this object is used in a
     *   player command to locationally qualify "all" (such as when we're
     *   the box in "examine everything in the box"), and we have no
     *   contents.  By default, we throw the standard parser error ("You
     *   see nothing unusual in the box"). 
     */
    throwNothingInLocation()
        { throw new ParseFailureException(&nothingInLocation, self); }

    /*
     *   Get a list of the other "facets" of this object.  A facet is
     *   another program object that to the player looks like the same or
     *   part of the same physical object.  For example, it's often
     *   convenient to represent a door using two game objects - one for
     *   each side - but the two really represent the same door from the
     *   player's perspective.
     *   
     *   The parser uses an object's facets to resolve a pronoun when the
     *   original antecedent goes out of scope.  In our door example, if
     *   we refer to the door, then walk through it to the other side,
     *   then refer to 'it', the parser will realize from the facet
     *   relationship that 'it' now refers to the other side of the door.  
     */
    getFacets() { return []; }

    /*
     *   Ownership: a vocab-object can be marked as owned by a given Thing.
     *   This allows command input to refer to the owned object using
     *   possessive syntax (such as, in English, "x's y").
     *   
     *   This method returns true if 'self' is owned by 'obj'.  The parser
     *   generally tests for ownership in this direction, as opposed to
     *   asking for obj's owner, because a given object might have multiple
     *   owners, and might not be able to enumerate them all (or, at least,
     *   might not be able to enumerate them efficiently).  It's usually
     *   efficient to determine whether a given object qualifies as an
     *   owner, and from the parser's persepctive that's the question
     *   anyway, since it wants to determine if the "x" in "x's y"
     *   qualifies as my owner.
     *   
     *   By default, we simply return true if 'obj' matches our 'owner'
     *   property (and is not nil).  
     */
    isOwnedBy(obj) { return owner != nil && owner == obj; }

    /*
     *   Get our nominal owner.  This is the owner that we report for this
     *   object if we're asked to distinguish this object from another
     *   object in a disambiguation prompt.  The nominal owner isn't
     *   necessarily the only owner.  Note that if getNominalOwner()
     *   returns a non-nil value, isOwnedBy(getNominalOwner()) should
     *   always return true.
     *   
     *   By default, we'll simply return our 'owner' property.  
     */
    getNominalOwner() { return owner; }

    /* our explicit owner, if any */
    owner = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Find the best facet from the given list of facets, from the
 *   perspective of the given actor.  We'll find the facet that has the
 *   best visibility, or, visibilities being equal, the best touchability. 
 */
findBestFacet(actor, lst)
{
    local infoList;
    local best;

    /* 
     *   if the list has no elements, there's no result; if it has only one
     *   element, it's obviously the best one 
     */
    if (lst.length() == 0)
        return nil;
    if (lst.length() == 1)
        return lst[1];
    
    /* make a list of the visibilities of the given facets */
    infoList = lst.mapAll({x: new SightTouchInfo(actor, x)});

    /* scan the list and find the entry with the best results */
    best = nil;
    foreach (local cur in infoList)
        best = SightTouchInfo.selectBetter(best, cur);

    /* return the best result */
    return best.obj_;
}

/*
 *   A small data structure class recording SenseInfo objects for sight and
 *   touch.  We use this to pick the best facet from a list of facets.  
 */
class SightTouchInfo: object
    construct(actor, obj)
    {
        /* remember our object */
        obj_ = obj;
        
        /* get the visual SenseInfo in the actor's best sight-like sense */
        visInfo = actor.bestVisualInfo(obj);

        /* get the 'touch' SenseInfo for the object */
        touchInfo = actor.senseObj(touch, obj);
    }

    /* the object we're associated with */
    obj_ = nil

    /* our SenseInfo objects for sight and touch */
    visInfo = nil
    touchInfo = nil

    /*
     *   Class method: select the "better" of two SightTouchInfo's.
     *   Returns the one with the more transparent visual status, or,
     *   visual transparencies being equal, the one with the more
     *   transparent touch status. 
     */
    selectBetter(a, b)
    {
        local d;
        
        /* if one or the other is nil, return the non-nil one */
        if (a == nil)
            return b;
        if (b == nil)
            return a;

        /* if the visual transparencies differ, compare visually */
        d = a.visInfo.compareTransTo(b.visInfo);
        if (d > 0)
            return a;
        else if (d < 0)
            return b;
        else
        {
            /* the visual status it the same, so compare by touch */
            d = a.touchInfo.compareTransTo(b.touchInfo);
            if (d >= 0)
                return a;
            else
                return b;
        }
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Thing: the basic class for game objects.  An object of this class
 *   represents a physical object in the simulation.
 */
class Thing: VocabObject
    /* 
     *   on constructing a new Thing, initialize it as we would a
     *   statically instantiated Thing 
     */
    construct()
    {
        /* 
         *   If we haven't already been through here for this object, do
         *   the static initialization.  (Because many library classes and
         *   game objects inherit from multiple Thing subclasses, we'll
         *   sometimes inherit this constructor multiple times in the
         *   course of creating a single new object.  The set-up work we do
         *   isn't meant to be repeated and can create internal
         *   inconsistencies if it is.) 
         */
        if (!isThingConstructed)
        {
            /* inherit base class construction */
            inherited();

            /* initialize the Thing properties */
            initializeThing();

            /* note that we've been through this constructor now */
            isThingConstructed = true;
        }
    }

    /* 
     *   Have we been through Thing.construct() yet for this object?  Note
     *   that this will only be set for dynamically created instances
     *   (i.e., objects created with 'new'). 
     */
    isThingConstructed = nil

    /*
     *   My global parameter name.  This is a name that can be used in
     *   {xxx} parameters in messages to refer directly to this object.
     *   This is nil by default, which means we have no global parameter
     *   name.  Define this to a single-quoted string to set a global
     *   parameter name.
     *   
     *   Global parameter names can be especially useful for objects whose
     *   names change in the course of the game, such as actors who are
     *   known by one name until introduced, after which they're known by
     *   another name.  It's a little nicer to write "{He/the bob}" than
     *   "<<bob.theName>>".  We can do this with a global parameter name,
     *   because it allows us to use {bob} as a message parameter, even
     *   when the actor isn't involved directly in any command.
     *   
     *   Note that these parameter names are global, so no two objects are
     *   allowed to have the same name.  These names are also subordinate
     *   to the parameter names in the current Action, so names that the
     *   actions define, such as 'dobj' and 'actor', should usually be
     *   avoided.  
     */
    globalParamName = nil

    /* 
     *   Set the global parameter name dynamically.  If you need to add a
     *   new global parameter name at run-time, call this rather than
     *   setting the property directly, to ensure that the name is added to
     *   the message builder's name table.  You can also use this to delete
     *   an object's global parameter name, by passing nil for the new
     *   name.
     *   
     *   (You only need to use this method if you want to add or change a
     *   name dynamically at run-time, because the library automatically
     *   initializes the table for objects with globalParamName settings
     *   defined at compile-time.)  
     */
    setGlobalParamName(name)
    {
        /* if we already had a name in the table, remove our old name */
        if (globalParamName != nil
            && langMessageBuilder.nameTable_[globalParamName] == self)
            langMessageBuilder.nameTable_.removeElement(globalParamName);

        /* remember our name locally */
        globalParamName = name;

        /* add the name to the message builder's table */
        if (name != nil)
            langMessageBuilder.nameTable_[name] = self;
    }

    /*
     *   "Special" description.  This is the generic place to put a
     *   description of the object that should appear in the containing
     *   room's full description.  If the object defines a special
     *   description, the object is NOT listed in the basic contents list
     *   of the room, because listing it with the contents would be
     *   redundant with the special description.
     *   
     *   By default, we have no special description.  If a special
     *   description is desired, define this to a double-quoted string
     *   containing the special description, or to a method that displays
     *   the special description.  
     */
    specialDesc = nil

    /*
     *   The special descriptions to use under obscured and distant
     *   viewing conditions.  By default, these simply display the normal
     *   special description, but these can be overridden if desired to
     *   show different messages under these viewing conditions.
     *   
     *   Note that if you override these alternative special descriptions,
     *   you MUST also provide a base specialDesc.  The library figures
     *   out whether or not to show any sort of specialDesc first, based
     *   on the presence of a non-nil specialDesc; only then does the
     *   library figure out which particular variation to use.
     *   
     *   Note that remoteSpecialDesc takes precedence over these methods.
     *   That is, when 'self' is in a separate top-level room from the
     *   point-of-view actor, we'll use remoteSpecialDesc to generate our
     *   description, even if the sense path to 'self' is distant or
     *   obscured.  
     */
    distantSpecialDesc = (specialDesc)
    obscuredSpecialDesc = (specialDesc)

    /*
     *   The "remote" special description.  This is the special
     *   description that will be used when this object is not in the
     *   point-of-view actor's current top-level room, but is visible in a
     *   connected room.  For example, if two top-level rooms are
     *   connected by a window, so that an actor in one room can see the
     *   objects in the other room, this method will be used to generate
     *   the description of the object when the actor is in the other
     *   room, viewing this object through the window.
     *   
     *   By default, we just use the special description.  It's usually
     *   better to customize this to describe the object from the given
     *   point of view.  'actor' is the point-of-view actor.  
     */
    remoteSpecialDesc(actor) { distantSpecialDesc; }

    /*
     *   List order for the special description.  Whenever there's more
     *   than one object showing a specialDesc at the same time (in a
     *   single room description, for example), we'll use this to order the
     *   specialDesc displays.  We'll display in ascending order of this
     *   value.  By default, we use the same value for everything, so
     *   listing order is arbitrary; when one specialDesc should appear
     *   before or after another, this property can be used to control the
     *   relative ordering.  
     */
    specialDescOrder = 100

    /*
     *   Special description phase.  We list special descriptions for a
     *   room's full description in two phases: one phase before we show
     *   the room's portable contents list, and another phase after we show
     *   the contents list.  This property controls the phase in which we
     *   show this item's special description.  This only affects special
     *   descriptions that are shown with room descriptions; in other
     *   cases, such as "examine" descriptions of objects, all of the
     *   special descriptions are usually shown together.
     *   
     *   By default, we show our special description (if any) before the
     *   room's contents listing, because most special descriptions act
     *   like extensions of the room's main description and thus should be
     *   grouped directly with the room's descriptive text.  Objects with
     *   special descriptions that are meant to indicate more ephemeral
     *   properties of the location can override this to be listed after
     *   the room's portable contents.
     *   
     *   One situation where you usually will want to list a special
     *   description after contents is when the special description applies
     *   to an item that's contained in a portable item.  Since the
     *   container will be listed with the room contents, as it's portable,
     *   we'll usually want the special description of this child item to
     *   show up after the contents listing, so that it shows up after its
     *   container is mentioned.
     *   
     *   Note that the specialDescOrder is secondary to this phase
     *   grouping, because we essentially list special items in two
     *   separate groups.  
     */
    specialDescBeforeContents = true

    /*
     *   Show my special description, given a SenseInfo object for the
     *   visual sense path from the point of view of the description.  
     */
    showSpecialDescWithInfo(info, pov)
    {
        local povActor = getPOVActorDefault(gActor);
        
        /* 
         *   Determine what to show, based on the point-of-view location
         *   and the sense path.  If the point of view isn't in the same
         *   top-level location as 'self', OR the actor isn't the same as
         *   the POV, use the remote special description; otherwise, select
         *   the obscured, distant, or basic special description according
         *   to the transparency of the sense path.  
         */
        if (getOutermostRoom() != povActor.getOutermostRoom()
            || pov != povActor)
        {
            /* 
             *   different top-level rooms, or different actor and POV -
             *   use the remote description 
             */
            showRemoteSpecialDesc(povActor);
        }
        else if (info.trans == obscured)
        {
            /* we're obscured, so show our obscured special description */
            showObscuredSpecialDesc();
        }
        else if (info.trans == distant)
        {
            /* we're at a distance, so use our distant special description */
            showDistantSpecialDesc();
        }
        else if (canDetailsBeSensed(sight, info, pov))
        {
            /* 
             *   we're not obscured or distant, and our details can be
             *   sensed, so show our fully-visible special description 
             */
            showSpecialDesc();
        }
    }

    /*
     *   Show the special description, if we have one.  If we are using
     *   our initial description, we'll show that; otherwise, if we have a
     *   specialDesc property, we'll show that.
     *   
     *   Note that the initial description overrides the specialDesc
     *   property whenever useInitSpecialDesc() returns true.  This allows
     *   an object to have both an initial description that is used until
     *   the object is moved, and a separate special description used
     *   thereafter.  
     */
    showSpecialDesc()
    {
        /* 
         *   if we are to use our initial description, show that;
         *   otherwise, show our special description 
         */
        if (useInitSpecialDesc())
            initSpecialDesc;
        else
            specialDesc;
    }

    /* show the special description under obscured viewing conditions */
    showObscuredSpecialDesc()
    {
        if (useInitSpecialDesc())
            obscuredInitSpecialDesc;
        else
            obscuredSpecialDesc;
    }

    /* show the special description under distant viewing conditions */
    showDistantSpecialDesc()
    {
        if (useInitSpecialDesc())
            distantInitSpecialDesc;
        else
            distantSpecialDesc;
    }

    /* show the remote special description */
    showRemoteSpecialDesc(actor)
    {
        if (useInitSpecialDesc())
            remoteInitSpecialDesc(actor);
        else
            remoteSpecialDesc(actor);
    }

    /*
     *   Determine if we should use a special description.  By default, we
     *   have a special description if we have either a non-nil
     *   specialDesc property, or we have an initial description.  
     */
    useSpecialDesc()
    {
        /* 
         *   if we have a non-nil specialDesc, or we are to use an initial
         *   description, we have a special description 
         */
        return propType(&specialDesc) != TypeNil || useInitSpecialDesc();
    }

    /*
     *   Determine if we should use our special description in the given
     *   room's LOOK AROUND description.  By default, this simply returns
     *   useSpecialDesc().  
     */
    useSpecialDescInRoom(room) { return useSpecialDesc(); }

    /*
     *   Determine if we should use our special description in the given
     *   object's contents listings, for the purposes of "examine <cont>"
     *   or "look in <cont>".  By default, we'll use our special
     *   description for a given container if we'd use our special
     *   description in general, AND we're actually inside the container
     *   being examined.  
     */
    useSpecialDescInContents(cont)
    {
        /* 
         *   if we would otherwise use a special description, and we are
         *   contained within the given object, use our special description
         *   in the container 
         */
        return useSpecialDesc() && self.isIn(cont);
    }

    /*
     *   Show our special description as part of a parent's full
     *   description.  
     */
    showSpecialDescInContentsWithInfo(info, pov, cont)
    {
        local povActor = getPOVActorDefault(gActor);
        
        /* determine what to show, based on the location and sense path */
        if (getOutermostRoom() != povActor.getOutermostRoom()
            || pov != povActor)
            showRemoteSpecialDescInContents(povActor, cont);
        else if (info.trans == obscured)
            showObscuredSpecialDescInContents(povActor, cont);
        else if (info.trans == distant)
            showDistantSpecialDescInContents(povActor, cont);
        else if (canDetailsBeSensed(sight, info, pov))
            showSpecialDescInContents(povActor, cont);
    }

    /* 
     *   Show the special description in contents listings under various
     *   sense conditions.  By default, we'll use the corresponding special
     *   descriptions for full room descriptions.  These can be overridden
     *   to show special versions of the description when we're examining
     *   particular containers, if desired.  'actor' is the actor doing the
     *   looking.  
     */
    showSpecialDescInContents(actor, cont)
        { showSpecialDesc(); }
    showObscuredSpecialDescInContents(actor, cont)
        { showObscuredSpecialDesc(); }
    showDistantSpecialDescInContents(actor, cont)
        { showDistantSpecialDesc(); }
    showRemoteSpecialDescInContents(actor, cont)
        { showRemoteSpecialDesc(actor); }

    /*
     *   If we define a non-nil initSpecialDesc, this property will be
     *   called to describe the object in room listings as long as the
     *   object is in its "initial" state (as determined by isInInitState:
     *   this is usually true until the object is first moved to a new
     *   location).  By default, objects don't have initial descriptions.
     *   
     *   If this is non-nil, and the object is portable, this will be used
     *   (as long as the object is in its initial state) instead of
     *   showing the object in an ordinary room-contents listing.  This
     *   can be used to give the object a special mention in its initial
     *   location in the game, rather than letting the ordinary
     *   room-contents lister lump it in with all of the other portable
     *   object lying around.  
     */
    initSpecialDesc = nil

    /*
     *   The initial descriptions to use under obscured and distant
     *   viewing conditions.  By default, these simply show the plain
     *   initSpecialDesc; these can be overridden, if desired, to show
     *   alternative messages when viewing conditions are less than ideal.
     *   
     *   Note that in order for one of these alternative initial
     *   descriptions to be shown, the regular initSpecialDesc MUST be
     *   defined, even if it's never actually used.  We make the decision
     *   to display these other descriptions based on the existence of a
     *   non-nil initSpecialDesc, so always define initSpecialDesc
     *   whenever these are defined.  
     */
    obscuredInitSpecialDesc = (initSpecialDesc)
    distantInitSpecialDesc = (initSpecialDesc)

    /* the initial remote special description */
    remoteInitSpecialDesc(actor) { distantInitSpecialDesc; }

    /*
     *   If we define a non-nil initDesc, and the object is in its initial
     *   description state (as indicated by isInInitState), then we'll use
     *   this property instead of "desc" to describe the object when
     *   examined.  This can be used to customize the description the
     *   player sees in parallel to initSpecialDesc.  
     */
    initDesc = nil

    /*
     *   Am I in my "initial state"?  This is used to determine if we
     *   should show the initial special description (initSpecialDesc) and
     *   initial examine description (initDesc) when describing the
     *   object.  By default, we consider the object to be in its initial
     *   state until the first time it's moved.
     *   
     *   You can override this to achieve other effects.  For example, if
     *   you want to use the initial description only the first time the
     *   object is examined, and then revert to the ordinary description,
     *   you could override this to return (!described).  
     */
    isInInitState = (!moved)

    /*
     *   Determine whether or not I should be mentioned in my containing
     *   room's description (on LOOK AROUND) using my initial special
     *   description (initSpecialDesc).  This returns true if I have an
     *   initial description that isn't nil, and I'm in my initial state.
     *   If this returns nil, the object should be described in room
     *   descriptions using the ordinary generated message (either the
     *   specialDesc, if we have one, or the ordinary mention in the list
     *   of portable room contents).  
     */
    useInitSpecialDesc()
        { return isInInitState && propType(&initSpecialDesc) != TypeNil; }

    /* 
     *   Determine if I should be described on EXAMINE using my initial
     *   examine description (initDesc).  This returns true if I have an
     *   initial examine desription that isn't nil, and I'm in my initial
     *   state.  If this returns nil, we'll show our ordinary description
     *   (given by the 'desc' property).  
     */
    useInitDesc()
        { return isInInitState && propType(&initDesc) != TypeNil; }


    /*
     *   Flag: I've been moved out of my initial location.  Whenever we
     *   move the object to a new location, we'll set this to true.  
     */
    moved = nil

    /*
     *   Flag: I've been seen by the player character.  This is nil by
     *   default; we set this to true whenever we show a room description
     *   from the player character's perspective, and the object is
     *   visible.  The object doesn't actually have to be mentioned in the
     *   room description to be marked as seen - it merely has to be
     *   visible to the player character.
     *   
     *   Note that this is only the DEFAULT 'seen' property, which all
     *   Actor objects use by default.  The ACTUAL property that a given
     *   Actor uses depends on the actor's seenProp, which allows a game to
     *   keep track separately of what each actor has seen by using
     *   different 'seen' properties for different actors.  
     */
    seen = nil

    /*
     *   Flag: suppress the automatic setting of the "seen" status for this
     *   object in room and object descriptions.  Normally, we'll
     *   automatically mark every visible object as seen (by calling
     *   gActor.setHasSeen()) whenever we do a LOOK AROUND.  We'll also
     *   automatically mark as seen every visible object within an object
     *   examined explicitly (such as with EXAMINE, LOOK IN, LOOK ON, or
     *   OPEN).  This property can override this automatic status change:
     *   when this property is true, we will NOT mark this object as seen
     *   in any of these cases.  When this property is true, the game must
     *   explicitly mark the object as seen, if and when desired, by
     *   calling actor.setHasSeen().
     *   
     *   Sometimes, an object is not meant to be immediately obvious.  For
     *   example, a puzzle box might have a hidden button that can't be
     *   seen on casual examination.  In these cases, you can use
     *   suppressAutoSeen to ensure that the object won't be marked as seen
     *   merely by virtue of its being visible at the time of a LOOK AROUND
     *   or EXAMINE command.
     *   
     *   Note that this property does NOT affect the object's actual
     *   visibility or other sensory attributes.  This property merely
     *   controls the automatic setting of the "seen" status of the object.
     */
    suppressAutoSeen = nil

    /*
     *   Flag: I've been desribed.  This is nil by default; we set this to
     *   true whenever the player explicitly examines the object. 
     */
    described = nil

    /*
     *   Mark all visible contents of 'self' as having been seen.
     *   'infoTab' is a LookupTable of sight information, as returned by
     *   visibleInfoTable().  This should be called any time an object is
     *   examined in such a way that its contents should be considered to
     *   have been seen.
     *   
     *   We will NOT mark as seen any objects that have suppressAutoSeen
     *   set to true.  
     */
    setContentsSeenBy(infoTab, actor)
    {
        /* 
         *   run through the table of visible objects, and mark each one
         *   that's contained within 'self' as having been seen 
         */
        infoTab.forEachAssoc(new function(obj, info)
        {
            if (obj.isIn(self) && !obj.suppressAutoSeen)
                actor.setHasSeen(obj);
        });
    }

    /*
     *   Mark everything visible as having been seen.  'infoTab' is a
     *   LookupTable of sight information, as returned by
     *   visibleInfoTable().  We'll mark everything visible in the table
     *   as having been seen by the actor, EXCEPT objects that have
     *   suppressAutoSeen set to true.  
     */
    setAllSeenBy(infoTab, actor)
    {
        /* mark everything as seen, except suppressAutoSeen items */
        infoTab.forEachAssoc(new function(obj, info)
        {
            if (!obj.suppressAutoSeen)
                actor.setHasSeen(obj);
        });
    }

    /*
     *   Note that I've been seen by the given actor, setting the given
     *   "seen" property.  This routine notifies the object that it's just
     *   been observed by the given actor, allowing it to take any special
     *   action it wants to take in such cases.  
     */
    noteSeenBy(actor, prop)
    {
        /* by default, simply set the given "seen" property to true */
        self.(prop) = true;
    }

    /*
     *   Flag: this object is explicitly "known" to actors in the game,
     *   even if it's never been seen.  This allows the object to be
     *   resolved as a topic in ASK ABOUT commands and the like.
     *   Sometimes, actors know about an object even before it's been seen
     *   - they might simply know about it from background knowledge, or
     *   they might hear about it from another character, for example.
     *   
     *   Like the 'seen' property, this is merely the DEFAULT 'known'
     *   property that we use for actors.  Each actor can individually use
     *   a separate property to track its own knowledge if it prefers; it
     *   can do this simply by overriding its isKnownProp property.  
     */
    isKnown = nil

    /*
     *   Hide from 'all' for a given action.  If this returns true, this
     *   object will never be included in 'all' lists for the given action
     *   class.  This should generally be set only for objects that serve
     *   some sort of internal purpose and don't represent physical
     *   objects in the model world.  By default, objects are not hidden
     *   from 'all' lists.
     *   
     *   Note that returning nil doesn't put the object into an 'all'
     *   list.  Rather, it simply *leaves* it in any 'all' list it should
     *   happen to be in.  Each action controls its own selection criteria
     *   for 'all', and different verbs use different criteria.  No matter
     *   how an action chooses its 'all' list, though, an item will always
     *   be excluded if hideFromAll() returns true for the item.  
     */
    hideFromAll(action) { return nil; }

    /*
     *   Hide from defaulting for a given action.  By default, we're
     *   hidden from being used as a default object for a given action if
     *   we're hidden from the action for 'all'. 
     */
    hideFromDefault(action) { return hideFromAll(action); }
    
    /*
     *   Determine if I'm to be listed at all in my room's description,
     *   and in descriptions of objects containing my container.
     *   
     *   Most objects should be listed normally, but some types of objects
     *   should be suppressed from the normal room listing.  For example,
     *   fixed-in-place scenery objects are generally described in the
     *   custom message for the containing room, so these are normally
     *   omitted from the listing of the room's contents.
     *   
     *   By default, we'll return the same thing as isListedInContents -
     *   that is, if this object is to be listed when its *direct*
     *   container is examined, it'll also be listed by default when any
     *   further enclosing container (including the enclosing room) is
     *   described.  Individual objects can override this to use different
     *   rules.
     *   
     *   Why would we want to be able to list an object when examining in
     *   its direct container, but not when examining an enclosing
     *   container, or the enclosing room?  The most common reason is to
     *   control the level of detail, to avoid overloading the broad
     *   description of the room and the main things in it with every
     *   detail of every deeply-nested container.  
     */
    isListed { return isListedInContents; }

    /*
     *   Determine if I'm listed in explicit "examine" and "look in"
     *   descriptions of my direct container.
     *   
     *   By default, we return true as long as we're not using our special
     *   description in this particular context.  Examining or looking in a
     *   container will normally show special message for any contents of
     *   the container, so we don't want to list the items with special
     *   descriptions in the ordinary list as well.  
     */
    isListedInContents { return !useSpecialDescInContents(location); }

    /*
     *   Determine if I'm listed in inventory listings.  By default, we
     *   include every item in an inventory list.
     */
    isListedInInventory { return true; }

    /* 
     *   by default, regular objects are not listed when they arrive
     *   aboard vehicles (only actors are normally listed in this fashion) 
     */
    isListedAboardVehicle = nil

    /*
     *   Determine if I'm listed as being located in the given room part.
     *   We'll first check to make sure the object is nominally contained
     *   in the room part; if not, it's not listed in the room part.  We'll
     *   then ask the room part itself to make the final determination.  
     */
    isListedInRoomPart(part)
    {
        /* 
         *   list me if I'm nominally contained in the room part AND the
         *   room part wants me listed 
         */
        return (isNominallyInRoomPart(part)
                && part.isObjListedInRoomPart(self));
    }

    /*
     *   Determine if we're nominally in the given room part (floor,
     *   ceiling, wall, etc).  This returns true if we *appear* to be
     *   located directly in/on the given room part object.
     *   
     *   In most cases, a portable object might start out with a special
     *   initial room part location, but once moved and then dropped
     *   somewhere, ends up nominally in the nominal drop destination of
     *   the location where it was dropped.  For example, a poster might
     *   start out being nominally attached to a wall, or a light bulb
     *   might be nominally hanging from the ceiling; if these objects are
     *   taken and then dropped somewhere, they'll simply end up on the
     *   floor.
     *   
     *   Our default behavior models this.  If we've never been moved,
     *   we'll indicate that we're in our initial room part location,
     *   given by initNominalRoomPartLocation.  Once we've been moved (or
     *   if our initNominalRoomPartLocation is nil), we'll figure out what
     *   the nominal drop destination is for our current container, and
     *   then see if we appear to be in that nominal drop destination.  
     */
    isNominallyInRoomPart(part)
    {
        /* 
         *   If we're not directly in the apparent container of the given
         *   room part, then we certainly are not even nominally in the
         *   room part.  We only appear to be in a room part when we're
         *   actually in the room directly containing the room part. 
         */
        if (location == nil
            || location.getRoomPartLocation(part) != location)
            return nil;

        /* if we've never been moved, use our initial room part location */
        if (!moved && initNominalRoomPartLocation != nil)
            return (part == initNominalRoomPartLocation);

        /* if we have an explicit special room part location, use that */
        if (specialNominalRoomPartLocation != nil)
            return (part == specialNominalRoomPartLocation);

        /*
         *   We don't seem to have an initial or special room part
         *   location, but there's still one more possibility: if the room
         *   part is the nominal drop destination, then we are by default
         *   nominally in that room part, because that's where we
         *   otherwise end up in the absence of any special room part
         *   location setting.  
         */
        if (location.getNominalDropDestination() == part)
            return true;

        /* we aren't nominally in the given room part */
        return nil;
    }

    /*
     *   Determine if I should show my special description with the
     *   description of the given room part (floor, ceiling, wall, etc).
     *   
     *   By default, we'll include our special description with a room
     *   part's description if either (1) we are using our initial
     *   description, and our initNominalRoomPartLocation is the given
     *   part; or (2) we are using our special description, and our
     *   specialNominalRoomPartLocation is the given part.
     *   
     *   Note that, by default, we do NOT use our special description for
     *   the "default" room part location - that is, for the nominal drop
     *   destination for our containing room, which is where we end up by
     *   default, in the absence of an initial or special room part
     *   location setting.  We don't use our special description in this
     *   default location because special descriptions are most frequently
     *   used to describe an object that is specially situated, and hence
     *   we don't want to assume a default situation.  
     */
    useSpecialDescInRoomPart(part)
    {
        /* if we're not nominally in the part, rule it out */
        if (!isNominallyInRoomPart(part))
            return nil;

        /* 
         *   if we're using our initial description, and we are explicitly
         *   in the given room part initially, use the special description
         *   for the room part 
         */
        if (useInitSpecialDesc() && initNominalRoomPartLocation == part)
            return true;

        /* 
         *   if we're using our special description, and we are explicitly
         *   in the given room part as our special location, use the
         *   special description 
         */
        if (useSpecialDesc() && specialNominalRoomPartLocation == part)
            return true;

        /* do not use the special description in the room part */
        return nil;
    }

    /* 
     *   Our initial room part location.  By default, we set this to nil,
     *   which means that we'll use the nominal drop destination of our
     *   actual initial location when asked.  If desired, this can be set
     *   to another part; for example, if a poster is initially described
     *   as being "on the north wall," this should set to the default north
     *   wall object.  
     */
    initNominalRoomPartLocation = nil

    /*
     *   Our "special" room part location.  By default, we set this to
     *   nil, which means that we'll use the nominal drop destination of
     *   our actual current location when asked.
     *   
     *   This property has a function similar to
     *   initNominalRoomPartLocation, but is used to describe the nominal
     *   room part container of the object has been moved (and even before
     *   it's been moved, if initNominalRoomPartLocation is nil).
     *   
     *   It's rare for an object to have a special room part location
     *   after it's been moved, because most games simply don't provide
     *   commands for things like re-attaching a poster to a wall or
     *   re-hanging a fan from the ceiling.  When it is possible to move
     *   an object to a new special location, though, this property can be
     *   used to flag its new special location.  
     */
    specialNominalRoomPartLocation = nil

    /*
     *   Determine if my contents are to be listed when I'm shown in a
     *   listing (in a room description, inventory list, or a description
     *   of my container).
     *   
     *   Note that this doesn't affect listing my contents when I'm
     *   *directly* examined - use contentsListedInExamine to control that.
     *   
     *   By default, we'll list our contents in these situations if (1) we
     *   have some kind of presence in the description of whatever it is
     *   the player is looking at, either by being listed in the ordinary
     *   way (that is, isListed is true) or in a custom way (via a
     *   specialDesc), and (2) contentsListedInExamine is true.  The
     *   reasoning behind this default is that if the object itself shows
     *   up in the description, then we usually want its contents to be
     *   mentioned as well, but if the object itself isn't mentioned, then
     *   its contents probably shouldn't be, either.  And in any case, if
     *   the contents aren't listed even when we *directly* examine the
     *   object, then we almost certainly don't want its contents mentioned
     *   when we only indirectly mention the object in the course of
     *   describing something enclosing it.  
     */
    contentsListed = (contentsListedInExamine
                      && (isListed || useSpecialDesc()))

    /*
     *   Determine if my contents are to be listed when I'm directly
     *   examined (with an EXAMINE/LOOK AT command).  By default, we
     *   *always* list our contents when we're directly examined.  
     */
    contentsListedInExamine = true

    /*
     *   Determine if my contents are listed separately from my own list
     *   entry.  If this is true, then my contents will be listed in a
     *   separate sentence from my own listing; for example, if 'self' is a
     *   box, we'll be listed like so:
     *   
     *.    You are carrying a box. The box contains a key and a book.
     *   
     *   By default, this is nil, which means that we list our contents
     *   parenthetically after our name:
     *   
     *.     You are carrying a box (which contains a key and a book).
     *   
     *   Using a separate listing is sometimes desirable for an object that
     *   will routinely contain listable objects that in turn have listable
     *   contents of their own, because it can help break up a long listing
     *   that would otherwise use too many nested parentheticals.
     *   
     *   Note that this only applies to "wide" listings; "tall" listings
     *   will show contents in the indented tree format regardless of this
     *   setting.  
     */
    contentsListedSeparately = nil

    /*
     *   The language-dependent part of the library must provide a couple
     *   of basic descriptor methods that we depend upon.  We provide
     *   several descriptor methods that we base on the basic methods.
     *   The basic methods that must be provided by the language extension
     *   are:
     *   
     *   listName - return a string giving the "list name" of the object;
     *   that is, the name that should appear in inventory and room
     *   contents lists.  This is called with the visual sense info set
     *   according to the point of view.
     *   
     *   countName(count) - return a string giving the "counted name" of
     *   the object; that is, the name as it should appear with the given
     *   quantifier.  In English, this might return something like 'one
     *   box' or 'three books'.  This is called with the visual sense info
     *   set according to the point of view.  
     */

    /*
     *   Show this item as part of a list.  'options' is a combination of
     *   ListXxx flags indicating the type of listing.  'infoTab' is a
     *   lookup table of SenseInfo objects giving the sense information
     *   for the point of view.  
     */
    showListItem(options, pov, infoTab)
    {
        /* show the list, using the 'listName' of the state */
        showListItemGen(options, pov, infoTab, &listName);
    }

    /*
     *   General routine to show the item as part of a list.
     *   'stateNameProp' is the property to use in any listing state
     *   object to obtain the state name.  
     */
    showListItemGen(options, pov, infoTab, stateNameProp)
    {
        local info;
        local st;
        local stName;

        /* get my visual information from the point of view */
        info = infoTab[self];

        /* show the item's list name */
        say(withVisualSenseInfo(pov, info, &listName));

        /* 
         *   If we have a list state with a name, show it.  Note that to
         *   obtain the state name, we have to pass a list of the objects
         *   being listed to the stateNameProp method of the state object;
         *   we're the only object we're showing for this particular
         *   display list element, so we simply pass a single-element list
         *   containing 'self'.  
         */
        if ((st = getStateWithInfo(info, pov)) != nil
            && (stName = st.(stateNameProp)([self])) != nil)
        {
            /* we have a state with a name - show it */
            gLibMessages.showListState(stName);
        }
    }

    /*
     *   Show this item as part of a list, grouped with a count of
     *   list-equivalent items.
     */
    showListItemCounted(lst, options, pov, infoTab)
    {
        /* show the item using the 'listName' property of the state */
        showListItemCountedGen(lst, options, pov, infoTab, &listName);
    }

    /* 
     *   General routine to show this item as part of a list, grouped with
     *   a count of list-equivalent items.  'stateNameProp' is the
     *   property of any list state object that we should use to obtain
     *   the name of the listing state.  
     */
    showListItemCountedGen(lst, options, pov, infoTab, stateNameProp)
    {
        local info;
        local stateList;
        
        /* get the visual information from the point of view */
        info = infoTab[self];
        
        /* show our counted name */
        say(countListName(lst.length(), pov, info));

        /* check for list states */
        stateList = new Vector(10);
        foreach (local cur in lst)
        {
            local st;

            /* get this item's sense information from the list */
            info = infoTab[cur];
            
            /* 
             *   if this item has a list state with a name, include it in
             *   our list of states to show 
             */
            if ((st = cur.getStateWithInfo(info, pov)) != nil
                && st.(stateNameProp)(lst) != nil)
            {
                local stInfo;

                /* 
                 *   if this state is already in our list, simply
                 *   increment the count of items in this state;
                 *   otherwise, add the new state to the list 
                 */
                stInfo = stateList.valWhich({x: x.stateObj == st});
                if (stInfo != nil)
                {
                    /* it's already in the list - count the new item */
                    stInfo.addEquivObj(cur);
                }
                else
                {
                    /* it's not in the list - add it */
                    stateList.append(
                        new EquivalentStateInfo(st, cur, stateNameProp));
                }
            }
        }

        /* if the state list is non-empty, show it */
        if (stateList.length() != 0)
        {
            /* put the state list in its desired order */
            stateList.sort(SortAsc, {a, b: (a.stateObj.listingOrder
                                            - b.stateObj.listingOrder)});
                                           
            /*
             *   If there's only one item in the state list, and all of
             *   the objects are in that state, then show the "all in
             *   state" message.  Otherwise, show the list of states with
             *   counts.
             *   
             *   (Note that it's possible to have just one state in the
             *   list without having all of the objects in this state,
             *   because we could have some of the objects in an unnamed
             *   state, in which case they wouldn't contribute to the list
             *   at all.)  
             */
            if (stateList.length() == 1
                && stateList[1].getEquivCount() == lst.length())
            {
                /* everything is in the same state */
                gLibMessages.allInSameListState(stateList[1].getEquivList(),
                                                stateList[1].getName());
            }
            else
            {
                /* list the state items */
                equivalentStateLister.showListAll(stateList.toList(), 0, 0);
            }
        }
    }

    /*
     *   Single-item counted listing description.  This is used to display
     *   an item with a count of equivalent items ("four gold coins").
     *   'info' is the sense information from the current point of view
     *   for 'self', which we take to be representative of the sense
     *   information for all of the equivalent items.  
     */
    countListName(equivCount, pov, info)
    {
        return withVisualSenseInfo(pov, info, &countName, equivCount);
    }

    /*
     *   Show this item as part of an inventory list.  By default, we'll
     *   show the regular list item name.  
     */
    showInventoryItem(options, pov, infoTab)
    {
        /* show the item, using the inventory state name */
        showListItemGen(options, pov, infoTab, &inventoryName);
    }
    showInventoryItemCounted(lst, options, pov, infoTab)
    {
        /* show the item, using the inventory state name */
        showListItemCountedGen(lst, options, pov, infoTab, &inventoryName);
    }

    /*
     *   Show this item as part of a list of items being worn.
     */
    showWornItem(options, pov, infoTab)
    {
        /* show the item, using the worn-listing state name */
        showListItemGen(options, pov, infoTab, &wornName);
    }
    showWornItemCounted(lst, options, pov, infoTab)
    {
        /* show the item, using the worn-listing state name */
        showListItemCountedGen(lst, options, pov, infoTab, &wornName);
    }

    /*
     *   Get the "listing state" of the object, given the visual sense
     *   information for the object from the point of view for which we're
     *   generating the listing.  This returns a ThingState object
     *   describing the object's state for the purposes of listings.  This
     *   should return nil if the object doesn't have varying states for
     *   listings.
     *   
     *   By default, we return a list state if the visual sense path is
     *   transparent or attenuated, or we have large visual scale.  In
     *   other cases, we assume that the details of the object are not
     *   visible under the current sense conditions; since the list state
     *   is normally a detail of the object, we don't return a list state
     *   when the details of the object are not visible.  
     */
    getStateWithInfo(info, pov)
    {
        /* 
         *   if our details can be sensed, return the list state;
         *   otherwise, return nil, since the list state is a detail 
         */
        if (canDetailsBeSensed(sight, info, pov))
            return getState();
        else
            return nil;
    }

    /* 
     *   Get our state - returns a ThingState object describing the state.
     *   By default, we don't have varying states, so we simply return
     *   nil.  
     */
    getState = nil

    /*
     *   Get a list of all of our possible states.  For an object that can
     *   assume varying states as represented by getState, this should
     *   return the list of all possible states that the object can
     *   assume.  
     */
    allStates = []

    /*
     *   The default long description, which is displayed in response to
     *   an explicit player request to examine the object.  We'll use a
     *   generic library message; most objects should override this to
     *   customize the object's desription.
     *   
     *   Note that we show this as a "default descriptive report," because
     *   this default message indicates merely that there's nothing
     *   special to say about the object.  If we generate any additional
     *   description messages, such as status reports ("it's open" or "it
     *   contains a gold key") or special descriptions for things inside,
     *   we clearly *do* have something special to say about the object,
     *   so we'll want to suppress the nothing-special message.  To
     *   accomplish this suppression, all we have to do is report our
     *   generic description as a default descriptive report, and the
     *   transcript will automatically filter it out if there are any
     *   other reports for this same action.
     *   
     *   Note that any time this is overridden by an object with any sort
     *   of actual description, the override should NOT use
     *   defaultDescReport.  Instead, simply set this to display the
     *   descriptive message directly:
     *   
     *   desc = "It's a big green box. " 
     */
    desc { defaultDescReport(&thingDescMsg, self); }

    /*
     *   Our LOOK IN description.  This is shown when we explicitly LOOK IN
     *   the object.  By default, we just report that there's nothing
     *   unusual inside.  
     */
    lookInDesc { mainReport(&nothingInsideMsg); }

    /*
     *   The default "distant" description.  If this is defined for an
     *   object, then we evaluate it to display the description when an
     *   actor explicitly examines this object from a point of view where
     *   we have a "distant" sight path to the object.
     *   
     *   If this property is left undefined for an object, then we'll
     *   describe this object when it's distant in one of two ways.  If
     *   the object has its 'sightSize' property set to 'large', we'll
     *   display the ordinary 'desc', because its large visual size makes
     *   its details visible at a distance.  If the 'sightSize' is
     *   anything else, we'll instead display the default library message
     *   indicating that the object is too far away to see any details.
     *   
     *   To display a specific description when the object is distant, set
     *   this to a double-quoted string, or to a method that displays a
     *   message.  
     */
    // distantDesc = "the distant description"

    /* the default distant description */
    defaultDistantDesc { gLibMessages.distantThingDesc(self); }

    /*
     *   The "obscured" description.  If this is defined for an object,
     *   then we call it to display the description when an actor
     *   explicitly examines this object from a point of view where we
     *   have an "obscured" sight path to the object.
     *   
     *   If this property is left undefined for an object, then we'll
     *   describe this object when it's obscured in one of two ways.  If
     *   the object has its 'sightSize' property set to 'large', we'll
     *   display the ordinary 'desc', because its large visual size makes
     *   its details visible even when the object is obscured.  If the
     *   'sightSize' is anything else, we'll instead display the default
     *   library message indicating that the object is too obscured to see
     *   any details.
     *   
     *   To display a specific description when the object is visually
     *   obscured, override this to a method that displays your message.
     *   'obs' is the object that's obstructing the view - this will be
     *   something on our sense path, such as a dirty window, that the
     *   actor has to look through to see 'self'.  
     */
    // obscuredDesc(obs) { "the obscured description"; }

    /*
     *   The "remote" description.  If this is defined for an object, then
     *   we call it to display the description when an actor explicitly
     *   examines this object from a separate top-level location.  That
     *   is, when the actor's outermost enclosing room is different from
     *   our own outermost enclosing room, we'll use this description.
     *   
     *   If this property is left undefined, then we'll describe this
     *   object when it's distant as though it were in the same room.  So,
     *   we'll select the obscured, distant, or ordinary description,
     *   according to the sense path.  
     */
    // remoteDesc(pov) { "the remote description" }

    /* the default obscured description */
    defaultObscuredDesc(obs) { gLibMessages.obscuredThingDesc(self, obs); }

    /* 
     *   The "sound description," which is the description displayed when
     *   an actor explicitly listens to the object.  This is used when we
     *   have a transparent sense path and no associated "emanation"
     *   object; when we have an associated emanation object, we use its
     *   description instead of this one.  
     */
    soundDesc { defaultDescReport(&thingSoundDescMsg, self); }

    /* distant sound description */
    distantSoundDesc { gLibMessages.distantThingSoundDesc(self); }

    /* obscured sound description */
    obscuredSoundDesc(obs) { gLibMessages.obscuredThingSoundDesc(self, obs); }

    /*
     *   The "smell description," which is the description displayed when
     *   an actor explicitly smells the object.  This is used when we have
     *   a transparent sense path to the object, and we have no
     *   "emanation" object; when we have an associated emanation object,
     *   we use its description instead of this one.  
     */
    smellDesc { defaultDescReport(&thingSmellDescMsg, self); }

    /* distant smell description */
    distantSmellDesc { gLibMessages.distantThingSmellDesc(self); }

    /* obscured smell description */
    obscuredSmellDesc(obs) { gLibMessages.obscuredThingSmellDesc(self, obs); }

    /*
     *   The "taste description," which is the description displayed when
     *   an actor explicitly tastes the object.  Note that, unlike sound
     *   and smell, we don't distinguish levels of transparency or
     *   distance with taste, because tasting an object requires direct
     *   physical contact with it.  
     */
    tasteDesc { gLibMessages.thingTasteDesc(self); }

    /* 
     *   The "feel description," which is the description displayed when
     *   an actor explicitly feels the object.  As with taste, we don't
     *   distinguish transparency or distance. 
     */
    feelDesc { gLibMessages.thingFeelDesc(self); }

    /*
     *   Show the smell/sound description for the object as part of a room
     *   description.  These are displayed when the object is in the room
     *   and it has a presence in the corresponding sense.  By default,
     *   these show nothing.
     *   
     *   In most cases, regular objects don't override these, because most
     *   regular objects have no direct sensory presence of their own.
     *   Instead, a Noise or Odor is created and added to the object's
     *   direct contents, and the Noise or Odor provides the object's
     *   sense presence.  
     */
    smellHereDesc() { }
    soundHereDesc() { }

    /*
     *   "Equivalence" flag.  If this flag is set, then all objects with
     *   the same immediate superclass will be considered interchangeable;
     *   such objects will be listed collectively in messages (so we would
     *   display "five coins" rather than "a coin, a coin, a coin, a coin,
     *   and a coin"), and will be treated as equivalent in resolving noun
     *   phrases to objects in user input.
     *   
     *   By default, this property is nil, since we want most objects to
     *   be treated as unique.  
     */
    isEquivalent = nil

    /*
     *   My Distinguisher list.  This is a list of Distinguisher objects
     *   that can be used to distinguish this object from other objects.
     *   
     *   Distinguishers are listed in order of priority.  The
     *   disambiguation process looks for distinguishers capable of telling
     *   objects apart, starting with the first in the list.  The
     *   BasicDistinguisher is generally first in every object's list,
     *   because any two objects can be told apart if they come from
     *   different classes.
     *   
     *   By default, each object has the "basic" distinguisher, which tells
     *   objects apart on the basis of the "isEquivalent" property and
     *   their superclasses; the ownership distinguisher, which tells
     *   objects apart based on ownership; and the location distinguisher,
     *   which identifies objects by their immediate containers.  
     */
    distinguishers = [basicDistinguisher,
                      ownershipDistinguisher,
                      locationDistinguisher]

    /*
     *   Get the distinguisher to use for printing this object's name in an
     *   action announcement (such as a multi-object, default object, or
     *   vague-match announcement).  We check the global option setting to
     *   see if we should actually use distinguishers for this; if so, we
     *   call getInScopeDistinguisher() to find the correct distinguisher,
     *   otherwise we use the "null" distinguisher, which simply lists
     *   objects by their base names.  
     */
    getAnnouncementDistinguisher()
    {
        return (gameMain.useDistinguishersInAnnouncements
                ? getInScopeDistinguisher()
                : nullDistinguisher);
    }

    /*
     *   Get a distinguisher that differentiates me from all of the other
     *   objects in scope, if possible, or at least from some of the other
     *   objects in scope.  We use this to generate object announcements,
     *   since it will do the best job of distinguishing the object being
     *   announced from other nearby objects.  
     */
    getInScopeDistinguisher()
    {
        local bestDist, bestCnt;

        /* get the list of objects in scope, excluding myself */
        local s = gActor.scopeList() - self;

        /* 
         *   now include only the objects that are basic equivalents -
         *   these are the only ones we care about telling apart from us,
         *   since all of the other objects can be distinguished by their
         *   disambig name 
         */
        s = s.subset({obj: !basicDistinguisher.canDistinguish(self, obj)});

        /* if we're unique by vocabulary, don't bother */
        if (s.length() == 0)
            return basicDistinguisher;

        /* as a last resort, fall back on the basic distinguisher */
        bestDist = basicDistinguisher;
        bestCnt = s.countWhich({obj: bestDist.canDistinguish(self, obj)});

        /* 
         *   run through my distinguisher list looking for the
         *   distinguisher that can tell me apart from the largest number
         *   of my vocab equivalents 
         */
        foreach (local dist in distinguishers)
        {
            /* if this is the default, skip it */
            if (dist == bestDist)
                continue;

            /* check to see how many objects 'dist' can distinguish me from */
            local cnt = s.countWhich({obj: dist.canDistinguish(self, obj)});

            /* 
             *   if it can distinguish me from every other object, use this
             *   one - it uniquely identifies me 
             */
            if (cnt == s.length())
                return dist;

            /* 
             *   that can't distinguish us from everything else here, but
             *   if it's the best so far, remember it; we'll fall back on
             *   the best that we find if we fail to find a perfect
             *   distinguisher 
             */
            if (cnt > bestCnt)
            {
                bestDist = dist;
                bestCnt = cnt;
            }
        }

        /*
         *   We didn't find any distinguishers that can tell me apart from
         *   every other object, so choose the one that can tell me apart
         *   from the most other objects. 
         */
        return bestDist;
    }

    /*
     *   Determine if I'm equivalent, for the purposes of command
     *   vocabulary, to given object.
     *   
     *   We'll run through our list of distinguishers and check with each
     *   one to see if it can tell us apart from the other object.  If we
     *   can find at least one distinguisher that can tell us apart, we're
     *   not equivalent.  If we have no distinguisher that can tell us
     *   apart from the other object, we're equivalent.  
     */
    isVocabEquivalent(obj)
    {
        /* 
         *   Check each distinguisher - if we can find one that can tell
         *   us apart from the other object, we're not equivalent.  If
         *   there are no distinguishers that can tell us apart, we're
         *   equivalent.  
         */
        return (distinguishers.indexWhich(
            {cur: cur.canDistinguish(self, obj)}) == nil);
    }

    /*
     *   My associated "collective group" objects.  A collective group is
     *   an abstract object, not part of the simulation (i.e, not directly
     *   manipulable by characters as a separate object) that can stand in
     *   for an entire group of related objects in some actions.  For
     *   example, we might have a collective "money" object that stands in
     *   for any group of coins and paper bills for "examine" commands, so
     *   that when the player says something like "look at money" or "count
     *   money," we use the single collective money object to handle the
     *   command rather than running the command iteratively on each of the
     *   individual coins and bills present.
     *   
     *   We can have be associated with more than one collective group.
     *   For example, a diamond could be in a "treasure" group as well as a
     *   "jewelry" group.  
     *   
     *   The associated collective objects are normally of class
     *   CollectiveGroup.  By default, if our 'collectiveGroup' property is
     *   not nil, our list consists of that one item; otherwise we just use
     *   an empty list.  
     */
    collectiveGroups = (collectiveGroup != nil ? [collectiveGroup] : [])

    /*
     *   Our collective group.  Note - this property is obsolescent; it's
     *   supported only for compatibility with old code.  New games should
     *   use 'collectiveGroups' instead.  
     */
    collectiveGroup = nil

    /*
     *   Are we associated with the given collective group?  We do if it's
     *   in our collectiveGroups list.  
     */
    hasCollectiveGroup(g) { return collectiveGroups.indexOf(g) != nil; }

    /*
     *   "List Group" objects.  This specifies a list of ListGroup objects
     *   that we use to list this object in object listings, such as
     *   inventory lists and room contents lists.
     *   
     *   An object can be grouped in more than one way.  When multiple
     *   groups are specified here, the order is significant:
     *   
     *   - To the extent two groups entirely overlap, which is to say that
     *   one of the pair entirely contains the other (for example, if
     *   every coin is a kind of money, then the "money" listing group
     *   would contain every object in the "coin" group, plus other
     *   objects as well: the coin group is a subset of the money group),
     *   the groups must be listed from most general to most specific (for
     *   our money/coin example, then, money would come before coin in the
     *   group list).
     *   
     *   - When two groups do not overlap, then the earlier one in our
     *   list is given higher priority.
     *   
     *   By default, we return an empty list.
     */
    listWith = []

    /* 
     *   Get the list group for my special description.  This works like
     *   the ordinary listWith, but is used to group me with other objects
     *   showing special descriptions, rather than in ordinary listings.
     */
    specialDescListWith = []

    /*
     *   Our interior room name.  This is the status line name we display
     *   when an actor is within this object and can't see out to the
     *   enclosing room.  Since we can't rely on the enclosing room's
     *   status line name if we can't see the enclosing room, we must
     *   provide one of our own.
     *   
     *   By default, we'll use our regular name.
     */
    roomName = (name)

    /*
     *   Show our interior description.  We use this to generate the long
     *   "look" description for the room when an actor is within this
     *   object and cannot see the enclosing room.
     *   
     *   Note that this is used ONLY when the actor cannot see the
     *   enclosing room - when the enclosing room is visible (because the
     *   nested room is something like a chair that doesn't enclose the
     *   actor, or can enclose the actor but is open or transparent), then
     *   we'll simply use the description of the enclosing room instead,
     *   adding a note to the short name shown at the start of the room
     *   description indicating that the actor is in the nested room.
     *   
     *   By default, we'll show the appropriate "actor here" description
     *   for the posture, so we'll say something like "You are sitting on
     *   the red chair" or "You are in the phone booth."  Instances can
     *   override this to customize the description with something more
     *   detailed, if desired.  
     */
    roomDesc { gActor.listActorPosture(getPOVActorDefault(gActor)); }

    /*
     *   Show our first-time room description.  This is the description we
     *   show when the actor is seeing our interior description for the
     *   first time.  By default, we just show the ordinary room
     *   description, but this can be overridden to provide a special
     *   description the first time the actor sees the room. 
     */
    roomFirstDesc { roomDesc; }

    /*
     *   Show our remote viewing description.  This is used when we show a
     *   description of a room, via lookAroundWithin, and the actor who's
     *   viewing the room is remote.  Note that the library never does this
     *   itself, since there's no command in the basic library that lets a
     *   player view a remote room.  However, a game might want to generate
     *   such a description to handle a command like LOOK THROUGH KEYHOLE,
     *   so we provide this extra description method for the game's use.
     *   
     *   By default, we simply show the normal room description.  You'll
     *   probably want to override this any time you actually use it,
     *   though, to describe what the actor sees from the remote point of
     *   view.  
     */
    roomRemoteDesc(actor) { roomDesc; }

    /* our interior room name when we're in the dark */
    roomDarkName = (gLibMessages.roomDarkName)

    /* show our interior description in the dark */
    roomDarkDesc { gLibMessages.roomDarkDesc; }

    /*
     *   Describe an actor in this location either from the point of view
     *   of a separate top-level room, or at a distance.  This is called
     *   when we're showing a room description (for LOOK AROUND), and the
     *   given actor is in a remote room or at a distance visually from
     *   the actor doing the looking, and the given actor is contained
     *   within this room.
     *   
     *   By default, if we have a location, and the actor doing the
     *   looking can see out into the enclosing room, we'll defer to the
     *   location.  Otherwise, we'll show a default library message ("The
     *   actor is nearby").  
     */
    roomActorThereDesc(actor)
    {
        local pov = getPOV();
        
        /* 
         *   if we have an enclosing location, and the POV actor can see
         *   out to our location, defer to our location; otherwise, show
         *   the default library message 
         */
        if (location != nil && pov != nil && pov.canSee(location))
            location.roomActorThereDesc(actor);
        else
            gLibMessages.actorInRemoteRoom(actor, self, pov);
    }
    
    /*
     *   Look around from the point of view of this object and on behalf of
     *   the given actor.  This can be used to generate a description as
     *   seen from this object by the given actor, and is suitable for
     *   cases where the actor can use this object as a sensing device.  We
     *   simply pass this through to lookAroundPov(), passing 'self' as the
     *   point-of-view object.
     *   
     *   'verbose' is a combination of LookXxx flags (defined in adv3.h)
     *   indicating what style of description we want.  This can also be
     *   'true', in which case we'll show the standard verbose listing
     *   (LookRoomName | LookRoomDesc | LookListSpecials |
     *   LookListPortables); or 'nil', in which case we'll use the standard
     *   terse listing (LookRoomName | LookListSpecials |
     *   LookListPortables).  
     */
    lookAround(actor, verbose)
    {
        /* look around from my point of view */
        lookAroundPov(actor, self, verbose);
    }

    /*
     *   Look around from within this object, looking from the given point
     *   of view and on behalf of the given actor.
     *   
     *   'actor' is the actor doing the looking, and 'pov' is the point of
     *   view of the description.  These are usually the same, but need not
     *   be.  For example, an actor could be looking at a room through a
     *   hole in the wall, in which case the POV would be the object
     *   representing the "side" of the hole in the room being described.
     *   Or, the actor could be observing a remote room via a
     *   closed-circuit television system, in which case the POV would be
     *   the camera in the remote room.  (The POV is the physical object
     *   receiving the photons in the location being described, so it's the
     *   camera, not the TV monitor that the actor's looking at.)
     *   
     *   'verbose' has the same meaning as it does in lookAround().  
     *   
     *   This routine checks to see if 'self' is the "look-around ceiling,"
     *   which is for most purposes the outermost visible container of this
     *   object; see isLookAroundCeiling() for more.  If this object is the
     *   look-around ceiling, then we'll call lookAroundWithin() to
     *   generate the description of the interior of 'self'; otherwise,
     *   we'll recursively defer to our immediate container so that it can
     *   make the same test.  In most cases, the outermost visible
     *   container that actually generates the description will be a Room
     *   or a NestedRoom.  
     */
    lookAroundPov(actor, pov, verbose)
    {
        /*
         *   If we're the "look around ceiling," generate the description
         *   within this room.  Otherwise, we have an enclosing location
         *   that provides the interior description. 
         */
        if (isLookAroundCeiling(actor, pov))
        {
            /* we're the "ceiling," so describe our interior */
            fromPOV(actor, pov, &lookAroundWithin, actor, pov, verbose);
        }
        else
        {
            /* our location provides the interior description */
            location.lookAroundPov(actor, pov, verbose);
        }
    }

    /*
     *   Are we the "look around ceiling"?  If so, it means that a LOOK
     *   AROUND for an actor within this location (or from a point of view
     *   within this location) will use our own interior description, via
     *   lookAroundWithin().  If we're not the ceiling, then we defer to
     *   our location, letting it describe its interior.
     *   
     *   By default, we're the ceiling if we're a top-level room (that is,
     *   we have no enclosing location), OR it's not possible to see out to
     *   our location.  In either of these cases, we can't see anything
     *   outside of this room, so we have to generate our own interior
     *   description.  However, if we do have a location that we can see,
     *   then we'll assume that this object just represents a facet of its
     *   enclosing location, so the enclosing location provides the room
     *   interior description.
     *   
     *   In some cases, a room might want to provide its own LOOK AROUND
     *   interior description directly even if its location is visible.
     *   For example, if the player's inside a wooden booth with a small
     *   window that can see out to the enclosing location, LOOK AROUND
     *   should probably describe the interior of the booth rather than the
     *   enclosing location: even though the exterior is technically
     *   visible, the booth clearly dominates the view, from the player's
     *   perspective.  In this case, we'd want to override this routine to
     *   indicate that we're the LOOK AROUND ceiling, despite our
     *   location's visibility.  
     */
    isLookAroundCeiling(actor, pov)
    {
        /* 
         *   if we don't have a location, or we can't see our location,
         *   then we have to provide our own description, so we're the LOOK
         *   AROUND "ceiling" 
         */
        return (location == nil || !actor.canSee(location));
    }

    /*
     *   Provide a "room description" of the interior of this object.  This
     *   routine is primarily intended as a subroutine of lookAround() and
     *   lookAroundPov() - most game code shouldn't need to call this
     *   routine directly.  Note that if you *do* call this routine
     *   directly, you *must* set the global point-of-view variables, which
     *   you can do most easily by calling this routine indirectly through
     *   the fromPOV() method.
     *   
     *   The library calls this method when an actor performs a "look
     *   around" command, and the actor is within this object, and the
     *   actor can't see anything outside of this object; this can happen
     *   simply because we're a top-level room, but it can also happen when
     *   we're a closed opaque container or there's not enough light to see
     *   the enclosing location.
     *   
     *   The parameters have the same meaning as they do in
     *   lookAroundPov().
     *   
     *   Note that this method must be overridden if a room overrides the
     *   standard mechanism for representing its contents list (i.e., it
     *   doesn't store its complete set of direct contents in its
     *   'contents' list property).  
     *   
     *   In most cases, this routine will only be called in Room and
     *   NestedRoom objects, because actors can normally only enter those
     *   types of objects.  However, it is possible to try to describe the
     *   interior of other types of objects if (1) the game allows actors
     *   to enter other types of objects, or (2) the game provides a
     *   non-actor point-of-view object, such as a video camera, that can
     *   be placed in ordinary containers and which transmit what they see
     *   for remote viewing.  
     */
    lookAroundWithin(actor, pov, verbose)
    {
        local illum;
        local infoTab;
        local info;
        local specialList;
        local specialBefore, specialAfter;

        /* check for the special 'true' and 'nil' settings for 'verbose' */
        if (verbose == true)
        {
            /* true -> show the standard verbose description */
            verbose = (LookRoomName | LookRoomDesc
                       | LookListSpecials | LookListPortables);
        }
        else if (verbose == nil)
        {
            /* nil -> show the standard terse description */
            verbose = (LookRoomName | LookListSpecials | LookListPortables);
        }

        /* 
         *   if we've never seen the room before, include the room
         *   description, even if it wasn't specifically requested 
         */
        if (!actor.hasSeen(self))
            verbose |= LookRoomDesc;

        /* 
         *   get a table of all of the objects that the viewer can sense
         *   in the location using sight-like senses 
         */
        infoTab = actor.visibleInfoTableFromPov(pov);

        /* get the ambient illumination at the point of view */
        info = infoTab[pov];
        if (info != nil)
        {
            /* get the ambient illumination from the info list item */
            illum = info.ambient;
        }
        else
        {
            /* the actor's not in the list, so it must be completely dark */
            illum = 0;
        }

        /* 
         *   adjust the list of described items (usually, this removes the
         *   point-of-view object from the list, to ensure that we don't
         *   list it among the room's contents) 
         */
        adjustLookAroundTable(infoTab, pov, actor);
        
        /* if desired, display the room name */
        if ((verbose & LookRoomName) != 0)
        {
            "<.roomname>";
            lookAroundWithinName(actor, illum);
            "<./roomname>";
        }

        /* if we're in verbose mode, display the full description */
        if ((verbose & LookRoomDesc) != 0)
        {
            /* display the full room description */
            "<.roomdesc>";
            lookAroundWithinDesc(actor, illum);
            "<./roomdesc>";
        }

        /* show the initial special descriptions, if desired */
        if ((verbose & LookListSpecials) != 0)
        {
            local plst;

            /* 
             *   Display any special description messages for visible
             *   objects, other than those carried by the actor.  These
             *   messages are part of the verbose description rather than
             *   the portable item listing, because these messages are
             *   meant to look like part of the room's full description
             *   and thus should not be included in a non-verbose listing.
             *   
             *   Note that we only want to show objects that are currently
             *   using special descriptions AND which aren't contained in
             *   the actor doing the looking, so subset the list
             *   accordingly.  
             */
            specialList = specialDescList(
                infoTab,
                {obj: obj.useSpecialDescInRoom(self) && !obj.isIn(actor)});

            /* 
             *   partition the special list into the parts to be shown
             *   before and after the portable contents list 
             */
            plst = partitionList(specialList,
                                 {obj: obj.specialDescBeforeContents});
            specialBefore = plst[1];
            specialAfter = plst[2];

            /* 
             *   at this point, describe only the items that are to be
             *   listed specially before the room's portable contents list 
             */
            specialContentsLister.showList(pov, nil, specialBefore,
                                           0, 0, infoTab, nil);
        }

        /* show the portable items, if desired */
        if ((verbose & LookListPortables) != 0)
        {
            /* 
             *   Describe each visible object directly contained in the
             *   room that wants to be listed - generally, we list any
             *   mobile objects that don't have special descriptions.  We
             *   list items in all room descriptions, verbose or not,
             *   because the portable item list is more of a status display
             *   than it is a part of the full description.
             *   
             *   Note that the infoTab has sense data that will ensure that
             *   we don't show anything we shouldn't if the room is dark.  
             */
            lookAroundWithinContents(actor, illum, infoTab);
        }

        /*
         *   If we're showing special descriptions, show the special descs
         *   for any items that want to be listed after the room's portable
         *   contents.  
         */
        if ((verbose & LookListSpecials) != 0)
        {
            /* show the subset that's listed after the room contents */
            specialContentsLister.showList(pov, nil, specialAfter,
                                           0, 0, infoTab, nil);
        }
        
        /* show all of the sounds we can hear */
        lookAroundWithinSense(actor, pov, sound, roomListenLister);

        /* show all of the odors we can smell */
        lookAroundWithinSense(actor, pov, smell, roomSmellLister);

        /* show the exits, if desired */
        lookAroundWithinShowExits(actor, illum);

        /* 
         *   If we're not in the dark, note that we've been seen.  Don't
         *   count this as having seen the location if we're in the dark,
         *   since we won't normally describe any detail in the dark.  
         */
        if (illum > 1)
            actor.setHasSeen(self);
    }

    /*
     *   Display the "status line" name of the room.  This is normally a
     *   brief, single-line description.
     *   
     *   By long-standing convention, each location in a game usually has a
     *   distinctive name that's displayed here.  Players usually find
     *   these names helpful in forming a mental map of the game.
     *   
     *   By default, if we have an enclosing location, and the actor can
     *   see the enclosing location, we'll defer to the location.
     *   Otherwise, we'll display our roo interior name.  
     */
    statusName(actor)
    {
        /* 
         *   use the enclosing location's status name if there is an
         *   enclosing location and its visible; otherwise, show our
         *   interior room name 
         */
        if (location != nil && actor.canSee(location))
            location.statusName(actor);
        else
            lookAroundWithinName(actor, actor.getVisualAmbient());
    }

    /*
     *   Adjust the sense table a "look around" command.  This is called
     *   after we calculate the sense info table, but before we start
     *   describing any of the room's contents, to give us a chance to
     *   remove any items we don't want described (or, conceivably, to add
     *   any items we do want described but which won't show up with the
     *   normal sense calculations).
     *   
     *   By default, we simply remove the point-of-view object from the
     *   list, to ensure that it's not included among the objects mentioned
     *   as being in the room.  We don't want to mention the point of view
     *   because the POV object because a POV isn't normally in its own
     *   field of view.  
     */
    adjustLookAroundTable(tab, pov, actor)
    {
        /* remove the POV object from the table */
        tab.removeElement(pov);

        /* give the actor a chance to adjust the table as well */
        if (self not in (actor, pov))
        {
            /* let the POV object adjust the table */
            pov.adjustLookAroundTable(tab, pov, actor);

            /* if the actor differs from the POV, let it adjust the table */
            if (actor != pov)
                actor.adjustLookAroundTable(tab, pov, actor);
        }
    }

    /*
     *   Show my name for a "look around" command.  This is used to display
     *   the location name when we're providing the room description.
     *   'illum' is the ambient visual sense level at the point of view.
     *   
     *   By default, we show our interior room name or interior dark room
     *   name, as appropriate to the ambient light level at the point of
     *   view.  
     */
    lookAroundWithinName(actor, illum)
    {
        /* 
         *   if there's light, show our name; otherwise, show our
         *   in-the-dark name 
         */
        if (illum > 1)
        {
            /* we can see, so show our interior description */
            "\^<<roomName>>";
        }
        else
        {
            /* we're in the dark, so show our default dark name */
            say(roomDarkName);
        }

        /* show the actor's posture as part of the description */
        actor.actorRoomNameStatus(self);
    }

    /*
     *   Show our "look around" long description.  This is used to display
     *   the location's full description when we're providing the room
     *   description - that is, when the actor doing the looking is inside
     *   me.  This displays only the room-specific portion of the room's
     *   description; it does not display the status line or anything
     *   about the room's dynamic contents.  
     */
    lookAroundWithinDesc(actor, illum)
    {
        /* 
         *   check for illumination - we must have at least dim ambient
         *   lighting (level 2) to see the room's long description 
         */
        if (illum > 1)
        {
            local pov = getPOVDefault(actor);
            
            /* 
             *   Display the normal description of the room - use the
             *   roomRemoteDesc if the actor isn't in the same room OR the
             *   point of view isn't the same as the actor; use the
             *   firstDesc if this is the first time in the room; and
             *   otherwise the basic roomDesc.  
             */
            if (!actor.isIn(self) || actor != pov)
            {
                /* we're viewing the room remotely */
                roomRemoteDesc(actor);
            }
            else if (actor.hasSeen(self))
            {
                /* we've seen it already - show the basic room description */
                roomDesc;
            }
            else
            {
                /* 
                 *   we've never seen this location before - show the
                 *   first-time description 
                 */
                roomFirstDesc;
            }
        }
        else
        {
            /* display the in-the-dark description of the room */
            roomDarkDesc;
        }
    }

    /*
     *   Show my room contents for a "look around" description within this
     *   object. 
     */
    lookAroundWithinContents(actor, illum, infoTab)
    {
        local lst;
        local lister;
        local remoteLst;
        local outer;
        local recurse;

        /* get our outermost enclosing room */
        outer = getOutermostRoom();

        /* mark everything visible from the room as having been seen */
        setAllSeenBy(infoTab, actor);
        
        /* 
         *   if the illumination is less than 'dim' (level 2), display
         *   only self-illuminating items 
         */
        if (illum != nil && illum < 2)
        {
            /* 
             *   We're in the dark - list only those objects that the actor
             *   can sense via the sight-like senses, which will be the
             *   list of self-illuminating objects.  Don't include items
             *   being carried by the actor, though, since those don't
             *   count as part of the actor's surroundings.  (To produce
             *   this list, simply make a list consisting of all of the
             *   objects in the sense info table that aren't contained in
             *   the actor; since the table naturally includes only objects
             *   that are illuminated, the entire contents of the table
             *   will give us the visible objects.)  
             */
            lst = senseInfoTableSubset(
                infoTab, {obj, info: !obj.isIn(actor)});
            
            /* use my dark contents lister */
            lister = darkRoomContentsLister;

            /* there are no remote-room lists to generate */
            remoteLst = nil;

            /* 
             *   Since we can't see what we're looking at, we can't see the
             *   positional relationships among the things we're looking
             *   at, so we can't show the contents tree recursively.  We
             *   just want to show our flat list of what's visible on
             *   account of self-illumination.  
             */
            recurse = nil;
        }
        else
        {
            /* start with my contents list */
            lst = contents;

            /* always remove the actor from the list */
            lst -= actor;

            /* 
             *   Use the normal (lighted) lister.  Note that if the actor
             *   isn't in the same room, or the point of view isn't the
             *   same as the actor, we want to generate remote
             *   descriptions, so use the remote contents lister in these
             *   cases.  
             */
            lister = (actor.isIn(self) && actor == getPOVDefault(actor)
                      ? roomContentsLister
                      : remoteRoomContentsLister(self));

            /*
             *   Generate a list of all of the *other* top-level rooms
             *   with contents visible from here.  To do this, build a
             *   list of all of the unique top-level rooms, other than our
             *   own top-level room, that contain items in the visible
             *   information table.  
             */
            remoteLst = new Vector(5);
            infoTab.forEachAssoc(new function(obj, info)
            {
                /* if this object isn't in our top-level room, note it */
                if (obj != outer && !obj.isIn(outer))
                {
                    local objOuter;
                    
                    /* 
                     *   it's not in our top-level room, so find its
                     *   top-level room 
                     */
                    objOuter = obj.getOutermostRoom();

                    /* if this one isn't in our list yet, add it */
                    if (remoteLst.indexOf(objOuter) == nil)
                        remoteLst.append(objOuter);
                }
            });

            /* 
             *   since we can see what we're looking at, show the
             *   relationships among the contents by listing recursively 
             */
            recurse = true;
        }

        /* add a paragraph before the room's contents */
        "<.p>";

        /* show the contents */
        lister.showList(actor, self, lst, (recurse ? ListRecurse : 0),
                        0, infoTab, nil);

        /* 
         *   if we can see anything in remote top-level rooms, show the
         *   contents of each other room with visible contents 
         */
        if (remoteLst != nil)
        {
            /* show each remote room's contents */
            for (local i = 1, local len = remoteLst.length() ; i <= len ; ++i)
            {
                local cur;
                local cont;

                /* get this item */
                cur = remoteLst[i];

                /* start with the direct contents of this remote room */
                cont = cur.contents;

                /* 
                 *   Remove any objects that are also contents of the
                 *   local room or of any other room we've already listed.
                 *   It's possible that we have a MultiLoc that's in one
                 *   or more of the visible top-level locations, so we
                 *   need to make sure we only list each one once. 
                 */
                cont = cont.subset({x: !x.isIn(outer)});
                for (local j = 1 ; j < i ; ++j)
                    cont = cont.subset({x: !x.isIn(remoteLst[j])});
                
                /* 
                 *   list this remote room's contents using our remote
                 *   lister for this remote room 
                 */
                outer.remoteRoomContentsLister(cur).showList(
                    actor, cur, cont, ListRecurse, 0, infoTab, nil);
            }
        }
    }

    /*
     *   Add to the room description a list of things we notice through a
     *   specific sense.  This is used to add things we can hear and smell
     *   to a room description.
     */
    lookAroundWithinSense(actor, pov, sense, lister)
    {
        local infoTab;
        local presenceList;

        /* 
         *   get the information table for the desired sense from the
         *   given point of view 
         */
        infoTab = pov.senseInfoTable(sense);

        /* 
         *   Get the list of everything with a presence in this sense.
         *   Include only items that aren't part of the actor's inventory,
         *   since inventory items aren't part of the room and thus
         *   shouldn't be described as part of the room.  
         */
        presenceList = senseInfoTableSubset(infoTab,
            {obj, info: obj.(sense.presenceProp) && !obj.isIn(actor)});

        /* mark each item in the presence list as known */
        foreach (local cur in presenceList)
            actor.setKnowsAbout(cur);

        /* list the items */
        lister.showList(pov, nil, presenceList, 0, 0, infoTab, nil);
    }

    /*
     *   Show the exits from this room as part of a description of the
     *   room, if applicable.  By default, if we have an exit lister
     *   object, we'll invoke it to show the exits.  
     */
    lookAroundWithinShowExits(actor, illum)
    {
        /* if we have an exit lister, have it show a list of exits */
        if (gExitLister != nil)
            gExitLister.lookAroundShowExits(actor, self, illum);
    }

    /* 
     *   Get my lighted room contents lister - this is the Lister object
     *   that we use to display the room's contents when the room is lit.
     *   We'll return the default library room lister.  
     */
    roomContentsLister { return roomLister; }

    /*
     *   Get my lister for the contents of the given remote room.  A
     *   remote room is a separate top-level room that an actor can see
     *   from its current location; for example, if two top-level rooms
     *   are connected by a window, so that an actor standing in one room
     *   can see into the other room, then the other room is the remote
     *   room, from the actor's perspective.
     *   
     *   This is called on the actor's outermost room to get the lister
     *   for objects visible in the given remote room.  This lets each
     *   room customize the way it describes the objects in adjoining
     *   rooms as seen from its point of view.
     *   
     *   We provide a simple default lister that yields decent results in
     *   most cases.  However, it's often desirable to customize this, to
     *   be more specific about how we see the items: "Through the window,
     *   you see..." or "Further down the hall, you see...".  An easy way
     *   to provide such custom listings is to return a new
     *   CustomRoomLister, supplying the prefix and suffix strings as
     *   parameters:
     *   
     *.    return new CustomRoomLister(
     *.       'Further down the hall, {you/he} see{s}', '.');
     */
    remoteRoomContentsLister(other) { return new RemoteRoomLister(other); }

    /* 
     *   Get my dark room contents lister - this is the Lister object we'll
     *   use to display the room's self-illuminating contents when the room
     *   is dark.  
     */
    darkRoomContentsLister { return darkRoomLister; }

    /*
     *   Get the outermost containing room.  We return our container, if
     *   we have one, or self if not.  
     */
    getOutermostRoom()
    {
        /* return our container's outermost room, if we have one */
        return (location != nil ? location.getOutermostRoom() : self);
    }

    /* get the outermost room that's visible from the given point of view */
    getOutermostVisibleRoom(pov)
    {
        local enc;
        
        /* 
         *   Get our outermost enclosing visible room - this is the
         *   outermost enclosing room that the POV can see.  If we manage
         *   to find an enclosing visible room, return that, since it's
         *   "more outer" than we are.  
         */
        if (location != nil
            && (enc = location.getOutermostVisibleRoom(pov)) != nil)
            return enc;

        /* 
         *   We either don't have any enclosing rooms, or none of them are
         *   visible.  So, our outermost enclosing room is one of two
         *   things: if we're visible, it's us; if we're not even visible
         *   ourselves, there's no visible room at all, so return nil.  
         */
        return (pov.canSee(self) ? self : nil);
    }

    /*
     *   Get the location traveler - this is the object that's actually
     *   going to change location when a traveler within this location
     *   performs a travel command to travel via the given connector.  By
     *   default, we'll indicate what our containing room indicates.  (The
     *   enclosing room might be a vehicle or an ordinary room; in any
     *   case, it'll know what to do, so we merely have to ask it.)
     *   
     *   We defer to our enclosing room by default because this allows for
     *   things like a seat in a car: the actor is sitting in the seat and
     *   starts traveling in the car, so the seat calls the enclosing room,
     *   which is the car, and the car returns itself, since it's the car
     *   that will be traveling.  
     */
    getLocTraveler(trav, conn)
    {
        /* 
         *   if we have a location, defer to it; otherwise, just return the
         *   proposed traveler 
         */
        return (location != nil ? location.getLocTraveler(trav, conn) : trav);
    }

    /*
     *   Get the "location push traveler" - this is the object that's going
     *   to travel for a push-travel action performed by a traveler within
     *   this location.  This is called by a traveler within this location
     *   to find out if the location wants to be involved in the travel, as
     *   a vehicle might be.  By default, we let our location handle it, if
     *   we have one.  
     */
    getLocPushTraveler(trav, obj)
    {
        /* 
         *   defer to our location if we have one, otherwise just use the
         *   proposed traveler 
         */
        return (location != nil
                ? location.getLocPushTraveler(trav, obj)
                : trav);
    }

    /*
     *   Get the travel preconditions for an actor in this location.  For
     *   ordinary objects, we'll just defer to our container, if we have
     *   one.  
     */
    roomTravelPreCond()
    {
        /* if we have a location, defer to it */
        if (location != nil)
            return location.roomTravelPreCond();

        /* 
         *   we have no location, and we impose no conditions of our by
         *   default, so simply return an empty list 
         */
        return [];
    }

    /*
     *   Determine if the current gActor, who is directly in this location,
     *   is "travel ready."  This means that the actor is ready, as far as
     *   this location is concerned, to traverse the given connector.  By
     *   default, we'll defer to our location.
     *   
     *   Note that if you override this to return nil, you should also
     *   provide a custom 'notTravelReadyMsg' property that explains the
     *   objection.  
     */
    isActorTravelReady(conn)
    {
        /* if we have a location, defer to it */
        if (location != nil)
            return location.isActorTravelReady(conn);

        /* 
         *   we have no location, and we impose no conditions of our own by
         *   default, so we have no objection
         */
        return true;
    }

    /* explain the reason isActorTravelReady() returned nil */
    notTravelReadyMsg = (location != nil ? location.notTravelReadyMsg : nil)

    /* show a list of exits as part of failed travel - defer to location */
    cannotGoShowExits(actor)
    {
        if (location != nil)
            location.cannotGoShowExits(actor);
    }

    /* show exits in the statusline - defer to our location */
    showStatuslineExits()
    {
        if (location != nil)
            location.showStatuslineExits();
    }

    /* get the status line exit lister height - defer to our location */
    getStatuslineExitsHeight()
        { return location != nil ? location.getStatuslineExitsHeight() : 0; }

    /*
     *   Get the travel connector from this location in the given
     *   direction.
     *   
     *   Map all of the directional connections to their values in the
     *   enclosing location, except for any explicitly defined in this
     *   object.  (In most cases, ordinary objects won't define any
     *   directional connectors directly, since those connections usually
     *   apply only to top-level rooms.)
     *   
     *   If 'actor' is non-nil, we'll limit our search to enclosing
     *   locations that the actor can currently see.  If 'actor' is nil,
     *   we'll consider any connector in any enclosing location, whether
     *   the actor can see the enclosing location or not.  It's useful to
     *   pass in a nil actor in cases where we're interested in the
     *   structure of the map and not actual travel, such as when
     *   constructing an automatic map or computing possible courses
     *   between locations.  
     */
    getTravelConnector(dir, actor)
    {
        local conn;
        
        /* 
         *   if we explicitly define the link property, and the result is a
         *   non-nil value, use that definition 
         */
        if (propDefined(dir.dirProp) && (conn = self.(dir.dirProp)) != nil)
            return conn;

        /* 
         *   We don't define the direction link explicitly.  If the actor
         *   can see our container, or there's no actor involved, retrieve
         *   the link from our container; otherwise, stop here, and simply
         *   return the default link for the direction.  
         */
        if (location != nil && (actor == nil || actor.canSee(location)))
        {
            /* 
             *   the actor can see out to our location (or there's no actor
             *   involved at all), so get the connector for the location 
             */
            return location.getTravelConnector(dir, actor);
        }
        else
        {
            /* 
             *   the actor can't see our location, so there's no way to
             *   travel this way - return the default connector for the
             *   direction 
             */
            return dir.defaultConnector(self);
        }
    }

    /*
     *   Search our direction list for a connector that will lead the
     *   given actor to the given destination.  
     */
    getConnectorTo(actor, dest)
    {
        local conn;
        
        /* search all of our directions */
        foreach (local dir in Direction.allDirections)
        {
            /* get the connector for this direction */
            conn = getTravelConnector(dir, actor);

            /* ask the connector for a connector to the given destination */
            if (conn != nil)
                conn = conn.connectorGetConnectorTo(self, actor, dest);

            /* if we found a valid connector, return it */
            if (conn != nil)
                return conn;
        }

        /* didn't find a match */
        return nil;
    }

    /*
     *   Find a direction linked to a given connector, for travel by the
     *   given actor.  Returns the first direction (as a Direction object)
     *   we find linked to the connector, or nil if we don't find any
     *   direction linked to it.  
     */
    directionForConnector(conn, actor)
    {
        /* search all known directions */
        foreach (local dir in Direction.allDirections)
        {
            /* if this direction is linked to the connector, return it */
            if (self.getTravelConnector(dir, actor) == conn)
                return dir;
        }

        /* we didn't find any directions linked to the connector */
        return nil;
    }

    /*
     *   Find the "local" direction link from this object to the given
     *   travel connector.  We'll only consider our own local direction
     *   links; we won't consider enclosing objects.  
     */
    localDirectionLinkForConnector(conn)
    {
        /* scan all direction links */
        foreach (local dir in Direction.allDirections)
        {
            /* 
             *   if we define this direction property to point to this
             *   connector, we've found our required location 
             */
            if (self.(dir.dirProp) == conn)
                return dir;
        }

        /* we didn't find a direction linked to the given connector */
        return nil;
    }

    /*
     *   Check that a traveler is directly in this room.  By default, a
     *   Thing is not a room, so a connector within a Thing actually
     *   requires the traveler to be in the Thing's container, not in the
     *   Thing itself.  So, defer to our container.  
     */
    checkTravelerDirectlyInRoom(traveler, allowImplicit)
    {
        /* defer to our location */
        return location.checkTravelerDirectlyInRoom(traveler, allowImplicit);
    }

    /*
     *   Check, using pre-condition rules, that the actor is removed from
     *   this nested location and moved to its exit destination.
     *   
     *   This is called when the actor is attempting to move to an object
     *   outside of this object while the actor is within this object (for
     *   example, if we have a platform containing a box containing a
     *   chair, 'self' is the box, and the actor is in the chair, an
     *   attempt to move to the platform will trigger a call to
     *   box.checkActorOutOfNested).
     *   
     *   By default, we're not a nested location, but we could *contain*
     *   nested locations.  So what we need to do is defer to the child
     *   object that actually contains the actor, to let it figure out what
     *   it means to move the actor out of itself.  
     */
    checkActorOutOfNested(allowImplicit)
    {
        /* find the child containing the actor, and ask it to do the work */
        foreach (local cur in contents)
        {
            if (gActor.isIn(cur))
                return cur.checkActorOutOfNested(allowImplicit);
        }

        /* didn't find a child containing the actor; just fail */
        reportFailure(&cannotDoFromHereMsg);
        exit;
    }

    /*
     *   Get my "room location."  If 'self' is capable of holding actors,
     *   this should return 'self'; otherwise, it should return the
     *   nearest enclosing container that's a room location.  By default,
     *   we simply return our location's room location.  
     */
    roomLocation = (location != nil ? location.roomLocation : nil)

    /*
     *   Get the apparent location of one of our room parts (the floor,
     *   the ceiling, etc).  By default, we'll simply ask our container
     *   about it, since a nested room by default doesn't have any of the
     *   standard room parts.  
     */
    getRoomPartLocation(part)
    {
        if (location != nil)
            return location.getRoomPartLocation(part);
        else
            return nil;
    }

    /*
     *   By default, an object containing the player character will forward
     *   the roomDaemon() call up to the container. 
     */
    roomDaemon()
    {
        if (location != nil)
            location.roomDaemon();
    }

    /*
     *   Our room atmospheric message list.  This can be set to an
     *   EventList that provides messages to be displayed while the player
     *   character is within this object.
     *   
     *   By default, if our container is visible to us, we'll use our
     *   container's atmospheric messages.  This can be overridden to
     *   provide our own atmosphere list when the player character is in
     *   this object.  
     */
    atmosphereList()
    {
        if (location != nil && gPlayerChar.canSee(location))
            return location.atmosphereList;
        else
            return nil;
    }

    /* 
     *   Get the notification list actions performed by actors within this
     *   object.  Ordinary objects don't have room notification lists, but
     *   we might be part of a nested set of objects that includes rooms,
     *   so simply look to our container for the notification list.  
     */
    getRoomNotifyList()
    {
        local lst = [];
        
        /* get the notification lists for our immediate containers */
        forEachContainer(
            {cont: lst = lst.appendUnique(cont.getRoomNotifyList())});

        /* return the result */
        return lst;
    }

    /*
     *   Are we aboard a ship?  By default, we'll return our location's
     *   setting for this property; if we have no location, we'll return
     *   nil.  In general, this should be overridden in shipboard rooms to
     *   return true.
     *   
     *   The purpose of this property is to indicate to the travel system
     *   that shipboard directions (fore, aft, port, starboard) make sense
     *   here.  When this returns true, and an actor attempts travel in a
     *   shipboard direction that doesn't allow travel here, we'll use the
     *   standard noTravel message.  When this returns nil, attempting to
     *   move in a shipboard direction will show a different response that
     *   indicates that such directions are not meaningful when not aboard
     *   a ship of some kind.
     *   
     *   Note that we look to our location unconditionally - we don't
     *   bother to check to see if we're open, transparent, or anything
     *   like that, so we'll check with our location even if the actor
     *   can't see the location.  The idea is that, no matter how nested
     *   within opaque containers we are, we should still know that we're
     *   aboard a ship.  This might not always be appropriate; if the
     *   actor is magically transported into an opaque container that
     *   happens to be aboard a ship somewhere, the actor probably
     *   shouldn't think of the location as shipboard until escaping the
     *   container, unless they'd know because of the rocking of the ship
     *   or some other sensory factor that would come through the opaque
     *   container.  When the shipboard nature of an interior location
     *   should not be known to the actor, this should simply be
     *   overridden to return nil.  
     */
    isShipboard()
    {
        /* if we have a location, use its setting; otherwise return nil */
        return (location != nil ? location.isShipboard() : nil);
    }

    /* 
     *   When an actor takes me, the actor will assign me a holding index
     *   value, which is simply a serial number indicating the order in
     *   which I was picked up.  This lets the actor determine which items
     *   have been held longest: the item with the lowest holding index
     *   has been held the longest.  
     */
    holdingIndex = 0

    /*
     *   An object has "weight" and "bulk" specifying how heavy and how
     *   large the object is.  These are in arbitrary units, and by
     *   default everything has a weight of 1 and a bulk of 1.
     */
    weight = 1
    bulk = 1

    /*
     *   Calculate the bulk contained within this object.  By default,
     *   we'll simply add up the bulks of all of our contents.
     */
    getBulkWithin()
    {
        local total;
        
        /* add up the bulks of our contents */
        total = 0;
        foreach (local cur in contents)
            total += cur.getBulk();
        
        /* return the total */
        return total;
    }

    /* 
     *   get my "destination name," for travel purposes; by default, we
     *   simply defer to our location, if we have one 
     */
    getDestName(actor, origin)
    {
        if (location != nil)
            return location.getDestName(actor, origin);
        else
            return '';
    }
    
    /*
     *   Get the effective location of an actor directly within me, for the
     *   purposes of a "follow" command.  To follow someone, we must have
     *   the same effective follow location that the target had when we
     *   last observed the target leaving.
     *   
     *   For most objects, we simply defer to the location.  
     */
    effectiveFollowLocation = (location != nil
                               ? location.effectiveFollowLocation
                               : self)

    /*
     *   Get the destination for an object dropped within me.  Ordinary
     *   objects can't contain actors, so an actor can't directly drop
     *   something within a regular Thing, but the same effect could occur
     *   if an actor throws a projectile, since the projectile might reach
     *   either the target or an intermediate obstruction, then bounce off
     *   and land in whatever contains the object hit.
     *   
     *   By default, objects dropped within us won't stay within us, but
     *   will land in our container's drop destination.
     *   
     *   'obj' is the object being dropped.  In some cases, the drop
     *   destination might differ according to the object; for example, if
     *   the floor is a metal grating, a large object might land on the
     *   grating when dropped, but a small object such as a coin might drop
     *   through the grating to the room below.  Note that 'obj' can be
     *   nil, if the caller simply wants to determine the generic
     *   destination where a *typical* object would land if dropped - this
     *   routine must therefore not assume that 'obj' is non-nil.
     *   
     *   'path' is the sense path (as returned by selectPathTo and the
     *   like) traversed by the operation that's seeking the drop
     *   destination.  For ordinary single-location objects, the path is
     *   irrelevant, but this information is sometimes useful for
     *   multi-location objects to let them know which "side" we approached
     *   them from.  Note that the path can be nil, so this routine must
     *   not depend upon a path being supplied; if the path is nil, the
     *   routine should assume that the ordinary "touch" sense path from
     *   the current actor to 'self' is to be used, because the actor is
     *   effectively reaching out and placing an object on self.  This
     *   means that when calling this routine, you can supply a nil path
     *   any time the actor is simply dropping an object.  
     */
    getDropDestination(obj, path)
    {
        /* 
         *   by default, the object lands in our container's drop
         *   destination; if we don't have a container, drop it here 
         */
        return location != nil
            ? location.getDropDestination(obj, path)
            : self;
    }

    /*
     *   Adjust a drop destination chosen in a THROW.  This is called on
     *   the object that was chosen as the preliminary landing location for
     *   the thrown object, as calculated by getHitFallDestination(), to
     *   give the preliminary destination a chance to redirect the landing
     *   to another object.  For example, if the preliminary destination
     *   simply isn't a suitable surface or container where a projectile
     *   could land, or it's not big enough to hold the projectile, the
     *   preliminary destination could override this method to redirect the
     *   landing to a more suitable object.
     *   
     *   By default, we don't need to make any adjustment, so we simply
     *   return 'self' to indicate that this can be used as the actual
     *   destination.  
     */
    adjustThrowDestination(thrownObj, path)
    {
        return self;
    }

    /*
     *   Receive a dropped object.  This is called when we are the actual
     *   (not nominal) drop destination for an object being dropped,
     *   thrown, or otherwise discarded.  This routine is responsible for
     *   carrying out the drop operation, and reporting the result of the
     *   command.
     *   
     *   'desc' is a "drop descriptor": an object of class DropType, which
     *   describes how the object is being discarded.  This can be a
     *   DropTypeDrop for a DROP command, a DropTypeThrow for a THROW
     *   command, or custom types defined by the game or by library
     *   extensions.
     *   
     *   In most cases, the actual *result* of dropping the object will
     *   always be the same for a given location, regardless of which
     *   command initiated the drop, so this routine won't usually have to
     *   look at 'desc' at all to figure out what happens.
     *   
     *   However, the *message* we generate does usually vary according to
     *   the drop type, because this is the report for the entire command.
     *   There are three main ways of handling this variation.
     *   
     *   - First, you can check what kind of descriptor it is (using
     *   ofKind, for example), and generate a custom message for each kind
     *   of descriptor.  Be aware that any library extensions you're using
     *   might have added new DropType subclasses.
     *   
     *   - Second, experienced programmers might prefer the arguably
     *   cleaner OO "visitor" pattern: treat 'desc' as a visitor, calling a
     *   single method that you define for your custom message.  You'll
     *   have to define that custom method in each DropType subclass, of
     *   course.
     *   
     *   - Third, you can build a custom message by combining the standard
     *   "message fragment" prefix provided by the drop descriptor with
     *   your own suffix.  The prefix will always be the start of a
     *   sentence describing what the player did: "You drop the box", "The
     *   ball hits the wall and bounces off", and so on.  Since the
     *   fragment always has the form of the start of a sentence, you can
     *   always add your own suffix: ", and falls to the floor", for
     *   example, or ", and falls into the chasm".  Note that if you're
     *   using one of the other approaches above, you'll probably want to
     *   combine that approach with this one to handle cases where you
     *   don't recognize the drop descriptor type.
     *   
     *   By default, we simply move the object into self and display the
     *   standard message using the descriptor (we use the "visitor"
     *   pattern to display that standard message).  This can be overridden
     *   in cases where it's necessary to move the object to a different
     *   location, or to invoke a side effect.  
     */
    receiveDrop(obj, desc)
    {
        /* simply move the object into self */
        obj.moveInto(self);

        /* generate the standard message */
        desc.standardReport(obj, self);
    }

    /*
     *   Get the nominal destination for an object dropped into this
     *   object.  This is used to report where an object ends up when the
     *   object is moved into me by some indirect route, such as throwing
     *   the object.
     *   
     *   By default, most objects simply return themselves, so we'll
     *   report something like "obj lands {in/on} self".  Some objects
     *   might want to report a different object as the destination; for
     *   example, an indoor room might want to report objects as falling
     *   onto the floor.  
     */
    getNominalDropDestination() { return self; }

    /*
     *   Announce myself as a default object for an action.  By default,
     *   we'll show the standard library message announcing a default.  
     */
    announceDefaultObject(whichObj, action, resolvedAllObjects)
    {
        /* use the standard library message for the announcement */
        return gLibMessages.announceDefaultObject(
            self, whichObj, action, resolvedAllObjects);
    }
    
    /*
     *   Calculate my total weight.  Weight is generally inclusive of the
     *   weights of contents or components, because anything inside an
     *   object normally contributes to the object's total weight. 
     */
    getWeight()
    {
        local total;

        /* start with my own intrinsic weight */
        total = weight;
                
        /* 
         *   add the weights of my direct contents, recursively
         *   calculating their total weights 
         */
        foreach (local cur in contents)
            total += cur.getWeight();

        /* return the total */
        return total;
    }

    /*
     *   Calculate my total bulk.  Most objects have a fixed external
     *   shape (and thus bulk) that doesn't vary as contents are added or
     *   removed, so our default implementation simply returns our
     *   intrinsic bulk without considering any contents.
     *   
     *   Some objects do change shape as contents are added.  Such objects
     *   can override this routine to provide the appropriate behavior.
     *   (We don't try to provide a parameterized total bulk calculator
     *   here because there are too many possible ways a container's bulk
     *   could be affected by its contents or by other factors.)
     *   
     *   If an object's bulk depends on its contents, the object should
     *   override notifyInsert() so that it calls checkBulkChange() - this
     *   will ensure that an object won't suddenly become too large for
     *   its container (or holding actor).  
     */
    getBulk()
    {
        /* by default, simply return my intrinsic bulk */
        return bulk;
    }

    /*
     *   Calculate the amount of bulk that this object contributes towards
     *   encumbering an actor directly holding the item.  By default, this
     *   simply returns my total bulk.
     *   
     *   Some types of objects will override this to cause the object to
     *   contribute more or less bulk to an actor holding the object.  For
     *   example, an article of clothing being worn by an actor typically
     *   contributes no bulk at all to the actor's encumbrance, because an
     *   actor wearing an item typically doesn't also have to hold on to
     *   the item.  Or, a small animal might encumber an actor more than
     *   its normal bulk because it's squirming around trying to get free.
     */
    getEncumberingBulk(actor)
    {
        /* by default, return our normal total bulk */
        return getBulk();
    }

    /*
     *   Calculate the amount of weight that this object contributes
     *   towards encumbering an actor directly holding the item.  By
     *   default, this simply returns my total weight.
     *   
     *   Note that we don't recursively sum the encumbering weights of our
     *   contents - we simply sum the actual weights.  We do it this way
     *   because items within a container aren't being directly held by
     *   the actor, so any difference between the encumbering and actual
     *   weights should not apply, even though the actor is indirectly
     *   holding the items.  If a subclass does want the sum of the
     *   encumbering weights, it should override this to make that
     *   calculation.  
     */
    getEncumberingWeight(actor)
    {
        /* by default, return our normal total weight */
        return getWeight();
    }

    /*
     *   "What if" test.  Make the given changes temporarily, bypassing
     *   any side effects that would normally be associated with the
     *   changes; invokes the given callback; then remove the changes.
     *   Returns the result of calling the callback function.
     *   
     *   The changes are expressed as pairs of argument values.  The first
     *   value in a pair is a property, and the second is a new value for
     *   the property.  For each pair, we'll set the given property to the
     *   given value.  The setting is direct - we don't invoke any method,
     *   because we don't want to cause any side effects at this point;
     *   we're interested only in what the world would look like if the
     *   given changes were to go into effect.
     *   
     *   A special property value of 'moveInto' can be used to indicate
     *   that the object should be moved into another object for the test.
     *   In this case, the second element of the pair is not a value to be
     *   stored into the moveInto property, but instead the value is a new
     *   location for the object.  We'll call the baseMoveInto method to
     *   move the object to the given new location.
     *   
     *   In any case, after making the changes, we'll invoke the given
     *   callback function, which we'll call with no arguments.
     *   
     *   Finally, on our way out, we'll restore the properties we changed
     *   to their original values.  We once again do this without any side
     *   effects.  Note that we restore the old values even if we exit
     *   with an exception.  
     */
    whatIf(func, [changes])
    {
        local oldList;
        local cnt;
        local i;
        
        /* 
         *   Allocate a vector with one slot per change, so that we have a
         *   place to store the old values of the properties we're
         *   changing.  Note that the argument list has two entries (prop,
         *   value) for each change. 
         */
        cnt = changes.length();
        oldList = new Vector(cnt / 2);

        /* run through the change list and make each change */
        for (i = 1 ; i <= cnt ; i += 2)
        {
            local curProp;
            local newVal;
            
            /* get the current property and new value */
            curProp = changes[i];
            newVal = changes[i+1];
            
            /* see what we have */
            switch(curProp)
            {
            case &moveInto:
                /*
                 *   It's the special moveInto property.  This indicates
                 *   that we are to move self into the given new 
                 *   location.  First save the current location, then 
                 *   use baseMoveInto to make the change without 
                 *   triggering any side effects.  Note that if this 
                 *   would create circular containment, we won't make 
                 *   the move.
                 */
                oldList.append(saveLocation());
                if (newVal != self && !newVal.isIn(self))
                    baseMoveInto(newVal);
                break;

            default:
                /* 
                 *   Anything else is a property to which we want to
                 *   assign the given new value.  Remember the old value
                 *   in our original values vector, then set the new value
                 *   from the argument.  
                 */
                oldList.append(self.(curProp));
                self.(curProp) = newVal;
                break;
            }
        }

        /* 
         *   the changes are now in effect - invoke the given callback
         *   function to check things out under the new conditions, but
         *   protect the code so that we are sure to restore things on the
         *   way out 
         */
        try
        {
            /* invoke the given callback, returning the result */
            return (func)();
        }
        finally
        {
            /* run through the change list and undo each change */
            for (i = 1, local j = 1 ; i <= cnt ; i += 2, ++j)
            {
                local curProp;
                local oldVal;
            
                /* get the current property and old value */
                curProp = changes[i];
                oldVal = oldList[j];
            
                /* see what we have */
                switch(curProp)
                {
                case &moveInto:
                    /* restore the location that we saved */
                    restoreLocation(oldVal);
                    break;

                default:
                    /* restore the property value */
                    self.(curProp) = oldVal;
                    break;
                }
            }
        }
    }

    /*
     *   Run a what-if test to see what would happen if this object were
     *   being held directly by the given actor.
     */
    whatIfHeldBy(func, actor)
    {
        /* 
         *   by default, simply run the what-if test with this object
         *   moved into the actor's inventory
         */
        return whatIf(func, &moveInto, actor);
    }

    /*
     *   Check a proposed change in my bulk.  When this is called, the new
     *   bulk should already be in effect (the best way to do this when
     *   just making a check is via whatIf).
     *   
     *   This routine can be called during the 'check' or 'action' stages
     *   of processing a command to determine if a change in my bulk would
     *   cause a problem.  If so, we'll add a failure report and exit the
     *   command.
     *   
     *   By default, notify our immediate container of the change to see
     *   if there's any objection.  A change in an object's bulk typically
     *   only aaffects its container or containers.
     *   
     *   The usual way to invoke this routine in a 'check' or 'action'
     *   routine is with something like this:
     *   
     *      whatIf({: checkBulkChange()}, &inflated, true);
     *   
     *   This checks to see if the change in my bulk implied by changing
     *   self.inflated to true would present a problem for my container,
     *   terminating with a reportFailure+exit if so.  
     */
    checkBulkChange()
    {
        /*
         *   Notify each of our containers of a change in our bulk; if
         *   any container has a problem with our new bulk, it can
         *   report a failure and exit.
         */
        forEachContainer(
            {loc: loc.checkBulkChangeWithin(self)});
    }

    /*
     *   Check a bulk change of one of my direct contents, given by obj.
     *   When this is called, 'obj' will be (tentatively) set to reflect
     *   its proposed new bulk; if this routine doesn't like the new bulk,
     *   it should issue a failure report and exit the command, which will
     *   cancel the command that would have caused the change and will
     *   prevent the proposed change from taking effect.
     *   
     *   By default, we'll do nothing; subclasses that are sensitive to
     *   the bulks of their contents should override this.  
     */
    checkBulkChangeWithin(changingObj)
    {
    }

    /*
     *   Get "bag of holding" affinities for the given list of objects.
     *   For each item in the list, we'll get the item's bulk, and get
     *   each bag's affinity for containing the item.  We'll note the bag
     *   with the highest affinity.  Once we have the list, we'll put it
     *   in descending order of affinity, and return the result as a
     *   vector.  
     */
    getBagAffinities(lst)
    {
        local infoVec;
        local bagVec; 
        
        /*
         *   First, build a list of all of the bags of holding within me.
         *   We start with the list of bags because we want to check each
         *   object to see which bag we might want to put it in.  
         */
        bagVec = new Vector(10);
        getBagsOfHolding(bagVec);

        /* if there are no bags of holding, there will be no affinities */
        if (bagVec.length() == 0)
            return [];

        /* create a vector to hold information on each child item */
        infoVec = new Vector(lst.length());

        /* look at each child item */
        foreach (local cur in lst)
        {
            local maxAff;
            local bestBag;

            /* find the bag with the highest affinity for this item */
            maxAff = 0;
            bestBag = nil;
            foreach (local bag in bagVec)
            {
                local aff;

                /* get this bag's affinity for this item */                
                aff = bag.affinityFor(cur);

                /* 
                 *   If this is the best so far, note it.  If this bag has
                 *   an affinity of zero for this item, it means it
                 *   doesn't want it at all, so don't even consider it in
                 *   this case. 
                 */
                if (aff != 0 && (bestBag == nil || aff > maxAff))
                {
                    /* this is the best so far - note it */
                    maxAff = aff;
                    bestBag = bag;
                }
            }

            /* 
             *   if we found a bag that wants this item, add it to our
             *   list of results 
             */
            if (bestBag != nil)
                infoVec.append(new BagAffinityInfo(
                    cur, cur.getEncumberingBulk(self),
                    maxAff, bestBag));
        }
        
        /* sort the list in descending order of affinity */
        infoVec = infoVec.sort(SortDesc, {a, b: a.compareAffinityTo(b)});

        /*
         *   Go through the list and ensure that any item in the list
         *   that's destined to go inside another item that's also in the
         *   list gets put in the other item before the second item gets
         *   put in its bag.  For example, if we have a credit card that
         *   goes in a wallet, and a wallet that goes in a pocket, we want
         *   to ensure that we put the credit card in the wallet before we
         *   put the wallet in the pocket.
         *   
         *   The point of this reordering is that, in some cases, we might
         *   not be able to move the first item once the second item has
         *   been moved.  In our credit-card/wallet/pocket example, the
         *   wallet might have to be closed to go in the pocket, at which
         *   point we'd have to take it out again to put the credit card
         *   into it, defeating the purpose of having put it away in the
         *   first place.  We avoid these kinds of circular traps by
         *   making sure the list is ordered such that we dispose of the
         *   credit card first, then the wallet.
         *   
         *   To avoid looping forever in cases of circular containment,
         *   don't allow any more moves than there are items in the list.  
         */
        for (local i = 1, local len = infoVec.length(), local moves = 0 ;
             i <= len && moves <= len ; ++i)
        {
            /* get this item and its destination */
            local cur = infoVec[i];
            local dest = cur.bag_;
            
            /* 
             *   check to see if the destination itself appears in the
             *   list as an item to be bagged 
             */
            local idx = infoVec.indexWhich({x: x.obj_ == dest});

            /* 
             *   if the destination item appears in the list before the
             *   current item, move the current item before its
             *   destination 
             */
            if (idx != nil && idx < i)
            {
                /* remove the current item from the list */
                infoVec.removeElementAt(i);

                /* re-insert it just before its destination item */
                infoVec.insertAt(idx, cur);

                /* count the move */
                ++moves;

                /* 
                 *   Now, back up and resume the scan from the item just
                 *   after the item that moves our destination bag.  This
                 *   will ensure that any items destined to go in the bag
                 *   we just moved are caught.  (Although note that we
                 *   start just after the destination item, rather than at
                 *   the destination item, because the destination item
                 *   can't be legitimately moved at this point: if it is,
                 *   it's because we have a circular containment plan.  We
                 *   obviously can't resolve circular containment into a
                 *   linear ordering, so there's no point in even looking
                 *   at the destination item again at this point.  If we
                 *   have a circular chain that's several elements long,
                 *   this won't avoid getting stuck; for that, we have to
                 *   count on our 'moves' limit.)  
                 */
                i = idx + 1;
            }
        }

        /* return the result */
        return infoVec;
    }
    
    /*
     *   Find all of the bags of holding contained within this object and
     *   add them to the given vector.  By default, we'll simply recurse
     *   into our children so they can add their bags of holding.  
     */
    getBagsOfHolding(vec)
    {
        /* 
         *   if my contents can't be touched from outside, there's no
         *   point in traversing our children 
         */
        if (transSensingIn(touch) != transparent)
            return;

        /* add each of my children's bags to the list */
        foreach (local cur in contents)
            cur.getBagsOfHolding(vec);
    }

    /*
     *   The strength of the light the object is giving off, if indeed it
     *   is giving off light.  This value should be one of the following:
     *   
     *   0: The object is giving off no light at all.
     *   
     *   1: The object is self-illuminating, but doesn't give off enough
     *   light to illuminate any other objects.  This is suitable for
     *   something like an LED digital clock.
     *   
     *   2: The object gives off dim light.  This level is bright enough to
     *   illuminate nearby objects, but not enough to go through obscuring
     *   media, and not enough for certain activities requiring strong
     *   lighting, such as reading.
     *   
     *   3: The object gives off medium light.  This level is bright enough
     *   to illuminate nearby objects, and is enough for most activities,
     *   including reading and the like.  Traveling through an obscuring
     *   medium reduces this level to dim (2).
     *   
     *   4: The object gives off strong light.  This level is bright enough
     *   to illuminate nearby objects, and travel through an obscuring
     *   medium reduces it to medium light (3).
     *   
     *   Note that the special value -1 is reserved as an invalid level,
     *   used to flag certain events (such as the need to recalculate the
     *   ambient light level from a new point of view).
     *   
     *   Most objects do not give off light at all.  
     */
    brightness = 0
    
    /*
     *   Sense sizes of the object.  Each object has an individual size for
     *   each sense.  By default, objects are medium for all senses; this
     *   allows them to be sensed from a distance or through an obscuring
     *   medium, but doesn't allow their details to be sensed.  
     */
    sightSize = medium
    soundSize = medium
    smellSize = medium
    touchSize = medium

    /*
     *   Determine whether or not the object has a "presence" in each
     *   sense.  An object has a presence in a sense if an actor
     *   immediately adjacent to the object could detect the object by the
     *   sense alone.  For example, an object has a "hearing presence" if
     *   it is making some kind of noise, and does not if it is silent.
     *   
     *   Presence in a given sense is an intrinsic (which does not imply
     *   unchanging) property of the object, in that presence is
     *   independent of the relationship to any given actor.  If an alarm
     *   clock is ringing, it has a hearing presence, unconditionally; it
     *   doesn't matter if the alarm clock is sealed inside a sound-proof
     *   box, because whether or not a given actor has a sense path to the
     *   object is a matter for a different computation.
     *   
     *   Note that presence doesn't control access: an actor might have
     *   access to an object for a sense even if the object has no
     *   presence in the sense.  Presence indicates whether or not the
     *   object is actively emitting sensory data that would make an actor
     *   aware of the object without specifically trying to apply the
     *   sense to the object.
     *   
     *   By default, an object is visible and touchable, but does not emit
     *   any sound or odor.  
     */
    sightPresence = true
    soundPresence = nil
    smellPresence = nil
    touchPresence = true

    /*
     *   My "contents lister."  This is a Lister object that we use to
     *   display the contents of this object for room descriptions,
     *   inventories, and the like.  
     */
    contentsLister = thingContentsLister

    /* 
     *   the Lister to use when showing my contents as part of my own
     *   description (i.e., for Examine commands) 
     */
    descContentsLister = thingDescContentsLister

    /* 
     *   the Lister to use when showing my contents in response to a
     *   LookIn command 
     */
    lookInLister = thingLookInLister

    /* 
     *   my "in-line" contents lister - this is the Lister object that
     *   we'll use to display my contents parenthetically as part of my
     *   list entry in a second-level contents listing
     */
    inlineContentsLister = inlineListingContentsLister

    /*
     *   My "special" contents lister.  This is the Lister to use to
     *   display the special descriptions for objects that have special
     *   descriptions when we're showing a room description for this
     *   object. 
     */
    specialContentsLister = specialDescLister


    /*
     *   Determine if I can be sensed under the given conditions.  Returns
     *   true if the object can be sensed, nil if not.  If this method
     *   returns nil, this object will not be considered in scope for the
     *   current conditions.
     *   
     *   By default, we return nil if the ambient energy level for the
     *   object is zero.  If the ambient level is non-zero, we'll return
     *   true in 'transparent' conditions, nil for 'opaque', and we'll let
     *   the sense decide via its canObjBeSensed() method for any other
     *   transparency conditions.  Note that 'ambient' as given here is the
     *   ambient level *at the object*, not as seen along my sense path -
     *   so this should NOT be given as the ambient value from a SenseInfo,
     *   which has already been adjusted for the sense path.  
     */
    canBeSensed(sense, trans, ambient)
    {
        /* 
         *   adjust the ambient level for the transparency path, if the
         *   sense uses ambience at all 
         */
        if (sense.ambienceProp != nil)
        {
            /* 
             *   adjust the ambient level for the transparency - if that
             *   leaves a level of zero, the object can't be sensed 
             */
            if (adjustBrightness(ambient, trans) == 0)
                return nil;
        }

        /* check the viewing conditions */
        switch(trans)
        {
        case transparent:
        case attenuated:
            /* 
             *   under transparent or attenuated conditions, I appear as
             *   myself 
             */
            return true;

        case obscured:
        case distant:
            /* 
             *   ask the sense to determine if I can be sensed under these
             *   conditions 
             */
            return sense.canObjBeSensed(self, trans, ambient);

        default:
            /* for any other conditions, I can't be sensed at all */
            return nil;
        }
    }

    /*
     *   Determine if I can be sensed IN DETAIL in the given sense, with
     *   the given SenseInfo description.  By default, an object's details
     *   can be sensed if the sense path is 'transparent' or 'attenuated',
     *   OR the object's scale in the sense is 'large'.
     */
    canDetailsBeSensed(sense, info, pov)
    {
        /* if the sense path is opaque, we can be sensed */
        if (info == nil || info.trans == opaque)
            return nil;

        /* 
         *   if we have 'large' scale in the sense, our details can be
         *   sensed under any conditions 
         */
        if (self.(sense.sizeProp) == large)
            return true;

        /* 
         *   we don't have 'large' scale in the sense, so we can be sensed
         *   in detail only if the sense path is 'transparent' or
         *   'attenuated' 
         */
        return (info.trans is in (transparent, attenuated));
    }

    /*
     *   Call a method on this object from the given point of view.  We'll
     *   push the current point of view, call the method, then restore the
     *   enclosing point of view. 
     */
    fromPOV(actor, pov, propToCall, [args])
    {
        /* push the new point of view */
        pushPOV(actor, pov);

        /* make sure we pop the point of view no matter how we leave */
        try
        {
            /* call the method */
            self.(propToCall)(args...);
        }
        finally
        {
            /* restore the enclosing point of view on the way out */
            popPOV();
        }
    }

    /*
     *   Every Thing has a location, which is the Thing that contains this
     *   object.  A Thing's location can only be a simple object
     *   reference, or nil; it cannot be a list, and it cannot be a method.
     *   
     *   If the location is nil, the object does not exist anywhere in the
     *   simulation's physical model.  A nil location can be used to
     *   remove an object from the game world, temporarily or permanently.
     *   
     *   In general, the 'location' property should be declared for each
     *   statically defined object (explicitly or implicitly via the '+'
     *   syntax).  'location' is a private property - it should never be
     *   evaluated or changed by any subclass or by any other object.
     *   Only Thing methods may evaluate or change the 'location'
     *   property.  So, you can declare a 'location' property when
     *   defining an object, but you should essentially never refer to
     *   'location' directly in any other context; instead, use the
     *   location and containment methods (isIn, etc) when you want to
     *   know an object's containment relationship to another object.  
     */
    location = nil

    /*
     *   Get the direct container we have in common with the given object,
     *   if any.  Returns at most one common container.  Returns nil if
     *   there is no common location.  
     */
    getCommonDirectContainer(obj)
    {
        local found;

        /* we haven't found one yet */
        found = nil;
        
        /* scan each of our containers for one in common with 'loc' */
        forEachContainer(new function(loc) {
            /*
             *   If this location of ours is a direct container of the
             *   other object, it is a common location, so note it.  
             */
            if (obj.isDirectlyIn(loc))
                found = loc;
        });

        /* return what we found */
        return found;
    }

    /*
     *   Get the container (direct or indirect) we have in common with the
     *   object, if any.  
     */
    getCommonContainer(obj)
    {
        local found;

        /* we haven't found one yet */
        found = nil;
        
        /* scan each of our containers for one in common with 'loc' */
        forEachContainer(new function(loc) {
            /*
             *   If this location of ours is a direct container of the
             *   other object, it is a common location, so note it.  
             */
            if (obj.isIn(loc))
                found = loc;
        });

        /* if we found a common container, return it */
        if (found != nil)
            return found;

        /* 
         *   we didn't find obj's container among our direct containers,
         *   so try our direct container's containers 
         */
        forEachContainer(new function(loc) {
            local cur;

            /* try finding for a common container of this container */
            cur = loc.getCommonContainer(obj);

            /* if we found it, note it */
            if (cur != nil)
                found = cur;
        });

        /* return what we found */
        return found;
    }

    /*
     *   Get our "identity" object.  This is the object that we appear to
     *   be an inseparable part of.
     *   
     *   In most cases, an object is its own identity object.  However,
     *   there are times when the object that a player sees isn't the same
     *   as the object that the parser resolves, because we're modeling a
     *   single physical object with several programming objects.  For
     *   example, a complex container has one or more secret internal
     *   program objects representing the different sub-containers; as far
     *   as the player is concerned, all of the sub-containers are just
     *   aspects of the parent complex container, not separate object, so
     *   the sub-containers all have the identity of the parent complex
     *   container.
     *   
     *   By default, this is simply 'self'.  Objects that take their
     *   effective identities from other objects must override this
     *   accordingly.  
     */
    getIdentityObject() { return self; }

    /*
     *   Am I a component of the given object?  The base Thing is not a
     *   component of anything, so we'll simply return nil. 
     */
    isComponentOf(obj) { return nil; }

    /*
     *   General initialization - this will be called during preinit so
     *   that we can set up the initial values of any derived internal
     *   properties.  
     */
    initializeThing()
    {
        /* initialize our location settings */
        initializeLocation();

        /* 
         *   if we're marked 'equivalent', it means we're interchangeable
         *   in parser input with other objects with the same name; set up
         *   our equivalence group listing if so 
         */
        if (isEquivalent)
            initializeEquivalent();

        /* if we have a global parameter name, add it to the global table */
        if (globalParamName != nil)
            langMessageBuilder.nameTable_[globalParamName] = self;
    }

    /*
     *   Initialize my location's contents list - add myself to my
     *   container during initialization
     */
    initializeLocation()
    {
        if (location != nil)
            location.addToContents(self);
    }

    /*
     *   Initialize this class object for listing its instances that are
     *   marked with isEquivalent.  We'll initialize a list group that
     *   lists our equivalent instances as a group.
     *   
     *   Objects are grouped by their equivalence key - each set of objects
     *   with the same key is part of the same group.  The key is
     *   determined by the language module, but is usually just the basic
     *   disambiguation name (the 'disambigName' property in English, for
     *   example).  
     */
    initializeEquivalent()
    {
        /* 
         *   If we're already in an equivalence group (because our key has
         *   changed, for example, or due to inheritance), remove the
         *   existing group from the group list.  If the group isn't
         *   changing after all, we'll just add it back, so removing it
         *   should be harmless.  
         */
        if (equivalentGrouper != nil)
            listWith -= equivalentGrouper;

        /* look up our equivalence key in the global table of groupers */
        equivalentGrouper = equivalentGrouperTable[equivalenceKey];

        /* if there's no grouper for our key, create one */
        if (equivalentGrouper == nil)
        {
            /* create a new grouper */
            equivalentGrouper = equivalentGrouperClass.createInstance();

            /* add it to the table */
            equivalentGrouperTable[equivalenceKey] = equivalentGrouper;
        }

        /* 
         *   Add it to our list, as long as our listWith isn't already
         *   defined as a method.  Note that we intentionally add it as the
         *   last element, because an equivalent group is the most specific
         *   kind of group possible.  
         */
        if (propType(&listWith) != TypeCode)
            listWith += equivalentGrouper;
    }

    /*
     *   A static game-wide table of equivalence groups.  This has a table
     *   of ListGroupEquivalent-derived objects, keyed by equivalence name.
     *   Each group of objects with the same equivalence name is listed in
     *   the same group and so has the same grouper object.  
     */
    equivalentGrouperTable = static (new LookupTable(32, 64))

    /* 
     *   my equivalence grouper class - when we initialize, we'll create a
     *   grouper of this class and store it in equivalentGrouper 
     */
    equivalentGrouperClass = ListGroupEquivalent

    /* 
     *   Our equivalent item grouper.  During initialization, we will
     *   create an equivalent grouper and store it in this property for
     *   each class object that has instances marked with isEquivalent.
     *   Note that this is stored with the class, because we want each of
     *   our equivalent instances to share the same grouper object so that
     *   they are listed together as a group.  
     */
    equivalentGrouper = nil

    /*
     *   My contents.  This is a list of the objects that this object
     *   directly contains.
     */
    contents = []

    /*
     *   Get my associated noise object.  By default, this looks for an
     *   item of class Noise directly within me.
     */
    getNoise()
    {
        /* 
         *   Look for a Noise object among my direct contents.  Only look
         *   for a noise with an active sound presence.  
         */
        return contents.valWhich(
            {obj: obj.ofKind(Noise) && obj.soundPresence});
    }

    /*
     *   Get my associated odor object.  By default, this looks for an
     *   item of class Odor directly within me. 
     */
    getOdor()
    {
        /* 
         *   Look for an Odor object among my direct contents.  Only look
         *   for an odor with an active smell presence. 
         */
        return contents.valWhich(
            {obj: obj.ofKind(Odor) && obj.smellPresence});
    }

    /*
     *   Get a vector of all of my contents, recursively including
     *   contents of contents.  
     */
    allContents()
    {
        local vec;
        
        /* start with an empty vector */
        vec = new Vector(32);

        /* add all of my contents to the vector */
        addAllContents(vec);

        /* return the result */
        return vec;
    }

    /*
     *   Get a list of objects suitable for matching ALL in TAKE ALL FROM
     *   <self>.  By default, this simply returns the list of everything in
     *   scope that's directly inside 'self'.  
     */
    getAllForTakeFrom(scopeList)
    {
        /* 
         *   include only objects contained within 'self' that aren't
         *   components of 'self' 
         */
        return scopeList.subset(
            {x: x != self && x.isDirectlyIn(self) && !x.isComponentOf(self)});
    }

    /* 
     *   add myself and all of my contents, recursively including contents
     *   of contents, to a vector 
     */
    addAllContents(vec)
    {
        /* visit everything in my contents */
        foreach (local cur in contents)
        {
            /* 
             *   if this item is already in the vector, skip it - we've
             *   already visited it and its contents
             */
            if (vec.indexOf(cur) != nil)
                continue;
            
            /* add this item */
            vec.append(cur);

            /* add this item's contents recursively */
            cur.addAllContents(vec);
        }
    }

    /*
     *   Show the contents of this object, as part of a recursive listing
     *   generated as part of the description of our container, our
     *   container's container, or any further enclosing container.
     *   
     *   If the object has any contents, we'll display a listing of the
     *   contents.  This is used to display the object's contents as part
     *   of the description of a room ("look around"), of an object
     *   ("examine box"), or of an object's contents ("look in box").
     *   
     *   'options' is the set of flags that we'll pass to showList(), and
     *   has the same meaning as for that function.
     *   
     *   'infoTab' is a lookup table of SenseInfo objects for the objects
     *   that the actor to whom we're showing the contents listing can see
     *   via the sight-like senses.
     *   
     *   This method should be overridden by any object that doesn't store
     *   its contents using a simple 'contents' list property.  
     */
    showObjectContents(pov, lister, options, indent, infoTab)
    {
        local cont;

        /* get my listable contents */
        cont = lister.getListedContents(self, infoTab);
        
        /* if the surviving list isn't empty, show it */
        if (cont != [])
            lister.showList(pov, self, cont, options, indent, infoTab, nil);
    }

    /*
     *   Show the contents of this object as part of an inventory listing.
     *   By default, we simply use the same listing we do for the normal
     *   contents listing. 
     */
    showInventoryContents(pov, lister, options, indent, infoTab)
    {
        /* by default, use the normal room/object contents listing */
        showObjectContents(pov, lister, options, indent, infoTab);
    }

    /*
     *   Get my listed contents for recursive object descriptions.  This
     *   returns the list of the direct contents that we'll mention when
     *   we're examining our container's container, a further enclosing
     *   container, or the enclosing room.
     *   
     *   By default, we'll return the same list we'll display on direct
     *   examination of this object.
     */
    getListedContents(lister, infoTab)
    {
        /* 
         *   return the contents we'd list when directly examining self,
         *   limited to the listable contents 
         */
        return getContentsForExamine(lister, infoTab)
            .subset({x: lister.isListed(x)});
    }

    /*
     *   Get the listed contents in a direct examination of this object.
     *   By default, this simply returns a list of all of our direct
     *   contents that are included in the info table.  
     */
    getContentsForExamine(lister, infoTab)
    {
        /*
         *   return only my direct contents that are found in the infoTab,
         *   since these are the objects that can be sensed from the
         *   actor's point of view 
         */
        return contents.subset({x: infoTab[x] != nil});
    }

    /* 
     *   Is this a "top-level" location?  A top-level location is an
     *   object which doesn't have another container, so its 'location'
     *   property is nil, but which is part of the game universe anyway.
     *   In most cases, a top-level location is simply a Room, since the
     *   network of rooms makes up the game's map.
     *   
     *   If an object has no location and is not itself a top-level
     *   location, then the object is not part of the game world.  It's
     *   sometimes useful to remove objects from the game world, such as
     *   when they're destroyed within the context of the game.  
     */
    isTopLevel = nil

    /*
     *   Determine if I'm is inside another Thing.  Returns true if this
     *   object is contained within 'obj', directly or indirectly (that
     *   is, this returns true if my immediate container is 'obj', OR my
     *   immediate container is in 'obj', recursively defined).
     *   
     *   isIn(nil) returns true if this object is "outside" the game
     *   world, which means that the object is not reachable from anywhere
     *   in the game map and is thus not part of the simulation.  This is
     *   the case if our outermost container is NOT a top-level object, as
     *   indicated by its isTopLevel property.  If we're inside an object
     *   marked as a top-level object, or we're inside an object that's
     *   inside a top-level object (and so on), then we're part of the
     *   game world, so isIn(nil) will return nil.  If our outermost
     *   container is has a nil isTopLevel property, isIn(nil) will return
     *   true.
     *   
     *   Note that our notion of "in" is not limited to enclosing
     *   containment, because the same containment hierarchy is used to
     *   represent all types of containment relationships, including
     *   things being "on" other things and part of other things.  
     */
    isIn(obj)
    {
        local loc = location;
        
        /* if we have no location, we're not inside any object */
        if (loc == nil)
        {
            /*
             *   We have no container, so there are two possibilities:
             *   either we're not part of the game world at all, or we're
             *   a top-level object, which is an object that's explicitly
             *   part of the game world and at the top of the containment
             *   tree.  Our 'isTopLevel' property determines which it is.
             *   
             *   If they're asking us if we're inside 'nil', then what
             *   they want to know is if we're outside the game world.  If
             *   we're not a top-level object, we are outside the game
             *   world, so isIn(nil) is true; if we are a top-level
             *   object, then we're explicitly part of the game world, so
             *   isIn(nil) is false.
             *   
             *   If they're asking us if we're inside any non-nil object,
             *   then they simply want to know if we're inside that
             *   object.  So, if 'obj' is not nil, we must return nil:
             *   we're not in any object, so we can't be in 'obj'.  
             */
            if (obj == nil)
            {
                /* 
                 *   they want to know if we're outside the simulation
                 *   entirely: return true if we're NOT a top-level
                 *   object, nil otherwise 
                 */
                return !isTopLevel;
            }
            else
            {
                /*
                 *   they want to know if we're inside some specific
                 *   object; we can't be, because we're not in any object 
                 */
                return nil;
            }
        }

        /* if obj is my immediate container, I'm obviously in it */
        if (loc == obj)
            return true;

        /* I'm in obj if my container is in obj */
        return loc.isIn(obj);
    }

    /*
     *   Determine if I'm directly inside another Thing.  Returns true if
     *   this object is contained directly within obj.  Returns nil if
     *   this object isn't directly within obj, even if it is indirectly
     *   in obj (i.e., its container is directly or indirectly in obj).  
     */
    isDirectlyIn(obj)
    {
        /* I'm directly in obj only if it's my immediate container */
        return location == obj;
    }

    /*
     *   Determine if I'm "nominally" inside the given Thing.  Returns true
     *   if the object is actually within the given object, OR the object
     *   is a room part and I'm nominally in the room part.  
     */
    isNominallyIn(obj)
    {
        /* if I'm actually in the given object, I'm nominally in it as well */
        if (isIn(obj))
            return true;

        /* 
         *   if the object is a room part, and we're nominally in the room
         *   part, then we're nominally in the object 
         */
        if (obj.ofKind(RoomPart) && isNominallyInRoomPart(obj))
            return true;

        /* we're not nominally in the object */
        return nil;
    }

    /*
     *   Determine if this object is contained within an item fixed in
     *   place within the given location.  We'll check our container to
     *   see if its contents are within an object fixed in place in the
     *   given location.
     *   
     *   This is a rather specific check that might seem a bit odd, but
     *   for some purposes it's useful to treat objects within fixed
     *   containers in a location as though they were in the location
     *   itself, because fixtures of a location are to some extent parts
     *   of the location.  
     */
    isInFixedIn(loc)
    {
        /* return true if my location's contents are in fixed things */
        return location != nil && location.contentsInFixedIn(loc);
    }

    /* Am I either inside 'obj', or equal to 'obj'?  */
    isOrIsIn(obj) { return self == obj || isIn(obj); }

    /*
     *   Are my contents within a fixed item that is within the given
     *   location?  By default, we return nil because we are not ourselves
     *   fixed. 
     */
    contentsInFixedIn(loc) { return nil; }

    /*
     *   Determine if I'm "held" by an actor, for the purposes of being
     *   manipulated in an action.  In most cases, an object is considered
     *   held by an actor if it's directly within the actor's inventory,
     *   because the actor's direct inventory represents the contents of
     *   the actors hands (or equivalent).
     *   
     *   Some classes might override this to change the definition of
     *   "held" to include things not directly in the actor's inventory or
     *   exclude things directly in the inventory.  For example, an item
     *   being worn is generally not considered held even though it might
     *   be in the direct inventory, and a key on a keyring is considered
     *   held if the keyring is being held.  
     */
    isHeldBy(actor)
    {
        /* 
         *   by default, an object is held if and only if it's in the
         *   direct inventory of the actor 
         */
        return isDirectlyIn(actor);
    }

    /*
     *   Are we being held by the given actor for the purposes of the
     *   objHeld precondition?  By default, and in almost all cases, this
     *   simply returns isHeldBy().  In some rare cases, it might make
     *   sense to consider an object to meet the objHeld condition even if
     *   isHeldBy returns nil; for example, an actor's hands and other
     *   body parts can't be considered to be held, but they also don't
     *   need to be for any command operating on them.  
     */
    meetsObjHeld(actor) { return isHeldBy(actor); }

    /*
     *   Add to a vector all of my contents that are directly held when
     *   I'm being directly held.  This is used to add the direct contents
     *   of an item to scope when the item itself is being directly held.
     *   
     *   In most cases, we do nothing.  Certain types of objects override
     *   this because they consider their contents to be held if they're
     *   held.  For example, a keyring considers all of its keys to be
     *   held if the keyring itself is held, because the keys are attached
     *   to the keyring rather than contained within it.  
     */
    appendHeldContents(vec)
    {
        /* by default, do nothing */
    }

    /*
     *   Determine if I'm "owned" by another object.  By default, if we
     *   have an explicit owner, then we are owned by 'obj' if and only if
     *   'obj' is our explicit owner; otherwise, if 'obj' is our immediate
     *   location, and our immediate location can own us (as reported by
     *   obj.canOwn(self)), then 'obj' owns us; otherwise, 'obj' owns us
     *   if our immediate location CANNOT own us AND our immediate
     *   location is owned by 'obj'.  This last case is tricky: it means
     *   that if we're inside something other than 'obj' that can own us,
     *   such as another actor, then 'obj' doesn't own us because our
     *   immediate location does; it also means that if we're inside an
     *   object that has an explicit owner rather than an owner based on
     *   location, we have the same explicit owner, so a dollar bill
     *   inside Bob's wallet which is in turn being carried by Charlie is
     *   owned by Bob, not Charlie.
     *   
     *   This is used to determine ownership for the purpose of
     *   possessives in commands.  Ownership is not always exclusive: it
     *   is possible for a given object to have multiple owners in some
     *   cases.  For example, if Bob and Bill are both sitting on a couch,
     *   the couch could be referred to as "bob's couch" or "bill's
     *   couch", so the couch is owned by both Bob and Bill.  It is also
     *   possible for an object to be unowned.
     *   
     *   In most cases, ownership is a function of location (possession is
     *   nine-tenths of the law, as they say), but not always; in some
     *   cases, an object has a particular owner regardless of its
     *   location, such as "bob's wallet".  This default implementation
     *   allows for ownership by location, as well as explicit ownership,
     *   with explicit ownership (as indicated by the self.owner property)
     *   taking precedence.  
     */
    isOwnedBy(obj)
    {
        local cont;
        
        /* 
         *   if I have an explicit owner, then obj is my owner if it
         *   matches my explicit owner 
         */
        if (owner != nil)
            return owner == obj;
        
        /*
         *   Check my immediate container to see if it's the owner in
         *   question.  
         */
        if (isDirectlyIn(obj))
        {
            /* 
             *   My immediate container is the owner of interest.  If this
             *   container can own me, then I'm owned by it because we
             *   didn't find any other owners first.  Otherwise, I'm not
             *   owned by it because it can't own me in the first place. 
             */
            return obj.canOwn(self);
        }

        /* presume we have a single-location container */
        cont = location;

        /* check to see if we have no single location */
        if (cont == nil)
        {
            /* 
             *   I have no location, so either I'm not inside anything at
             *   all, or I have multiple locations.  If we're indirectly
             *   in 'obj', then proceed up the containment tree branch by
             *   which 'obj' contains us.  If we're not even indirectly in
             *   'obj', then there's no containment relationship that
             *   establishes ownership.  
             */
            if (isIn(obj))
            {
                /* 
                 *   we're indirectly in 'obj', so get the containment
                 *   branch on which we're contained by 'obj' 
                 */
                forEachContainer(new function(x)
                {
                    if (x == obj || x.isIn(obj))
                        cont = x;
                });
            }
            else
            {
                /* 
                 *   we're not in 'obj', so there's no containment
                 *   relationship that establishes ownership 
                 */
                return nil;
            }
        }

        /*
         *   My immediate container is not the object of interest.  If
         *   this container can own me, then I'm owned by this container
         *   and NOT by obj.  
         */
        if (cont.canOwn(self))
            return nil;

        /*
         *   My container can't own me, so it's not my owner, so our owner
         *   is my container's owner - thus, I'm owned by obj if my
         *   location is owned by obj.  
         */
        return cont.isOwnedBy(obj);
    }

    /*
     *   My explicit owner.  By default, objects do not have explicit
     *   owners, which means that the owner at any given time is
     *   determined by the object's location - my innermost container that
     *   can own me is my owner.
     *   
     *   However, in some cases, an object is inherently owned by a
     *   particular other object (usually an actor), and this is invariant
     *   even when the object moves to a new location not within the
     *   owner.  For such cases, this property can be set to the explicit
     *   owner object, which will cause self.isOwnedBy(obj) to return true
     *   if and only if obj == self.owner.  
     */
    owner = nil

    /*
     *   Get the "nominal owner" of this object.  This is the owner that
     *   we report for the object if asked to distinguish this object from
     *   another via the OwnershipDistinguisher.  Note that the nominal
     *   owner is not necessarily the only owner, because an object can
     *   have multiple owners in some cases; however, the nominal owner
     *   must always be an owner, in that isOwnedBy(getNominalOwner())
     *   should always return true.
     *   
     *   By default, if we have an explicit owner, we'll return that.
     *   Otherwise, if our immediate container can own us, we'll return
     *   our immediate container.  Otherwise, we'll return our immediate
     *   container's nominal owner.  Note that this last case means that a
     *   dollar bill inside Bob's wallet will be Bob's dollar bill, even
     *   if Bob's wallet is currently being carried by another actor.  
     */
    getNominalOwner()
    {
        /* if we have an explicit owner, return that */
        if (owner != nil)
            return owner;

        /* if we have no location, we have no owner */
        if (location == nil)
            return nil;

        /* if our immediate location can own us, return it */
        if (location.canOwn(self))
            return location;

        /* return our immediate location's owner */
        return location.getNominalOwner();
    }

    /*
     *   Can I own the given object?  By default, objects cannot own other
     *   objects.  This can be overridden when ownership is desired.
     *   
     *   This doesn't determine that we *do* own the given object, but
     *   only that we *can* own the given object.
     */
    canOwn(obj) { return nil; }

    /*
     *   Get the carrying actor.  This is the nearest enclosing location
     *   that's an actor. 
     */
    getCarryingActor()
    {
        /* if I don't have a location, there's no carrier */
        if (location == nil)
            return nil;

        /* if my location is an actor, it's the carrying actor */
        if (location.isActor)
            return location;

        /* return my location's carrying actor */
        return location.getCarryingActor();
    }

    /*
     *   Try making the current command's actor hold me.  By default,
     *   we'll simply try a "take" command on the object.  
     */
    tryHolding()
    {
        /*   
         *   Try an implicit 'take' command.  If the actor is carrying the
         *   object indirectly, make the command "take from" instead,
         *   since what we really want to do is take the object out of its
         *   container.  
         */
        if (isIn(gActor))
            return tryImplicitAction(TakeFrom, self, location);
        else
            return tryImplicitAction(Take, self);
    }

    /*
     *   Try moving the given object into this object, with an implied
     *   command.  By default, since an ordinary Thing doesn't have a way
     *   of adding new contents by a player command, this does nothing.
     *   Containers and other objects that can hold new contents can
     *   override this as appropriate.
     */
    tryMovingObjInto(obj) { return nil; }

    /* 
     *   Report a failure of the condition that tryMovingObjInto tries to
     *   bring into effect.  By default, this simply says that the object
     *   must be in 'self'.  Some objects might want to override this when
     *   they describe containment specially; for example, an actor might
     *   want to say that the actor "must be carrying" the object.  
     */
    mustMoveObjInto(obj) { reportFailure(&mustBeInMsg, obj, self); }

    /*
     *   Add an object to my contents.
     *   
     *   Note that this should NOT be overridden to cause side effects -
     *   if side effects are desired when inserting a new object into my
     *   contents, use notifyInsert().  This routine is not allowed to
     *   cause side effects because it is sometimes necessary to bypass
     *   side effects when moving an item.  
     */
    addToContents(obj)
    {
        /* add the object to my contents list */
        contents += obj;
    }

    /*
     *   Remove an object from my contents.
     *   
     *   Do NOT override this routine to cause side effects.  If side
     *   effects are desired when removing an object, use notifyRemove().  
     */
    removeFromContents(obj)
    {
        /* remove the object from my contents list */
        contents -= obj;
    }

    /*
     *   Save my location for later restoration.  Returns a value suitable
     *   for passing to restoreLocation. 
     */
    saveLocation()
    {
        /* 
         *   I'm an ordinary object with only one location, so simply
         *   return the location 
         */
        return location;
    }

    /*
     *   Restore a previously saved location.  Does not trigger any side
     *   effects. 
     */
    restoreLocation(oldLoc)
    {
        /* move myself without side effects into my old container */
        baseMoveInto(oldLoc);
    }

    /*
     *   Move this object to a new container.  Before the move is actually
     *   performed, we notify the items in the movement path of the
     *   change, then we send notifyRemove and notifyInsert messages to
     *   the old and new containment trees, respectively.
     *   
     *   All notifications are sent before the object is actually moved.
     *   This means that the current game state at the time of the
     *   notifications reflects the state before the move.  
     */
    moveInto(newContainer)
    {
        /* notify the path */
        moveIntoNotifyPath(newContainer);

        /* perform the main moveInto operations */
        mainMoveInto(newContainer);
    }

    /*
     *   Move this object to a new container as part of travel.  This is
     *   almost the same as the regular moveInto(), but does not attempt
     *   to calculate and notify the sense path to the new location.  We
     *   omit the path notification because travel is done via travel
     *   connections, which are not necessarily the same as sense
     *   connections.  
     */
    moveIntoForTravel(newContainer)
    {
        /* 
         *   perform the main moveInto operations, omitting the path
         *   notification 
         */
        mainMoveInto(newContainer);
    }

    /*
     *   Main moveInto - this is the mid-level containment changer; this
     *   routine sends notifications to the old and new container, but
     *   doesn't notify anything along the connecting sense path. 
     */
    mainMoveInto(newContainer)
    {
        /* notify my container that I'm being removed */
        sendNotifyRemove(self, newContainer, &notifyRemove);

        /* notify my new container that I'm about to be added */
        if (newContainer != nil)
            newContainer.sendNotifyInsert(self, newContainer, &notifyInsert);

        /* notify myself that I'm about to be moved */
        notifyMoveInto(newContainer);

        /* perform the basic containment change */
        baseMoveInto(newContainer);

        /* note that I've been moved */
        moved = true;
    }

    /*
     *   Base moveInto - this is the low-level containment changer; this
     *   routine does not send any notifications to any containers, and
     *   does not mark the object as moved.  This form should be used only
     *   for internal library-initiated state changes, since it bypasses
     *   all of the normal side effects of moving an object to a new
     *   container.  
     */
    baseMoveInto(newContainer)
    {
        /* if I have a container, remove myself from its contents list */
        if (location != nil)
            location.removeFromContents(self);

        /* remember my new location */
        location = newContainer;

        /*
         *   if I'm not being moved into nil, add myself to the
         *   container's contents
         */
        if (location != nil)
            location.addToContents(self);
    }

    /*
     *   Notify each element of the move path of a moveInto operation. 
     */
    moveIntoNotifyPath(newContainer)
    {
        local path;
        
        /* calculate the path; if there isn't one, there's nothing to do */
        if ((path = getMovePathTo(newContainer)) == nil)
            return;

        /*
         *   We must fix up the path's final element, depending on whether
         *   we're moving into the new container from the outside or from
         *   the inside.  
         */
        if (isIn(newContainer))
        {
            /*
             *   We're already in the new container, so we're moving into
             *   the new container from the inside.  The final element of
             *   the path is the new container, but since we're stopping
             *   within the new container, we don't need to traverse out
             *   of it.  Simply remove the final two elements of the path,
             *   since we're not going to make this traversal when moving
             *   the object.  
             */
            path = path.sublist(1, path.length() - 2);
        }
        else
        {
            /*   
             *   We're moving into the new container from the outside.
             *   Since we calculated the path to the container, we must
             *   now add a final element to traverse into the container;
             *   the final object in the path doesn't matter, since it's
             *   just a placeholder for the new item inside the container 
             */
            path += [PathIn, nil];
        }

        /* traverse the path, sending a notification to each element */
        traversePath(path, new function(target, op) {
            /* notify this path element */
            target.notifyMoveViaPath(self, newContainer, op);

            /* continue the traversal */
            return true;
        });
    }

    /*
     *   Call a function on each container.  If we have no location, we
     *   won't invoke the function at all.  We'll invoke the function as
     *   follows:
     *   
     *   (func)(location, args...)  
     */
    forEachContainer(func, [args])
    {
        /* call the function on our location, if we have one */
        if (location != nil)
            (func)(location, args...);
    }

    /* 
     *   Call a function on each *connected* container.  For most objects,
     *   this is the same as forEachContainer, but objects that don't
     *   connect their containers for sense purposes would do nothing
     *   here. 
     */
    forEachConnectedContainer(func, [args])
    {
        /* by default, use the standard forEachContainer handling */
        forEachContainer(func, args...);
    }

    /* 
     *   Get a list of all of my connected containers.  This simply returns
     *   the list that forEachConnectedContainer() iterates over.  
     */
    getConnectedContainers()
    {
        local loc;

        /* 
         *   if I have a non-nil location, return a list containing it;
         *   otherwise, simply return an empty list 
         */
        return ((loc = location) == nil ? [] : [loc]);
    }

    /*
     *   Clone this object for inclusion in a MultiInstance's contents
     *   tree.  When we clone an instance object for a MultiInstance, we'll
     *   also clone its contents, and their contents, and so on.  This
     *   routine creates a private copy of all of our contents.  
     */
    cloneMultiInstanceContents()
    {
        local origContents;
        
        /* 
         *   Remember my current contents list, then clear it out.  We want
         *   a private copy of everything in our contents list, so we want
         *   to forget our references to the template contents and start
         *   from scratch.  
         */
        origContents = contents;
        contents = [];

        /* clone each entry in our contents list */
        foreach (local cur in origContents)
            cur.cloneForMultiInstanceContents(self);
    }

    /* 
     *   Create a clone for inclusion in MultiInstance contents.  We'll
     *   recursively clone our own contents. 
     */
    cloneForMultiInstanceContents(loc)
    {
        /* create a new instance of myself */
        local cl = createInstance();

        /* move it into the new location */
        cl.baseMoveInto(loc);

        /* clone its contents */
        cl.cloneMultiInstanceContents();
    }

    /*
     *   Determine if I'm a valid staging location for the given nested
     *   room destination 'dest'.  This is called when the actor is
     *   attempting to enter the nested room 'dest', and the travel
     *   handlers find that we're the staging location for the room.  (A
     *   "staging location" is the location the actor is required to
     *   occupy immediately before moving into the destination.)
     *   
     *   If this object is a valid staging location, the routine should
     *   simply do nothing.  If this object isn't valid as a staging
     *   location, this routine should display an appropriate message and
     *   terminate the command with 'exit'.
     *   
     *   An arbitrary object can't be a staging location, simply because
     *   an actor can't enter an arbitrary object.  So, by default, we'll
     *   explain that we can't enter this object.  If the destination is
     *   contained within us, we'll provide a more specific explanation
     *   indicating that the problem is that the destination is within us.
     */
    checkStagingLocation(dest)
    {
        /* 
         *   if the destination is within us, explain specifically that
         *   this is the problem 
         */
        if (dest.isIn(self))
            reportFailure(&invalidStagingContainerMsg, self, dest);
        else
            reportFailure(&invalidStagingLocationMsg, self);

        /* terminate the command */
        exit;
    }

    /*
     *   by default, objects don't accept commands 
     */
    acceptCommand(issuingActor)
    {
        /* report that we don't accept commands */
        gLibMessages.cannotTalkTo(self, issuingActor);

        /* tell the caller we don't accept commands */
        return nil;
    }

    /*
     *   by default, most objects are not logical targets for commands 
     */
    isLikelyCommandTarget = nil

    /*
     *   Get my extra scope items.  This is a list of any items that we
     *   want to add to command scope (i.e., the set of all items to which
     *   an actor is allowed to refer with noun phrases) when we are
     *   ourselves in the command scope.  Returns a list of the items to
     *   add (or just [] if there are no items to add).
     *   
     *   By default, we add nothing.  
     */
    getExtraScopeItems(actor) { return []; }

    /*
     *   Generate a lookup table of all of the objects connected by
     *   containment to this object.  This table includes all containment
     *   connections, even through closed containers and the like.
     *   
     *   The table is keyed by object; the associated values are
     *   meaningless, as all that matters is whether or not an object is
     *   in the table.  
     */
    connectionTable()
    {
        local tab;
        local cache;

        /* if we already have a cached connection list, return it */
        if ((cache = libGlobal.connectionCache) != nil
            && (tab = cache[self]) != nil)
            return tab;

        /* remember the point of view globally, in case anyone needs it */
        senseTmp.pointOfView = self;

        /* create a lookup table for the results */
        tab = new LookupTable(32, 64);

        /* add everything connected to me */
        addDirectConnections(tab);

        /* cache the list, if we caching is active */
        if (cache != nil)
            cache[self] = tab;

        /* return the table */
        return tab;
    }

    /*
     *   Add this item and its direct containment connections to the lookup
     *   table.  This must recursively add all connected objects that
     *   aren't already in the table.  
     */
    addDirectConnections(tab)
    {
        local cur;
        
        /* add myself */
        tab[self] = true;

        /* 
         *   Add my CollectiveGroup objects, if any.  We don't have to
         *   traverse into the CollectiveGroup objects, since they don't
         *   connect us or itself to anything else; our connection to a
         *   collective group is one-way.  
         */
        foreach (cur in collectiveGroups)
            tab[cur] = true;

        /* 
         *   Add my contents to the table.  Loop over our contents list,
         *   and add each item that's not already in the table, then
         *   recursively everything connected to that item.
         *   
         *   Note that we use a C-style 'for' loop to iterate over the list
         *   by index, rather than a more modern tads-style 'foreach' loop,
         *   purely for performance reasons: this code is called an awful
         *   lot, and the iteration by loop index is very slightly faster.
         *   A 'foreach' requires calling a couple of methods of an
         *   iterator object each time through the loop, whereas we can do
         *   the index-based 'for' loop entirely with cached local
         *   variables.  The performance difference is negligible for one
         *   time through any given loop; it only matters to us here
         *   because this routine tends to be invoked *extremely*
         *   frequently (an average of roughly 500 times per turn in the
         *   library sample game, for example).  Game authors are strongly
         *   advised to consider 'foreach' and 'for' equivalent in
         *   performance for most purposes - just choose the clearest
         *   construct for your situation.  
         */
        for (local clst = contents, local i = 1,
             local len = clst.length() ; i <= len ; ++i)
        {
            /* get the current item */
            cur = clst[i];
            
            /* if it's not already in the table, add it (recursively) */
            if (tab[cur] == nil)
                cur.addDirectConnections(tab);
        }

        /* add my container if it's not already in the table */
        if ((cur = location) != nil && tab[cur] == nil)
            cur.addDirectConnections(tab);
    }

    /*
     *   Try an implicit action that would remove this object as an
     *   obstructor to 'obj' from the perspective of the current actor in
     *   the given sense.  This is invoked when this object is acting as
     *   an obstructor between the current actor and 'obj' for the given
     *   sense, and the caller wants to perform a command that requires a
     *   clear sense path to the given object in the given sense.
     *   
     *   If it is possible to perform an implicit command that would clear
     *   the obstruction, try performing the command, and return true.
     *   Otherwise, simply return nil.  The usual implied command rules
     *   should be followed (which can be accomplished simply by using
     *   tryImplictAction() to execute any implied command).
     *   
     *   The particular type of command that would remove this obstructor
     *   can vary by obstructor class.  For a container, for example, an
     *   "open" command is the usual remedy.  
     */
    tryImplicitRemoveObstructor(sense, obj)
    {
        /* by default, we have no way of clearing our obstruction */
        return nil;
    }

    /*
     *   Display a message explaining why we are obstructing a sense path
     *   to the given object.
     */
    cannotReachObject(obj)
    {
        /* 
         *   Default objects have no particular obstructive capabilities,
         *   so we can't do anything but show the default message.  This
         *   is a last resort that should rarely be used; normally, we
         *   will be able to identify a specific obstructor that overrides
         *   this to explain precisely what kind of obstruction is
         *   involved.  
         */
        gLibMessages.cannotReachObject(obj);
    }

    /*
     *   Display a message explaining that the source of a sound cannot be
     *   seen because I am visually obstructing it.  By default, we show
     *   nothing at all; subclasses can override this to provide a better
     *   explanation when possible.  
     */
    cannotSeeSoundSource(obj) { }

    /* explain why we cannot see the source of an odor */
    cannotSeeSmellSource(obj) { }

    /*
     *   Get the path for this object reaching out and touching the given
     *   object.  This can be used to determine whether or not an actor
     *   can touch the given object.  
     */
    getTouchPathTo(obj)
    {
        local path;
        local key = [self, obj];
        local cache;
        local info;

        /* if we have a cache, try finding the data in the cache */
        if ((cache = libGlobal.canTouchCache) != nil
            && (info = cache[key]) != nil)
        {
            /* we have a cache entry - return the path from the cache */
            return info.touchPath;
        }

        /* select the path from here to the target object */
        path = selectPathTo(obj, &canTouchViaPath);

        /* if caching is enabled, add a cache entry for the path */
        if (cache != nil)
            cache[key] = new CanTouchInfo(path);

        /* return the path */
        return path;
    }

    /*
     *   Determine if I can touch the given object.  By default, we can
     *   always touch our immediate container; otherwise, we can touch
     *   anything with a touch path that we can traverse.
     */
    canTouch(obj)
    {
        local path;
        local result;
        local key = [self, obj];
        local cache;
        local info;

        /* if we have a cache, check the cache for the desired data */
        if ((cache = libGlobal.canTouchCache) != nil
            && (info = cache[key]) != nil)
        {
            /* if we cached the canTouch result, return it */
            if (info.propDefined(&canTouch))
                return info.canTouch;

            /* 
             *   we didn't calculate the canTouch result, but we at least
             *   have a cached path that we can avoid recalculating 
             */
            path = info.touchPath;
        }
        else
        {
            /* we don't have a cached path, so calculate it anew */
            path = getTouchPathTo(obj);
        }
        
        /* if there's no 'touch' path, we can't touch the object */
        if (path == nil)
        {
            /* there's no path */
            result = nil;
        }
        else
        {
            /* we have a path - check to see if we can reach along it */
            result = traversePath(path,
                {ele, op: ele.canTouchViaPath(self, obj, op)});
        }

        /* if caching is active, cache our result */
        if (cache != nil)
        {
            /* if we don't already have a cache entry, create one */
            if (info == nil)
                cache[key] = info = new CanTouchInfo(path);

            /* save our canTouch result in the cache entry */
            info.canTouch = result;
        }

        /* return the result */
        return result;
    }

    /*
     *   Find the object that prevents us from touching the given object.
     */
    findTouchObstructor(obj)
    {
        /* cache 'touch' sense path information */
        cacheSenseInfo(connectionTable(), touch);
        
        /* return the opaque obstructor for the sense of touch */
        return findOpaqueObstructor(touch, obj);
    }

    /*
     *   Get the path for moving this object from its present location to
     *   the given new container. 
     */
    getMovePathTo(newLoc)
    {
        /* 
         *   select the path from here to the new location, using the
         *   canMoveViaPath method to discriminate among different path
         *   possibilities. 
         */
        return selectPathTo(newLoc, &canMoveViaPath);
    }

    /*
     *   Get the path for throwing this object from its present location
     *   to the given target object. 
     */
    getThrowPathTo(newLoc)
    {
        /*
         *   select the path from here to the target, using the
         *   canThrowViaPath method to discriminate among different paths 
         */
        return selectPathTo(newLoc, &canThrowViaPath);
    }

    /*
     *   Determine if we can traverse self for moving the given object in
     *   the given manner.  This is used to determine if a containment
     *   connection path can be used to move an object to a new location.
     *   Returns a CheckStatus object indicating success if we can
     *   traverse self, failure if not.
     *   
     *   By default, we'll simply return a success indicator.  Subclasses
     *   might want to override this for particular conditions.  For
     *   example, containers would normally override this to return nil
     *   when attempting to move an object in or out of a closed
     *   container.  Some special containers might also want to override
     *   this to allow moving an object in or out only if the object is
     *   below a certain size threshold, for example.
     *   
     *   'obj' is the object being moved, and 'dest' is the destination of
     *   the move.
     *   
     *   'op' is one of the pathXxx operations - PathIn, PathOut, PathPeer
     *   - specifying what kind of movement is being attempted.  PathIn
     *   indicates that we're moving 'obj' from outside self to inside
     *   self; PathOut indicates the opposite.  PathPeer indicates that
     *   we're moving 'obj' entirely within self - this normally means
     *   that we've moved the object out of one of our contents and will
     *   move it into another of our contents.  
     */
    checkMoveViaPath(obj, dest, op) { return checkStatusSuccess; }

    /*
     *   Determine if we can traverse this object for throwing the given
     *   object in the given manner.  By default, this returns the same
     *   thing as canMoveViaPath, since throwing is in most cases the same
     *   as ordinary movement.  Objects can override this when throwing an
     *   object through this path element should be treated differently
     *   from ordinary movement.
     */
    checkThrowViaPath(obj, dest, op)
        { return checkMoveViaPath(obj, dest, op); }

    /*
     *   Determine if we can traverse self in the given manner for the
     *   purposes of 'obj' touching another object.  'obj' is usually an
     *   actor; this determines if 'obj' is allowed to reach through this
     *   path element on the way to touching another object.
     *   
     *   By default, this returns the same thing as canMoveViaPath, since
     *   touching is in most cases the same as ordinary movement of an
     *   object from one location to another.  Objects can overridet his
     *   when touching an object through this path element should be
     *   treated differently from moving an object.  
     */
    checkTouchViaPath(obj, dest, op)
        { return checkMoveViaPath(obj, dest, op); }

    /*
     *   Determine if we can traverse this object for moving the given
     *   object via a path.  Calls checkMoveViaPath(), and returns true if
     *   checkMoveViaPath() indicates success, nil if it indicates failure.
     *   
     *   Note that this method should generally not be overridden; only
     *   checkMoveViaPath() should usually need to be overridden.  
     */
    canMoveViaPath(obj, dest, op)
        { return checkMoveViaPath(obj, dest, op).isSuccess; }

    /* determine if we can throw an object via this path */
    canThrowViaPath(obj, dest, op)
        { return checkThrowViaPath(obj, dest, op).isSuccess; }

    /* determine if we can reach out and touch an object via this path */
    canTouchViaPath(obj, dest, op)
        { return checkTouchViaPath(obj, dest, op).isSuccess; }

    /*
     *   Check moving an object through this container via a path.  This
     *   method is called during moveInto to notify each element along a
     *   move path that the movement is about to occur.  We call
     *   checkMoveViaPath(); if it indicates failure, we'll report the
     *   failure encoded in the status object and terminate the command
     *   with 'exit'.  
     *   
     *   Note that this method should generally not be overridden; only
     *   checkMoveViaPath() should usually need to be overridden.  
     */
    notifyMoveViaPath(obj, dest, op)
    {
        local stat;

        /* check the move */
        stat = checkMoveViaPath(obj, dest, op);

        /* if it's a failure, report the failure message and terminate */
        if (!stat.isSuccess)
        {
            /* report the failure */
            reportFailure(stat.msgProp, stat.msgParams...);

            /* terminate the command */
            exit;
        }
    }

    /*
     *   Choose a path from this object to a given object.  If no paths
     *   are available, returns nil.  If any paths exist, we'll find the
     *   shortest usable one, calling the given property on each object in
     *   the path to determine if the traversals are allowed.
     *   
     *   If we can find a path, but there are no good paths, we'll return
     *   the shortest unusable path.  This can be useful for explaining
     *   why the traversal is impossible.  
     */
    selectPathTo(obj, traverseProp)
    {
        local allPaths;
        local goodPaths;
        local minPath;
        
        /* get the paths from here to the given object */
        allPaths = getAllPathsTo(obj);

        /* if we found no paths, the answer is obvious */
        if (allPaths.length() == 0)
            return nil;

        /* start off with an empty vector for the good paths */
        goodPaths = new Vector(allPaths.length());

        /* go through the paths and find the good ones */
        for (local i = 1, local len = allPaths.length() ; i <= len ; ++i)
        {
            local path = allPaths[i];
            local ok;
            
            /* 
             *   traverse the path, calling the traversal check property
             *   on each point in the path; if any check property returns
             *   nil, it means that the traversal isn't allowed at that
             *   point, so we can't use the path 
             */
            ok = true;
            traversePath(path, new function(target, op) {
                /*
                 *   Invoke the check property on this target.  If it
                 *   doesn't allow the traversal, the path is unusable. 
                 */
                if (target.(traverseProp)(self, obj, op))
                {
                    /* we're still okay - continue the path traversal */
                    return true;
                }
                else
                {
                    /* failed - note that the path is no good */
                    ok = nil;

                    /* there's no need to continue the path traversal */
                    return nil;
                }
            });

            /* 
             *   if we didn't find any objections to the path, add this to
             *   the list of good paths 
             */
            if (ok)
                goodPaths.append(path);
        }

        /* if there are no good paths, take the shortest bad path */
        if (goodPaths.length() == 0)
            goodPaths = allPaths;

        /* find the shortest of the paths we're still considering */
        minPath = nil;
        for (local i = 1, local len = goodPaths.length() ; i <= len ; ++i)
        {
            /* get the current path */
            local path = goodPaths[i];
            
            /* if this is the best so far, note it */
            if (minPath == nil || path.length() < minPath.length())
                minPath = path;
        }

        /* return the shortest good path */
        return minPath;
    }

    /*
     *   Traverse a containment connection path, calling the given
     *   function for each element.  In each call to the callback, 'obj'
     *   is the container object being traversed, and 'op' is the
     *   operation being used to traverse it.
     *   
     *   At each stage, the callback returns true to continue the
     *   traversal, nil if we are to stop the traversal.
     *   
     *   Returns nil if any callback returns nil, true if all callbacks
     *   return true.  
     */
    traversePath(path, func)
    {
        local len;
        local target;

        /* 
         *   if there's no path at all, there's nothing to do - simply
         *   return true in this case, because no traversal callback can
         *   fail when there are no traversal callbacks to begin with 
         */
        if (path == nil || (len = path.length()) == 0)
            return true;
        
        /* traverse from the path's starting point */
        if (path[1] != nil && !(func)(path[1], PathFrom))
            return nil;

        /* run through this path and see if the traversals are allowed */
        for (target = nil, local i = 2 ; i <= len ; i += 2)
        {
            local op;
            
            /* get the next traversal operation */
            op = path[i];
            
            /* check the next traversal to see if it's allowed */
            switch(op)
            {
            case PathIn:
                /* 
                 *   traverse in - notify the previous object, since it's
                 *   the container we're entering 
                 */
                target = path[i-1];
                break;

            case PathOut:
                /*
                 *   traversing out of the current container - tell the
                 *   next object, since it's the object we're leaving 
                 */
                target = path[i+1];
                break;

            case PathPeer:
                /*
                 *   traversing from one object to a containment peer -
                 *   notify the container in common to both peers 
                 */
                target = path[i-1].getCommonDirectContainer(path[i+1]);
                break;

            case PathThrough:
                /* 
                 *   traversing through a multi-location connector (the
                 *   previous and next object will always be the same in
                 *   this case, so it doesn't really matter which we
                 *   choose) 
                 */
                target = path[i-1];
                break;
            }

            /* call the traversal callback */
            if (target != nil && !(func)(target, op))
            {
                /* the callback told us not to continue */
                return nil;
            }
        }

        /* 
         *   Traverse to the path's ending point, if we haven't already.
         *   Some operations do traverse to the right-hand element
         *   (PathOut in particular), in which case we'll already have
         *   reached the target.  Most operations traverse the left-hand
         *   element, though, so in these cases we need to visit the last
         *   element explicitly. 
         */
        if (path[len] != nil
            && path[len] != target
            && !(func)(path[len], PathTo))
            return nil;

        /* all callbacks told us to continue */
        return true;
    }

    /*
     *   Build a vector containing all of the possible paths we can
     *   traverse to get from me to the given object.  The return value is
     *   a vector of paths; each path is a list of containment operations
     *   needed to get from here to there.
     *   
     *   Each path item in the vector is a list arranged like so:
     *   
     *   [obj, op, obj, op, obj]
     *   
     *   Each 'obj' is an object, and each 'op' is an operation enum
     *   (PathIn, PathOut, PathPeer) that specifies how to get from the
     *   preceding object to the next object.  The first object in the
     *   list is always the starting object (i.e., self), and the last is
     *   always the target object ('obj').  
     */
    getAllPathsTo(obj)
    {
        local vec;
        
        /* create an empty vector to hold the return set */
        vec = new Vector(10);

        /* look along each connection from me */
        buildContainmentPaths(vec, [self], obj);

        /* if there's no path, check the object for a special path */
        if (obj != nil && vec.length() == 0)
            obj.specialPathFrom(self, vec);

        /* return the vector */
        return vec;
    }

    /*
     *   Get a "special" path from the given starting object to me.
     *   src.getAllPathsTo(obj) calls this on 'obj' when it can't find any
     *   actual containment path from 'src' to 'obj'.  If desired, this
     *   method should add the path or paths to the vector 'vec'.
     *   
     *   By default, we do nothing at all.  The purpose of this routine is
     *   to allow special objects that exist outside the normal containment
     *   model to insinuate themselves into the sense model under special
     *   conditions of their choosing.  
     */
    specialPathFrom(src, vec) { }

    /*
     *   Service routine for getAllPathsTo: build a vector of the
     *   containment paths starting with this object.  
     */
    buildContainmentPaths(vec, pathHere, obj)
    {
        local i, len;
        local cur;
        
        /* scan each of our contents */
        for (i = 1, len = contents.length() ; i <= len ; ++i)
        {
            /* get the current item */
            cur = contents[i];

            /*
             *   If this item is the target, we've found what we're
             *   looking for - simply add the path to here to the return
             *   vector.
             *   
             *   Otherwise, if this item isn't already in the path,
             *   traverse into it; if it's already in the path, there's no
             *   need to look at it because we've already looked at it to
             *   get here 
             */
            if (cur == obj)
            {
                /* 
                 *   The path to here is the path to the target.  Append
                 *   the target object itself with an 'in' operation.
                 *   Before adding the path to the result set, normalize
                 *   it.  
                 */
                vec.append(normalizePath(pathHere + [PathIn, obj]));
            }
            else if (pathHere.indexOf(cur) == nil)
            {
                /* 
                 *   look at this item, adding it to the path with an
                 *   'enter child' traversal 
                 */
                cur.buildContainmentPaths(vec, pathHere + [PathIn, cur], obj);
            }
        }

        /* scan each of our locations */
        for (local clst = getConnectedContainers, i = 1, len = clst.length() ;
             i <= len ; ++i)
        {
            /* get the current item */
            cur = clst[i];

            /*
             *   If this item is the target, we've found what we're
             *   looking for.  Otherwise, if this item isn't already in
             *   the path, traverse into it 
             */
            if (cur == obj)
            {
                /* 
                 *   We have the path to the target.  Add the traversal to
                 *   the container to the path, normalize the path, and
                 *   add the path to the result set. 
                 */
                vec.append(normalizePath(pathHere + [PathOut, cur]));
            }
            else if (pathHere.indexOf(cur) == nil)
            {
                /* 
                 *   Look at this container, adding it to the path with an
                 *   'exit to container' traversal.
                 */
                cur.buildContainmentPaths(vec,
                                          pathHere + [PathOut, cur], obj);
            }
        }
    }

    /*
     *   "Normalize" a containment path to remove redundant containment
     *   traversals.
     *   
     *   First, we expand any sequence of in+out operations that take us
     *   out of one root-level containment tree and into another to
     *   include a "through" operation for the multi-location object being
     *   traversed.  For example, if 'a' and 'c' do not share a common
     *   container, then we will turn this:
     *   
     *     [a PathIn b PathOut c]
     *   
     *   into this:
     *   
     *     [a PathIn b PathThrough b PathOut c]
     *   
     *   This will ensure that when we traverse the path, we will
     *   explicitly traverse through the connector material of 'b'.
     *   
     *   Second, we replace any sequence of out+in operations through a
     *   common container with "peer" operations across the container's
     *   contents directly.  For example, a path that looks like this
     *   
     *     [a PathOut b PathIn c]
     *   
     *   will be normalized to this:
     *   
     *     [a PathPeer c]
     *   
     *   This means that we go directly from a to c, traversing through
     *   the fill medium of their common container 'b' but not actually
     *   traversing out of 'b' and back into it.  
     */
    normalizePath(path)
    {
        /* 
         *   Traverse the path looking for items to normalize with the
         *   (in-out)->(through) transformation.  Start at the second
         *   element, which is the first path operation code.  
         */
        for (local i = 2 ; i <= path.length() ; i += 2)
        {
            /*
             *   If we're on an 'in' operation, and an 'out' operation
             *   immediately follows, and the object before the 'in' and
             *   the object after the 'out' do not share a common
             *   container, we must add an explicit 'through' step for the
             *   multi-location connector being traversed. 
             */
            if (path[i] == PathIn
                && i + 2 <= path.length()
                && path[i+2] == PathOut
                && path[i-1].getCommonDirectContainer(path[i+3]) == nil)
            {
                /* we need to add a 'through' operation */
                path = path.sublist(1, i + 1)
                       + PathThrough
                       + path.sublist(i + 1);
            }
        }

        /* 
         *   make another pass, this time applying the (out-in)->peer
         *   transformation 
         */
        for (local i = 2 ; i <= path.length() ; i += 2)
        {
            /*
             *   If we're on an 'out' operation, and an 'in' operation
             *   immediately follows, we can collapse the out+in sequence
             *   to a single 'peer' operation.  
             */
            if (path[i] == PathOut
                && i + 2 <= path.length()
                && path[i+2] == PathIn)
            {
                /* 
                 *   this sequence can be collapsed to a single 'peer'
                 *   operation - rewrite the path accordingly 
                 */
                path = path.sublist(1, i - 1)
                       + PathPeer
                       + path.sublist(i + 3);
            }
        }

        /* return the normalized path */
        return path;
    }
    
    /*
     *   Get the visual sense information for this object from the current
     *   global point of view.  If we have explicit sense information set
     *   with setSenseInfo, we'll return that; otherwise, we'll calculate
     *   the current sense information for the given point of view.
     *   Returns a SenseInfo object giving the information.  
     */
    getVisualSenseInfo()
    {
        local infoTab;
        
        /* if we have explicit sense information already set, use it */
        if (explicitVisualSenseInfo != nil)
            return explicitVisualSenseInfo;

        /* calculate the sense information for the point of view */
        infoTab = getPOVDefault(gActor).visibleInfoTable();

        /* return the information on myself from the table */
        return infoTab[self];
    }

    /*
     *   Call a description method with explicit point-of-view and the
     *   related point-of-view sense information.  'pov' is the point of
     *   view object, which is usually an actor; 'senseInfo' is a
     *   SenseInfo object giving the sense information for this object,
     *   which we'll use instead of dynamically calculating the sense
     *   information for the duration of the routine called.  
     */
    withVisualSenseInfo(pov, senseInfo, methodToCall, [args])
    {
        local oldSenseInfo;
        
        /* push the sense information */
        oldSenseInfo = setVisualSenseInfo(senseInfo);

        /* push the point of view */
        pushPOV(pov, pov);

        /* make sure we restore the old value no matter how we leave */
        try
        {
            /* 
             *   call the method with the given arguments, and return the
             *   result 
             */
            return self.(methodToCall)(args...);
        }
        finally
        {
            /* restore the old point of view */
            popPOV();
            
            /* restore the old sense information */
            setVisualSenseInfo(oldSenseInfo);
        }
    }

    /* 
     *   Set the explicit visual sense information; if this is not nil,
     *   getVisualSenseInfo() will return this rather than calculating the
     *   live value.  Returns the old value, which is a SenseInfo or nil.  
     */
    setVisualSenseInfo(info)
    {
        local oldInfo;

        /* remember the old value */
        oldInfo = explicitVisualSenseInfo;

        /* remember the new value */
        explicitVisualSenseInfo = info;

        /* return the original value */
        return oldInfo;
    }

    /* current explicit visual sense information overriding live value */
    explicitVisualSenseInfo = nil

    /*
     *   Determine how accessible my contents are to a sense.  Any items
     *   contained within a Thing are considered external features of the
     *   Thing, hence they are transparently accessible to all senses.
     */
    transSensingIn(sense) { return transparent; }

    /*
     *   Determine how accessible peers of this object are to the contents
     *   of this object, via a given sense.  This has the same meaning as
     *   transSensingIn(), but in the opposite direction: whereas
     *   transSensingIn() determines how accessible my contents are from
     *   the outside, this determines how accessible the outside is from
     *   the contents.
     *
     *   By default, we simply return the same thing as transSensingIn(),
     *   since most containers are symmetrical for sense passing from
     *   inside to outside or outside to inside.  However, we distinguish
     *   this as a separate method so that asymmetrical containers can
     *   have different effects in the different directions; for example,
     *   a box made of one-way mirrors might be transparent when looking
     *   from the inside to the outside, but opaque in the other
     *   direction.
     */
    transSensingOut(sense) { return transSensingIn(sense); }

    /*
     *   Get my "fill medium."  This is an object of class FillMedium that
     *   permeates our interior, such as fog or smoke.  This object
     *   affects the transmission of senses from one of our children to
     *   another, or between our interior and exterior.
     *   
     *   Note that the FillMedium object is usually a regular object in
     *   scope, so that the player can refer to the fill medium.  For
     *   example, if a room is filled with fog, the player might want to
     *   be able to refer to the fog in a command.
     *   
     *   By default, our medium is the same as our parent's medium, on the
     *   assumption that fill media diffuse throughout the location's
     *   interior.  Note, though, that Container overrides this so that a
     *   closed Container is isolated from its parent's fill medium -
     *   think of a closed bottle within a room filled with smoke.
     *   However, a fill medium doesn't expand from a child into its
     *   containers - it only diffuses into nested containers, never out.
     *   
     *   An object at the outermost containment level has no fill medium
     *   by default, so we return nil if our location is nil.
     *   
     *   Note that, unlike the "surface" material, the fill medium is
     *   assumed to be isotropic - that is, it has the same sense-passing
     *   characteristics regardless of the direction in which the energy
     *   is traversing the medium.  Since we don't have any information in
     *   our containment model about the positions of our objects relative
     *   to one another, we have no way to express anisotropy in the fill
     *   medium among our children anyway.
     *   
     *   Note further that energy going in or out of this object must
     *   traverse both the fill medium and the surface of the object
     *   itself.  Since we have no other information on the relative
     *   positions of our contents, we can only assume that they're
     *   uniformly distributed through our interior, so it is necessary to
     *   traverse the same amount of fill material to go from one child to
     *   any other or from a child to our inner surface.
     *   
     *   As a sense is transmitted, several consecutive traversals of a
     *   single fill material (i.e., a single object reference) will be
     *   treated as a single traversal of the material.  Since we don't
     *   have a notion of distance in our containment model, we can't
     *   assume that we cover a certain amount of distance just because we
     *   traverse a certain number of containment levels.  So, if we have
     *   three nested containment levels all inheriting a single fill
     *   material from their outermost parent, traversing from the inner
     *   container to the outer container will count as a single traversal
     *   of the material.  
     */
    fillMedium()
    {
        local loc;
        
        return ((loc = location) != nil ? loc.fillMedium() : nil);
    }

    /*
     *   Can I see/hear/smell the given object?  By default, an object can
     *   "see" (or "hear", etc) another if there's a clear path in the
     *   corresponding basic sense to the other object.  Note that actors
     *   override this, because they have a subjective view of the senses:
     *   an actor might see in a special infrared vision sense rather than
     *   (or in addition to) the normal 'sight' sense, for example.  
     */
    canSee(obj) { return senseObj(sight, obj).trans != opaque; }
    canHear(obj) { return senseObj(sound, obj).trans != opaque; }
    canSmell(obj) { return senseObj(smell, obj).trans != opaque; }

    /* can the given actor see/hear/smell/touch me? */
    canBeSeenBy(actor) { return actor.canSee(self); }
    canBeHeardBy(actor) { return actor.canHear(self); }
    canBeSmelledBy(actor) { return actor.canSmell(self); }
    canBeTouchedBy(actor) { return actor.canTouch(self); }

    /* can the player character see/hear/smell/touch me? */
    canBeSeen = (canBeSeenBy(gPlayerChar))
    canBeHeard = (canBeHeardBy(gPlayerChar))
    canBeSmelled = (canBeSmelledBy(gPlayerChar))
    canBeTouched = (canBeTouchedBy(gPlayerChar))

    /*
     *   Am I occluded by the given Occluder when viewed in the given sense
     *   from the given point of view?  The default Occluder implementation
     *   calls this on each object involved in the sense path to determine
     *   if it should occlude the object.  Returns true if we're occluded
     *   by the given Occluder, nil if not.  By default, we simply return
     *   nil to indicate that we're not occluded.  
     */
    isOccludedBy(occluder, sense, pov) { return nil; }

    /*
     *   Determine how well I can sense the given object.  Returns a
     *   SenseInfo object describing the sense path from my point of view
     *   to the object.
     *   
     *   Note that, because 'distant', 'attenuated', and 'obscured'
     *   transparency levels always compound (with one another and with
     *   themselves) to opaque, there will never be more than a single
     *   obstructor in a path, because any path with two or more
     *   obstructors would be an opaque path, and hence not a path at all.
     */
    senseObj(sense, obj)
    {
        local info;

        /* get the sense information from the table */
        info = senseInfoTable(sense)[obj];

        /* if we couldn't find the object, return an 'opaque' indication */
        if (info == nil)
            info = new SenseInfo(obj, opaque, nil, 0);

        /* return the sense data descriptor */
        return info;
    }

    /*
     *   Find an opaque obstructor.  This can be called immediately after
     *   calling senseObj() when senseObj() indicates that the object is
     *   opaquely obscured.  We will find the nearest (by containment)
     *   object where the sense status is non-opaque, and we'll return
     *   that object.
     *   
     *   senseObj() by itself does not determine the obstructor when the
     *   sense path is opaque, because doing so requires extra work.  The
     *   sense path calculator that senseObj() uses cuts off its search
     *   whenever it reaches an opaque point, because beyond that point
     *   nothing can be sensed.
     *   
     *   This can only be called immediately after calling senseObj()
     *   because we re-use the same cached sense path information that
     *   senseObj() uses.  
     */
    findOpaqueObstructor(sense, obj)
    {
        local path;
        
        /* get all of the paths from here to there */
        path = getAllPathsTo(obj);

        /* 
         *   if there are no paths, we won't be able to find a specific
         *   obstructor - the object simply isn't connected to us at all 
         */
        if (path == nil)
            return nil;

        /* 
         *   Arbitrarily take the first path - there must be an opaque
         *   obstructor on every path or we never would have been called
         *   in the first place.  One opaque obstructor is as good as any
         *   other for our purposes.  
         */
        path = path[1];

        /* 
         *   The last thing in the list that can be sensed is the opaque
         *   obstructor.  Note that the path entries alternate between
         *   objects and traversal operations.  
         */
        for (local i = 3, local len = path.length() ; i <= len ; i += 2)
        {
            local obj;
            local trans;
            local ambient;

            /* get this object */
            obj = path[i];

            /* 
             *   get the appropriate sense direction - if the path takes
             *   us out, look at the interior sense data for the object;
             *   otherwise look at its exterior sense data 
             */
            if (path[i-1] == PathOut)
            {
                /* we're looking outward, so use the interior data */
                trans = obj.tmpTransWithin_;
                ambient = obj.tmpAmbientWithin_;
            }
            else
            {
                /* we're looking inward, so use the exterior data */
                trans = obj.tmpTrans_;
                ambient = obj.tmpAmbient_;
            }

            /* 
             *   if this item cannot be sensed, the previous item is the
             *   opaque obstructor 
             */
            if (!obj.canBeSensed(sense, trans, ambient))
            {
                /* can't sense it - the previous item is the obstructor */
                return path[i-2];
            }
        }

        /* we didn't find any obstructor */
        return nil;
    }
    
    /*
     *   Build a list of full information on all of the objects reachable
     *   from me through the given sense, along with full information for
     *   each object's sense characteristics.
     *   
     *   We return a lookup table of each object that can be sensed (in
     *   the given sense) from the point of view of 'self'.  The key for
     *   each entry in the table is an object, and the corresponding value
     *   is a SenseInfo object describing the sense conditions for the
     *   object.  
     */
    senseInfoTable(sense)
    {
        local objs;
        local tab;
        local siz;
        local cache;
        local key = [self, sense];

        /* if we have cached sense information, simply return it */
        if ((cache = libGlobal.senseCache) != nil
            && (tab = cache[key]) != nil)
            return tab;
        
        /* 
         *   get the list of objects connected to us by containment -
         *   since the only way senses can travel between objects is via
         *   containment relationships, this is the complete set of
         *   objects that could be connected to us by any senses 
         */
        objs = connectionTable();

        /* we're the point of view for this path calculation */
        senseTmp.pointOfView = self;

        /* cache the sensory information for all of these objects */
        cacheSenseInfo(objs, sense);

        /* build a table of all of the objects we can reach */
        siz = objs.getEntryCount();
        tab = new LookupTable(32, siz == 0 ? 32 : siz);
        objs.forEachAssoc(new function(cur, val)
        {
            /* add this object's sense information to the table */
            cur.addToSenseInfoTable(sense, tab);
        });

        /* add the information vector to the sense cache */
        if (cache != nil)
            cache[key] = tab;

        /* return the result table */
        return tab;
    }

    /*
     *   Add myself to a sense info table.  This is called by
     *   senseInfoTable() for each object connected by containment to the
     *   source object 'src', after we've fully traversed the containment
     *   tree to initialize our current-sense properties (tmpAmbient_,
     *   tmpTrans_, etc).
     *   
     *   Our job is to figure out if 'src' can sense us, and add a
     *   SenseInfo entry to the LookupTable 'tab' (which is keyed by
     *   object, hence our key is simply 'self') if 'src' can indeed sense
     *   us.
     *   
     *   Note that an object that wants to set up its own special sensory
     *   data can do so by overriding this.  This routine will only be
     *   called on objects connected to 'src' by containment, though, so if
     *   an object overrides this in order to implement a special sensory
     *   system that's outside of the normal containment model, it must
     *   somehow ensure that it gets included in the containment connection
     *   table in the first place.  
     */
    addToSenseInfoTable(sense, tab)
    {
        local trans;
        local ambient;
        local obs;

        /* 
         *   consider the appropriate set of data, depending on whether the
         *   source is looking out from within me, or in from outside of me
         */
        if (tmpPathIsIn_)
        {
            /* we're looking in from without, so use our exterior data */
            trans = tmpTrans_;
            ambient = tmpAmbient_;
            obs = tmpObstructor_;
        }
        else
        {
            /* we're looking out from within, so use our interior data */
            trans = tmpTransWithin_;
            ambient = tmpAmbientWithin_;
            obs = tmpObstructorWithin_;
        }

        /* if the source can sense us, add ourself to the data */
        if (canBeSensed(sense, trans, ambient))
            tab[self] = new SenseInfo(self, trans, obs, ambient);
    }

    /*
     *   Build a list of the objects reachable from me through the given
     *   sense and with a presence in the sense. 
     */
    sensePresenceList(sense)
    {
        local infoTab;
        
        /* get the full sense list */
        infoTab = senseInfoTable(sense);

        /* 
         *   return only the subset of items that have a presence in this
         *   sense, and return only the items themselves, not the
         *   SenseInfo objects 
         */
        return senseInfoTableSubset(infoTab,
            {obj, info: obj.(sense.presenceProp)});
    }

    /*
     *   Determine the highest ambient sense level at this object for any
     *   of the given senses.
     *   
     *   Note that this method changes certain global variables used during
     *   sense and scope calculations.  Because of this, be careful not to
     *   call this method *during* sense or scope calculations.  In
     *   particular, don't call this from an object's canBeSensed() method
     *   or anything it calls.  For example, don't call this from a
     *   Hidden.discovered method.  
     */
    senseAmbientMax(senses)
    {
        local objs;
        local maxSoFar;

        /* we don't have any level so far */
        maxSoFar = 0;
        
        /* get the table of connected objects */
        objs = connectionTable();

        /* go through each sense */
        for (local i = 1, local lst = senses, local len = lst.length() ;
             i <= len ; ++i)
        {
            /* 
             *   cache the ambient level for this sense for everything
             *   connected by containment 
             */
            cacheAmbientInfo(objs, lst[i]);

            /* 
             *   if our cached level for this sense is the highest so far,
             *   remember it 
             */
            if (tmpAmbient_ > maxSoFar)
                maxSoFar = tmpAmbient_;
        }

        /* return the highest level we found */
        return maxSoFar;
    }

    /*
     *   Cache sensory information for all objects in the given list from
     *   the point of view of self.  This caches the ambient energy level
     *   at each object, if the sense uses ambient energy, and the
     *   transparency and obstructor on the best path in the sense to the
     *   object.  'objs' is the connection table, as generated by
     *   connectionTable().  
     */
    cacheSenseInfo(objs, sense)
    {
        /* clear out the end-of-calculation notification list */
        local nlst = senseTmp.notifyList;
        if (nlst.length() != 0)
            nlst.removeRange(1, nlst.length());

        /* first, calculate the ambient energy level at each object */
        cacheAmbientInfo(objs, sense);

        /* next, cache the sense path from here to each object */
        cacheSensePath(sense);

        /* notify each object in the notification list */
        for (local i = 1, local len = nlst.length() ; i <= len ; ++i)
            nlst[i].finishSensePath(objs, sense);
    }

    /*
     *   Cache the ambient energy level at each object in the table.  The
     *   list must include everything connected by containment.  
     */
    cacheAmbientInfo(objs, sense)
    {
        local aprop;
        
        /* 
         *   if this sense has ambience, transmit energy from sources to
         *   all reachable objects; otherwise, just clear out the sense
         *   data 
         */
        if ((aprop = sense.ambienceProp) != nil)
        {
            local sources;

            /* create a vector to hold the set of ambient energy sources */
            sources = new Vector(16);

            /* 
             *   Clear out any cached sensory information from past
             *   calculations, and note objects that have ambient energy to
             *   propagate.  
             */
            objs.forEachAssoc(new function(cur, val)
            {
                /* clear old sensory information for this object */
                cur.clearSenseInfo();

                /* if it's an energy source, note it */
                if (cur.(aprop) != 0)
                    sources.append(cur);
            });

            /* 
             *   Calculate the ambient energy level at each object.  To do
             *   this, start at each energy source and transmit its energy
             *   to all objects within reach of the sense. 
             */
            for (local i = 1, local len = sources.length() ; i <= len ; ++i)
            {
                /* get this item */
                local cur = sources[i];
                
                /* if this item transmits energy, process it */
                if (cur.(aprop) != 0)
                    cur.transmitAmbient(sense);
            }
        }
        else
        {
            /* 
             *   this sense doesn't use ambience - all we need to do is
             *   clear out any old sense data for this object 
             */
            objs.forEachAssoc({cur, val: cur.clearSenseInfo()});
        }
    }

    /*
     *   Transmit my radiating energy to everything within reach of the
     *   sense.  
     */
    transmitAmbient(sense)
    {
        local ambient;
        
        /* get the energy level I'm transmitting */
        ambient = self.(sense.ambienceProp);

        /* if this is greater than my ambient level so far, take it */
        if (ambient > tmpAmbient_)
        {
            /* 
             *   remember the new settings: start me with my own ambience,
             *   and with no fill medium in the way (there's nothing
             *   between me and myself, so I shine on myself with full
             *   force and with no intervening fill medium) 
             */
            tmpAmbient_ = ambient;
            tmpAmbientFill_ = nil;

            /* 
             *   if the level is at least 2, transmit to adjacent objects
             *   (level 1 is self-illumination only, so we don't transmit
             *   to anything else) 
             */
            if (ambient >= 2)
            {
                /* transmit to my containers */
                shineOnLoc(sense, ambient, nil);

                /* shine on my contents */
                shineOnContents(sense, ambient, nil);
            }
        }

        /* 
         *   Apply our interior self-illumination if necessary.  If we're
         *   self-illuminating, and our interior surface doesn't already
         *   have higher ambient energy from another source, then the
         *   ambient energy at our inner surface is simply 1, since we
         *   self-illuminate inside as well as outside. 
         */
        if (ambient == 1 && ambient > tmpAmbientWithin_)
            tmpAmbientWithin_ = ambient;
    }

    /*
     *   Transmit ambient energy to my location or locations. 
     */
    shineOnLoc(sense, ambient, fill)
    {
        /* 
         *   shine on my container, if I have one, and its immediate
         *   children 
         */
        if (location != nil)
            location.shineFromWithin(self, sense, ambient, fill);
    }

    /*
     *   Shine ambient energy at my surface onto my contents. 
     */
    shineOnContents(sense, ambient, fill)
    {
        local levelWithin;
        
        /*
         *   Figure the level of energy to transmit to my contents.  To
         *   reach my contents, the ambient energy here must traverse our
         *   surface.  Since we want to know what the ambient light at our
         *   surface looks like when viewed from our interior, we must use
         *   the sensing-out transparency. 
         */
        levelWithin = adjustBrightness(ambient, transSensingOut(sense));

        /* 
         *   Remember our ambient level at our inner surface, if the
         *   adjusted transmission is higher than our tentative ambient
         *   level already set from another source or path.  Note that the
         *   tmpAmbientWithin_ caches the ambient level at our inner
         *   surface, which comes before we adjust for our fill medium
         *   (because our fill medium is enclosed by our inner surface).  
         */
        if (levelWithin >= 2 && levelWithin > tmpAmbientWithin_)
        {
            local fillWithin;

            /* note my new interior ambience */
            tmpAmbientWithin_ = levelWithin;

            /*
             *   If there's a new fill material in my interior that the
             *   ambient energy here hasn't already just traversed, we must
             *   further adjust the ambient level in my interior by the
             *   fill transparency.  
             */
            fillWithin = tmpFillMedium_;
            if (fillWithin != fill && fillWithin != nil)
            {
                /* 
                 *   we're traversing a new fill material - adjust the
                 *   brightness further for the fill material 
                 */
                levelWithin = adjustBrightness(levelWithin,
                                               fillWithin.senseThru(sense));
            }
            
            /* if that leaves any ambience in my interior, transmit it */
            if (levelWithin >= 2)
            {
                /* shine on each object directly within me */
                for (local clst = contents, local i = 1,
                     local len = clst.length() ; i <= len ; ++i)
                {
                    /* transmit the ambient energy to this child item */
                    clst[i].shineFromWithout(self, sense,
                                             levelWithin, fillWithin);
                }
            }
        }
    }

    /*
     *   Transmit ambient energy from an object within me.  This transmits
     *   to my outer surface, and also to my own immediate children - in
     *   other words, to the peers of the child shining on us.  We need to
     *   transmit to the source's peers right now, because it might
     *   degrade the ambient energy to go out through our surface.  
     */
    shineFromWithin(fromChild, sense, ambient, fill)
    {
        local levelWithout;
        local levelWithin;
        local fillWithin;
        
        /*
         *   Calculate the change in energy as the sense makes its way to
         *   our "inner surface," and to peers of the sender - in both
         *   cases, the energy must traverse our fill medium to get to the
         *   next object.
         *   
         *   As always, energy must never traverse a single fill medium
         *   more than once consecutively, so if the last fill material is
         *   the same as the fill material here, no further adjustment is
         *   necessary for another traversal of the same material.  
         */
        levelWithin = ambient;
        fillWithin = tmpFillMedium_;
        if (fillWithin != fill && fillWithin != nil)
        {
            /* adjust the brightness for the fill traversal */
            levelWithin = adjustBrightness(levelWithin,
                                           fillWithin.senseThru(sense));
        }

        /* if there's no energy left to transmit, we're done */
        if (levelWithin < 2)
            return;

        /* 
         *   Since we're transmitting the energy from within us, calculate
         *   any attenuation as the energy goes from our inner surface to
         *   our outer surface - this is the energy that makes it through
         *   to our exterior and thus is the new ambient level at our
         *   surface.  We must calculate the attenuation that a viewer
         *   from outside sees looking at an energy source within us, so
         *   we must use the sensing-in transparency.
         *   
         *   Note that we start here with the level within that we've
         *   already calculated: we assume that the energy from our child
         *   must first traverse our interior medium before reaching our
         *   "inner surface," at which point it must then further traverse
         *   our surface material to reach our "outer surface," at which
         *   point it's the ambient level at our exterior.  
         */
        levelWithout = adjustBrightness(levelWithin, transSensingIn(sense));

        /* 
         *   The level at our outer surface is the new ambient level for
         *   this object.  The last fill material traversed is the fill
         *   material within me.  If it's the best yet, take it.
         */
        if (levelWithout > tmpAmbient_)
        {
            /* it's the best so far - cache it */
            tmpAmbient_ = levelWithout;
            tmpAmbientFill_ = fillWithin;

            /* transmit to our containers */
            shineOnLoc(sense, levelWithout, fillWithin);
        }

        /* transmit the level within to each peer of the sender */
        if (levelWithin > tmpAmbientWithin_)
        {
            /* note our level within */
            tmpAmbientWithin_ = levelWithin;

            /* transmit to each of our children */
            for (local i = 1, local clst = contents,
                 local len = clst.length() ; i <= len ; ++i)
            {
                /* get the current item */
                local cur = clst[i];

                /* if it's not the source, shine on it */
                if (cur != fromChild)
                    cur.shineFromWithout(self, sense,
                                         levelWithin, fillWithin);
            }
        }
    }

    /*
     *   Transmit ambient energy from an object immediately containing me.
     */
    shineFromWithout(fromParent, sense, level, fill)
    {
        /* if this is the best level yet, take it and transmit it */
        if (level > tmpAmbient_)
        {
            /* cache this new best level */
            tmpAmbient_ = level;
            tmpAmbientFill_ = fill;

            /* transmit it down to my children */
            shineOnContents(sense, level, fill);
        }
    }

    /*
     *   Cache the sense path for each object reachable from this point of
     *   view.  Fills in tmpTrans_ and tmpObstructor_ for each object with
     *   the best transparency path from the object to me.  
     */
    cacheSensePath(sense)
    {
        /* the view from me to myself is unobstructed */
        tmpPathIsIn_ = true;
        tmpTrans_ = transparent;
        tmpTransWithin_ = transparent;
        tmpObstructor_ = nil;
        tmpObstructorWithin_ = nil;

        /* build a path to my containers */
        sensePathToLoc(sense, transparent, nil, nil);

        /* build a path to my contents */
        sensePathToContents(sense, transparent, nil, nil);
    }

    /*
     *   Build a path to my location or locations 
     */
    sensePathToLoc(sense, trans, obs, fill)
    {
        /* 
         *   proceed to my container, if I have one, and its immediate
         *   children 
         */
        if (location != nil)
            location.sensePathFromWithin(self, sense, trans, obs, fill);
    }

    /*
     *   Build a sense path to my contents 
     */
    sensePathToContents(sense, trans, obs, fill)
    {
        local transWithin;
        local obsWithin;
        local fillWithin;

        /*
         *   Figure the transparency to my contents.  To reach my
         *   contents, we must look in through our surface.  If we change
         *   the transparency, we're the new obstructor.  
         */
        transWithin = transparencyAdd(trans, transSensingIn(sense));
        obsWithin = (trans == transWithin ? obs : self);

        /*
         *   If there's a new fill material in my interior that we haven't
         *   already just traversed, we must further adjust the
         *   transparency by the fill transparency. 
         */
        fillWithin = tmpFillMedium_;
        if (fillWithin != fill && fillWithin != nil)
        {
            local oldTransWithin = transWithin;
            
            /* we're traversing a new fill material */
            transWithin = transparencyAdd(transWithin,
                                          fillWithin.senseThru(sense));
            if (transWithin != oldTransWithin)
                obsWithin = fill;
        }

        /* if the path isn't opaque, proceed to my contents */
        if (transWithin != opaque)
        {
            /* build a path to each child */
            for (local clst = contents,
                 local i = 1, local len = clst.length() ; i <= len ; ++i)
            {
                /* build a path to this child */
                clst[i].sensePathFromWithout(self, sense, transWithin,
                                             obsWithin, fillWithin);
            }
        }
    }

    /*
     *   Build a path from an object within me. 
     */
    sensePathFromWithin(fromChild, sense, trans, obs, fill)
    {
        local transWithin;
        local fillWithin;
        local transWithout;
        local obsWithout;

        /* 
         *   Calculate the transparency change along the path from the
         *   child to our "inner surface" and to peers of the sender - in
         *   both cases, we must traverse the fill material. 
         *   
         *   As always, energy must never traverse a single fill medium
         *   more than once consecutively, so if the last fill material is
         *   the same as the fill material here, no further adjustment is
         *   necessary for another traversal of the same material.  
         */
        transWithin = trans;
        fillWithin = tmpFillMedium_;
        if (fillWithin != fill && fillWithin != nil)
        {
            /* adjust for traversing a new fill material */
            transWithin = transparencyAdd(transWithin,
                                          fillWithin.senseThru(sense));
            if (transWithin != trans)
                obs = fillWithin;
        }

        /* if we're opaque at this point, we're done */
        if (transWithin == opaque)
            return;

        /*
         *   Calculate the transparency going from our inner surface to
         *   our outer surface - we must traverse our own material to
         *   travel this segment. 
         */
        transWithout = transparencyAdd(transWithin, transSensingOut(sense));
        obsWithout = (transWithout != transWithin ? self : obs);

        /*
         *   We now have the path to our outer surface.  The last fill
         *   material traversed is the fill material within me.  If this
         *   is the best yet, remember it.  
         */
        if (transparencyCompare(transWithout, tmpTrans_) > 0)
        {
            /* it's the best so far - cache it */
            tmpTrans_ = transWithout;
            tmpObstructor_ = obsWithout;

            /* we're coming to this object from within */
            tmpPathIsIn_ = nil;

            /* transmit to our containers */
            sensePathToLoc(sense, transWithout, obsWithout, fillWithin);
        }

        /* 
         *   if this is the best interior transparency yet, build a path
         *   to each peer of the sender 
         */
        if (transparencyCompare(transWithin, tmpTransWithin_) > 0)
        {
            /* it's the best so far - cache it */
            tmpTransWithin_ = transWithin;
            tmpObstructorWithin_ = obs;

            /* we're coming to this object from within */
            tmpPathIsIn_ = nil;
            
            /* build a path to each peer of the sender */
            for (local i = 1, local clst = contents,
                 local len = clst.length() ; i <= len ; ++i)
            {
                /* get this item */
                local cur = clst[i];
                
                /* if it's not the source, build a path to it */
                if (cur != fromChild)
                    cur.sensePathFromWithout(self, sense, transWithin,
                                             obs, fillWithin);
            }
        }
    }

    /*
     *   Build a path from an object immediately containing me. 
     */
    sensePathFromWithout(fromParent, sense, trans, obs, fill)
    {
        /* if this is the best level yet, take it and keep going */
        if (transparencyCompare(trans, tmpTrans_) > 0)
        {
            /* remember this new best level */
            tmpTrans_ = trans;
            tmpObstructor_ = obs;

            /* we're coming to this object from outside */
            tmpPathIsIn_ = true;

            /* build a path down into my children */
            sensePathToContents(sense, trans, obs, fill);
        }
    }
    

    /*
     *   Clear the sensory scratch-pad properties, in preparation for a
     *   sensory calculation pass. 
     */
    clearSenseInfo()
    {
        tmpPathIsIn_ = true;
        tmpAmbient_ = 0;
        tmpAmbientWithin_ = 0;
        tmpAmbientFill_ = nil;
        tmpTrans_ = opaque;
        tmpTransWithin_ = opaque;
        tmpObstructor_ = nil;
        tmpObstructorWithin_ = nil;

        /* pre-calculate my fill medium */
        tmpFillMedium_ = fillMedium();
    }

    /* 
     *   Scratch-pad for calculating ambient energy level - valid only
     *   after calcAmbience and until the game state changes in any way.
     *   This is for internal use within the sense propagation methods
     *   only.  
     */
    tmpAmbient_ = 0

    /*
     *   Last fill material traversed by the ambient sense energy in
     *   tmpAmbient_.  We must keep track of this so that we can treat
     *   consecutive traversals of the same fill material as equivalent to
     *   a single traversal.  
     */
    tmpAmbientFill_ = nil

    /*
     *   Scrach-pad for the best transparency level to this object from
     *   the current point of view.  This is used during cacheSenseInfo to
     *   keep track of the sense path to this object. 
     */
    tmpTrans_ = opaque

    /*
     *   Scratch-pad for the obstructor that contributed to a
     *   non-transparent path to this object in tmpTrans_. 
     */
    tmpObstructor_ = nil

    /*
     *   Scratch-pads for the ambient level, best transparency, and
     *   obstructor to our *interior* surface.  We keep track of these
     *   separately from the exterior data so that we can tell what we
     *   look like from the persepctive of an object within us.  
     */
    tmpAmbientWithin_ = 0
    tmpTransWithin_ = opaque
    tmpObstructorWithin_ = nil

    /*
     *   Scratch-pad for the sense path direction at this object.  If this
     *   is true, the sense path is pointing inward - that is, the path
     *   from the source object to here is entering from outside me.
     *   Otherwise, the sense path is pointing outward.  
     */
    tmpPathIsIn_ = true

    /*
     *   My fill medium.  We cache this during each sense path
     *   calculation, since the fill medium calculation often requires
     *   traversing several containment levels. 
     */
    tmpFillMedium_ = nil

    /*
     *   Merge two senseInfoTable tables.  Merges the second table into
     *   the first.  If an object appears only in the first table, the
     *   entry is left unchanged; if an object appears only in the second
     *   table, the entry is added to the first table.  If an object
     *   appears in both tables, we'll keep the one with better detail or
     *   brightness, adding it to the first table if it's the one in the
     *   second table.  
     */
    mergeSenseInfoTable(a, b)
    {
        /* if either table is nil, return the other table */
        if (a == nil)
            return b;
        else if (b == nil)
            return a;
        
        /* 
         *   iterate over the second table, merging each item from the
         *   second table into the corresponding item in the first table 
         */
        b.forEachAssoc({obj, info: a[obj] = mergeSenseInfo(a[obj], info)});

        /* return the merged first table */
        return a;
    }

    /*
     *   Merge two SenseInfo items.  Chooses the "better" of the two items
     *   and returns it, where "better" is defined as more transparent,
     *   or, transparencies being equal, brighter in ambient energy.  
     */
    mergeSenseInfo(a, b)
    {
        /* if one or the other is nil, return the non-nil one */
        if (a == nil)
            return b;
        if (b == nil)
            return a;

        /*
         *   Both items are non-nil, so keep the better of the two.  If
         *   the transparencies aren't equal, keep the one that's more
         *   transparent.  Otherwise, keep the one with higher ambient
         *   energy. 
         */
        if (a.trans == b.trans)
        {
            /* 
             *   The transparencies are equal, so choose the one with
             *   higher ambient energy.  If those are the same,
             *   arbitrarily keep the first one. 
             */
            if (a.ambient >= b.ambient)
                return a;
            else
                return b;
        }
        else
        {
            /* 
             *   The transparencies are unequal, so pick the one with
             *   better transparency. 
             */
            if (transparencyCompare(a.trans, b.trans) < 0)
                return b;
            else
                return a;
        }
    }

    /*
     *   Receive notification that a command is about to be performed.
     *   This is called on each object connected by containment with the
     *   actor performing the command, and on any objects explicitly
     *   registered with the actor, the actor's location and its locations
     *   up to the outermost container, or the directly involved objects.  
     */
    beforeAction()
    {
        /* by default, do nothing */
    }

    /*
     *   Receive notification that a command has just been performed.
     *   This is called by the same rules as beforeAction(), but under the
     *   conditions prevailing after the command has been completed. 
     */
    afterAction()
    {
        /* by default, do nothing */
    }

    /*
     *   Receive notification that a traveler (an actor or a vehicle, for
     *   example) is about to depart via travelerTravelTo(), OR that an
     *   actor is about to move among nested locations via travelWithin().
     *   This notification is sent to each object connected to the traveler
     *   by containment, just before the traveler departs.
     *   
     *   If the traveler is traveling between top-level locations,
     *   'connector' is the TravelConnector object being traversed.  If an
     *   actor is merely moving between nested locations, 'connector' will
     *   be nil.  
     */
    beforeTravel(traveler, connector) { /* do nothing by default */ }

    /*
     *   Receive notification that a traveler has just arrived via
     *   travelerTravelTo().  This notification is sent to each object
     *   connected to the traveler by containment, in its new location,
     *   just after the travel completes. 
     */
    afterTravel(traveler, connector) { /* do nothing by default */ }

    /*
     *   Get my notification list - this is a list of objects on which we
     *   must call beforeAction and afterAction when this object is
     *   involved in a command as the direct object, indirect object, or
     *   any addition object (other than as the actor performing the
     *   command).
     *   
     *   The general notification mechanism always includes in the
     *   notification list all of the objects connected by containment to
     *   the actor; this method allows for explicit registration of
     *   additional objects that must be notified when commands are
     *   performed on this object even when the other objects are nowhere
     *   nearby.  
     */
    getObjectNotifyList()
    {
        /* return our registration list */
        return objectNotifyList;
    }

    /*
     *   Add an item to our registered notification list for actions
     *   involving this object as the direct object, indirect object, and
     *   so on.
     *   
     *   Items can be added here if they must be notified of actions
     *   involving this object regardless of the physical proximity of
     *   this item and the notification item.  
     */
    addObjectNotifyItem(obj)
    {
        objectNotifyList += obj;
    }

    /* remove an item from the registered notification list */
    removeObjectNotifyItem(obj)
    {
        objectNotifyList -= obj;
    }

    /* our list of registered notification items */
    objectNotifyList = []

    /* -------------------------------------------------------------------- */
    /*
     *   Verify a proposed change of location of this object from its
     *   current container hierarchy to the given new container.  We'll
     *   verify removal from each container up to but not including a
     *   parent that's in common with the new container - we stop upon
     *   reaching the common parent because the object isn't leaving the
     *   common parent, but merely repositioned around within it.  We'll
     *   also verify insertion into each new parent from the first
     *   non-common parent on down to the immediate new container.
     *   
     *   This routine is called any time an actor action would cause this
     *   object to be moved to a new container, so it is the common point
     *   at which to intercept any action that would attempt to move the
     *   object.  
     */
    verifyMoveTo(newLoc)
    {
        /* check removal up to the common parent */
        sendNotifyRemove(self, newLoc, &verifyRemove);

        /* check insertion into parents up to the common parent */
        if (newLoc != nil)
            newLoc.sendNotifyInsert(self, newLoc, &verifyInsert);
    }

    /*
     *   Send the given notification to each direct parent, each of their
     *   direct parents, and so forth, stopping when we reach parents that
     *   we have in common with our new location.  We don't notify parents
     *   in common with new location (or their parents) because we're not
     *   actually removing the object from the common parents.  
     */
    sendNotifyRemove(obj, newLoc, msg)
    {
        /* send notification to each container, as appropriate */
        forEachContainer(new function(loc) {
            /*
             *   If this container contains the new location, don't send it
             *   (or its parents) notification, since we're not leaving it.
             *   Otherwise, send the notification and proceed to its
             *   parents.  
             */
            if (newLoc == nil
                || (loc != newLoc && !newLoc.isIn(loc)))
            {
                /* notify this container of the removal */
                loc.(msg)(obj);

                /* recursively notify this container's containers */
                loc.sendNotifyRemove(obj, newLoc, msg);
            }
        });
    }

    /*
     *   Send the given notification to each direct parent, each of their
     *   direct parents, and so forth, stopping when we reach parents that
     *   we have in common with our new location.  We don't notify parents
     *   in common with new location (or their parents).
     *   
     *   This should always be called *before* a change of location is
     *   actually put into effect, so that we will still be in our old
     *   container when this is called.  'obj' is the object being
     *   inserted, and 'newCont' is the new direct container.  
     */
    sendNotifyInsert(obj, newCont, msg)
    {
        /* 
         *   If the object is already in me, there's no need to notify
         *   myself of the insertion; otherwise, send the notification.
         *   
         *   If the object is already inside me indirectly, and we're
         *   moving it directly in me, still notify myself, since we're
         *   still picking up a new direct child.  
         */
        if (!obj.isIn(self))
        {
            /* 
             *   before we notify ourselves, notify my own parents - this
             *   sends the notifications from the outside in 
             */
            forEachContainer({loc: loc.sendNotifyInsert(obj, newCont, msg)});

            /* notify this potential container of the insertion */
            self.(msg)(obj, newCont);
        }
        else if (newCont == self && !obj.isDirectlyIn(self))
        {
            /* notify myself of the new direct insertion */
            self.(msg)(obj, self);
        }
    }

    /*
     *   Verify removal of an object from my contents or a child object's
     *   contents.  By default we allow the removal.  This is to be called
     *   during verification only, so gVerifyResult is valid when this is
     *   called.  
     */
    verifyRemove(obj)
    {
    }

    /*
     *   Verify insertion of an object into my contents.  By default we
     *   allow it, unless I'm already inside the other object.  This is to
     *   be called only during verification.  
     */
    verifyInsert(obj, newCont)
    {
        /* 
         *   If I'm inside the other object, don't allow it, since this
         *   would create circular containment. 
         */
        if (isIn(obj))
            illogicalNow(obj.circularlyInMessage, newCont, obj);
    }

    /* 
     *   my message indicating that another object x cannot be put into me
     *   because I'm already in x 
     */
    circularlyInMessage = &circularlyInMsg

    /*
     *   Receive notification that we are about to remove an object from
     *   this container.  This is normally called during the action()
     *   phase.
     *   
     *   When an object is about to be moved via moveInto(), the library
     *   calls notifyRemove on the old container tree, then notifyInsert on
     *   the new container tree, then notifyMoveInto on the object being
     *   moved.  Any of these routines can cancel the operation by
     *   displaying an explanatory message and calling 'exit'.  
     */
    notifyRemove(obj)
    {
    }

    /*
     *   Receive notification that we are about to insert a new object into
     *   this container.  'obj' is the object being moved, and 'newCont' is
     *   the new direct container (which might be a child of ours).  This
     *   is normally called during the action() phase.
     *   
     *   During moveInto(), this is called on the new container tree after
     *   notifyRemove has been called on the old container tree.  This
     *   routine can cancel the move by displaying an explanatory message
     *   and calling 'exit'.  
     */
    notifyInsert(obj, newCont)
    {
    }

    /*
     *   Receive notification that I'm about to be moved to a new
     *   container.  By default, we do nothing; subclasses can override
     *   this to do any special processing when this object is moved.  This
     *   is normally called during the action() phase.
     *   
     *   During moveInto(), this routine is called after notifyRemove() and
     *   notifyInsert() have been called on the old and new container trees
     *   (respectively).  This routine can cancel the move by displaying an
     *   explanatory message and calling 'exit'.
     *   
     *   This routine is the last in the notification sequence, so if this
     *   routine doesn't cancel the move, then the move will definitely
     *   happen (at least to the extent that we'll definitely call
     *   baseMoveInto() to carry out the move).  
     */
    notifyMoveInto(newCont)
    {
    }

    /* -------------------------------------------------------------------- */
    /*
     *   Determine if one property on this object effectively "hides"
     *   another.  This is a sort of override check for two distinct
     *   properties.
     *   
     *   We look at the object to determine where prop1 and prop2 are
     *   defined in the class hierarchy.  If prop1 isn't defined, it
     *   definitely doesn't hide prop2.  If prop2 isn't defined, prop1
     *   definitely hides it.  If both are defined, then prop1 hides prop2
     *   if and only if it is defined at a point in the class hierarchy
     *   that is "more specialized" than prop2.  That is, for prop1 to
     *   hide prop2, the class that defines prop1 must either be the same
     *   as the class that defines prop2, or the class where prop1 is
     *   defined must inherit from the class that defines prop2, or the
     *   class where prop1 is defined must be earlier in a multiple
     *   inheritance list than the class defining prop2.  
     */
    propHidesProp(prop1, prop2)
    {
        local definer1;
        local definer2;
        
        /* 
         *   get the classes in our hierarchy where the two properties are
         *   defined 
         */
        definer1 = propDefined(prop1, PropDefGetClass);
        definer2 = propDefined(prop2, PropDefGetClass);

        /* if prop1 isn't defined, it definitely doesn't hide prop2 */
        if (definer1 == nil)
            return nil;

        /* if prop1 isn't defined, prop1 definitely hides it */
        if (definer2 == nil)
            return true;

        /* 
         *   They're both defined.  If definer1 inherits from definer2,
         *   then prop1 definitely hides prop2. 
         */
        if (definer1.ofKind(definer2))
            return true;

        /* 
         *   if definer2 inherits from definer1, then prop2 hides prop1,
         *   so prop1 doesn't hide prop2 
         */
        if (definer2.ofKind(definer1))
            return nil;

        /*
         *   The two classes don't have an inheritance relation among
         *   themselves, but they might still be related in our own
         *   hierarchy by multiple inheritance.  In particular, if there
         *   is some point in the hierarchy where we have multiple base
         *   classes, and one class among the multiple bases inherits from
         *   definer1 and the other from definer2, then the one that's
         *   earlier in the multiple inheritance list is the hider. 
         */
        return superHidesSuper(definer1, definer2);
    }

    /*
     *   Determine if a given superclass of ours hides another superclass
     *   of ours, by being inherited (directly or indirectly) in our class
     *   list ahead of the other. 
     */
    superHidesSuper(s1, s2)
    {
        local lst;
        local idx1, idx2;
        
        /* get our superclass list */
        lst = getSuperclassList();

        /* if we have no superclass, there is no hiding */
        if (lst.length() == 0)
            return nil;

        /* 
         *   if we have only one element, there's obviously no hiding
         *   going on at this level, so simply traverse into the
         *   superclass to see if the hiding happens there 
         */
        if (lst.length() == 1)
            return lst[1].superHidesSuper(s1, s2);

        /*
         *   Scan the superclass list to determine the first superclass to
         *   which each of the two superclasses is related.  Stop looking
         *   when we find both superclasses or exhaust our list.  
         */
        for (local i = 1, idx1 = idx2 = nil ;
             i <= lst.length() && (idx1 == nil || idx2 == nil) ; ++i)
        {
            /* 
             *   if we haven't found s1 yet, and this superclass of ours
             *   inherits from s1, this is the earliest at which we've
             *   found s1 
             */
            if (idx1 == nil && lst[i].ofKind(s1))
                idx1 = i;

            /* likewise for the other superclass */
            if (idx2 == nil && lst[i].ofKind(s2))
                idx2 = i;
        }

        /* 
         *   if we found the two superclasses at different points in our
         *   hierarchy, the one that comes earlier hides the other; so, if
         *   idx1 is less than idx2, s1 hides s2, and if idx1 is greater
         *   than idx2, s2 hides s1 (equivalently, s1 doesn't hide s2) 
         */
        if (idx1 != idx2)
            return idx1 < idx2;

        /*
         *   We found both superclasses at the same point in our
         *   hierarchy, so they must be multiply inherited by this base
         *   class or one of its classes.  Keep looking up the hierarchy
         *   for our answer. 
         */
        return lst[idx1].superHidesSuper(s1, s2);
    }

    /* 
     *   Generic "check" failure routine.  This displays the given failure
     *   message, then performs an 'exit' to cancel the current command.
     *   'msg' is the message to display - it can be a single-quoted string
     *   with the message text, or a property pointer.  If 'msg' is a
     *   property pointer, any necessary message parameters can be supplied
     *   as additional 'params' arguments.  
     */
    failCheck(msg, [params])
    {
        reportFailure(msg, params...);
        exit;
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Examine" action 
     */
    dobjFor(Examine)
    {
        preCond = [objVisible]
        verify()
        {
            /* give slight preference to an object being held */
            if (!isIn(gActor))
                logicalRank(80, 'not held');
        }
        action()
        {
            /* 
             *   call our mainExamine method from the current actor's point
             *   of view 
             */
            fromPOV(gActor, gActor, &mainExamine);
        }
    }

    /*
     *   Main examination processing.  This is called with the current
     *   global POV set to the actor performing the 'examine' command. 
     */
    mainExamine()
    {
        /* perform the basic 'examine' action */
        basicExamine();

        /* 
         *   listen to and smell the object, but only show a message if we
         *   have an explicitly associated Noise/Odor object 
         */
        basicExamineListen(nil);
        basicExamineSmell(nil);
        
        /* show special descriptions for any contents */
        examineSpecialContents();
    }

    /*
     *   Perform the basic 'examine' action.  This shows either the normal
     *   or initial long description (the latter only if the object hasn't
     *   been moved yet, and it has a special initial examine
     *   description), and marks the object as having been described at
     *   least once.  
     */
    basicExamine()
    {
        local info;
        local t;
        
        /* check the transparency to viewing this object */
        info = getVisualSenseInfo();
        t = info.trans;

        /*
         *   If the viewing conditions are 'obscured' or 'distant', use
         *   the special, custom messages for those conditions.
         *   Otherwise, if the details are visible (which will be the case
         *   even for a distant or obscured object if its sightSize is set
         *   to 'large'), use the ordinary 'desc' description.  Otherwise,
         *   use an appropriate default description indicating that the
         *   object's details aren't visible.  
         */
        if (getOutermostRoom() != getPOVDefault(gActor).getOutermostRoom()
            && propDefined(&remoteDesc))
        {
            /* we're remote, so show our remote description */
            remoteDesc(getPOVDefault(gActor));
        }
        else if (t == obscured && propDefined(&obscuredDesc))
        {
            /* we're obscured, so show our special obscured description */
            obscuredDesc(info.obstructor);
        }
        else if (t == distant && propDefined(&distantDesc))
        {
            /* we're distant, so show our special distant description */
            distantDesc;
        }
        else if (canDetailsBeSensed(sight, info, getPOVDefault(gActor)))
        {
            /* 
             *   Viewing conditions and/or scale are suitable for showing
             *   full details, so show my normal long description.  Use
             *   the initDesc if desired, otherwise show the normal "desc"
             *   description.  
             */
            if (useInitDesc())
                initDesc;
            else
                desc;

            /* note that we've examined it */
            described = true;

            /* show any subclass-specific status */
            examineStatus();
        }
        else if (t == obscured)
        {
            /* 
             *   we're obscured, and we're not large enough to see the
             *   details anyway, and we don't have a special description
             *   for when we're obscured; show the default description of
             *   an obscured object 
             */
            defaultObscuredDesc(info.obstructor);
        }
        else if (t == distant)
        {
            /* 
             *   we're distant, and we're not large enough to see the
             *   details anyway, and we don't have a special distant
             *   description; show the default distant description 
             */
            defaultDistantDesc;
        }
    }

    /*
     *   Show any status associated with the object as part of the full
     *   description.  This is shown after the basicExamine() message, and
     *   is only displayed if we can see full details of the object
     *   according to the viewing conditions.
     *   
     *   By default, we simply show our contents.  Even though this base
     *   class isn't set up as a container from the player's perspective,
     *   we could contain components which are themselves containers.  So,
     *   to ensure that we properly describe any contents of our contents,
     *   we need to list our children.  
     */
    examineStatus()
    {
        /* list my contents */
        examineListContents();
    }

    /* 
     *   List my contents as part of Examine processing.  We'll recursively
     *   list contents of contents; this will ensure that even if we have
     *   no listable contents, we'll list any listable contents of our
     *   contents.  
     */
    examineListContents()
    {
        /* show our contents with our normal contents lister */
        examineListContentsWith(descContentsLister);
    }

    /* list my contents for an "examine", using the given lister */
    examineListContentsWith(lister)
    {
        local tab;
        local lst;

        /* get the actor's visual sense information */
        tab = gActor.visibleInfoTable();

        /* mark my contents as having been seen */
        setContentsSeenBy(tab, gActor);

        /* if we don't list our contents on Examine, do nothing */
        if (!contentsListedInExamine)
            return;

        /* get my listed contents for 'examine' */
        lst = getContentsForExamine(lister, tab);
        
        /* show my listable contents, if I have any */
        lister.showList(gActor, self, lst, ListRecurse, 0, tab, nil);
    }

    /*
     *   Basic examination of the object for sound.  If the object has an
     *   associated noise object, we'll describe it.
     *   
     *   If 'explicit' is true, we'll show our soundDesc if we have no
     *   associated Noise object; otherwise, we'll show nothing at all
     *   unless we have a Noise object.  
     */
    basicExamineListen(explicit)
    {
        local obj;
        local info;
        local t;

        /* get my associated Noise object, if we have one */
        obj = getNoise();

        /* 
         *   If this is not an explicit LISTEN command, only add the sound
         *   description if we have a Noise object that is not marked as
         *   "ambient."  
         */
        if (!explicit && (obj == nil || obj.isAmbient))
            return;

        /* get our sensory information from the actor's point of view */
        info = getPOVDefault(gActor).senseObj(sound, self);
        t = info.trans;

        /*
         *   If we have a transparent path to the object, or we have
         *   'large' sound scale, show full details.  Otherwise, show the
         *   appropriate non-detailed message for the listening conditions.
         */
        if (canDetailsBeSensed(sound, info, getPOVDefault(gActor)))
        {
            /* 
             *   We can hear full details.  If we have a Noise object, show
             *   its "listen to source" description; otherwise, show our
             *   own default sound description.  
             */
            if (obj != nil)
            {
                /* note the explicit display of the Noise description */
                obj.noteDisplay();
                
                /* show the examine-source description of the Noise */
                obj.sourceDesc;
            }
            else
            {
                /* we have no Noise, so use our own description */
                soundDesc;
            }
        }
        else if (t == obscured)
        {
            /* show our 'obscured' description */
            obscuredSoundDesc(info.obstructor);
        }
        else if (t == distant)
        {
            /* show our 'distant' description */
            distantSoundDesc;
        }

        /* 
         *   If this is an explicit LISTEN TO directed at this object, also
         *   mention any sounds we can hear from objects inside me, other
         *   than the Noise object we've explicitly mentioned, that have a
         *   sound presence.  
         */
        if (explicit)
        {
            local senseTab;
            local presenceList;

            /* get the set of objects we can hear */
            senseTab = getPOVDefault(gActor).senseInfoTable(sound);

            /* get the subset with a sound presence that are inside me */
            presenceList = senseInfoTableSubset(
                senseTab,
                {x, info: x.soundPresence && x.isIn(self) && x != obj});

            /* show the soundHereDesc for each of these */
            foreach (local cur in presenceList)
                cur.soundHereDesc();
        }
    }

    /*
     *   Basic examination of the object for odor.  If the object has an
     *   associated odor object, we'll describe it.  
     */
    basicExamineSmell(explicit)
    {
        local obj;
        local info;
        local t;

        /* get our associated Odor object, if any */
        obj = getOdor();

        /* 
         *   If this is not an explicit SMELL command, only add the odor
         *   description if we have an Odor object that is not marked as
         *   "ambient."  
         */
        if (!explicit && (obj == nil || obj.isAmbient))
            return;
        
        /* get our sensory information from the actor's point of view */
        info = getPOVDefault(gActor).senseObj(smell, self);
        t = info.trans;

        /*
         *   If we have a transparent path to the object, or we have
         *   'large' sound scale, show full details.  Otherwise, show the
         *   appropriate non-detailed message for the listening conditions.
         */
        if (canDetailsBeSensed(smell, info, getPOVDefault(gActor)))
        {
            /* if we have a Noise object, show its "listen to source" */
            if (obj != nil)
            {
                /* note the explicit display of the Odor description */
                obj.noteDisplay();
                
                /* show the examine-source description of the Odor */
                obj.sourceDesc;
            }
            else
            {
                /* we have no associated Odor; show our default description */
                smellDesc;
            }
        }
        else if (t == obscured)
        {
            /* show our 'obscured' description */
            obscuredSmellDesc(info.obstructor);
        }
        else if (t == distant)
        {
            /* show our 'distant' description */
            distantSmellDesc;
        }

        /* 
         *   If this is an explicit SMELL command directed at this object,
         *   also mention any odors we can detect from objects inside me,
         *   other than the Odor object we've explicitly mentioned, that
         *   have an odor presence.  
         */
        if (explicit)
        {
            local senseTab;
            local presenceList;

            /* get the set of objects we can smell */
            senseTab = getPOVDefault(gActor).senseInfoTable(smell);

            /* get the subset with a smell presence that are inside me */
            presenceList = senseInfoTableSubset(
                senseTab,
                {x, info: x.smellPresence && x.isIn(self) && x != obj});

            /* show the smellHereDesc for each of these */
            foreach (local cur in presenceList)
                cur.smellHereDesc();
        }
    }

    /*
     *   Basic examination of an object for taste.  Unlike the
     *   smell/listen examination routines, we don't bother using a
     *   separate sensory emanation object for tasting, as tasting is
     *   always an explicit action, never passive.  Furthermore, since
     *   tasting requires direct physical contact with the object, we
     *   don't differentiate levels of transparency or distance.  
     */
    basicExamineTaste()
    {
        /* simply show our taste description */
        tasteDesc;
    }

    /*
     *   Basic examination of an object for touch.  As with the basic
     *   taste examination, we don't use an emanation object or
     *   distinguish transparency levels, because feeling an object
     *   requires direct physical contact.  
     */
    basicExamineFeel()
    {
        /* simply show our touch description */
        feelDesc;
    }

    /*
     *   Show the special descriptions of any contents.  We'll run through
     *   the visible information list for the location; for any visible
     *   item inside me that is using its special description, we'll
     *   display the special description as a separate paragraph.  
     */
    examineSpecialContents()
    {
        local lst;
        local infoTab;
        
        /* get the actor's table of visible items */
        infoTab = gActor.visibleInfoTable();

        /* 
         *   get the objects using special descriptions and contained
         *   within this object 
         */
        lst = specialDescList(infoTab,
                              {obj: obj.useSpecialDescInContents(self)});

        /* show the special descriptions */
        (new SpecialDescContentsLister(self)).showList(
            gActor, nil, lst, 0, 0, infoTab, nil);
    }

    /*
     *   Given a visible object info table (from Actor.visibleInfoTable()),
     *   get the list of objects, filtered by the given condition and
     *   sorted by specialDescOrder.  
     */
    specialDescList(infoTab, cond)
    {
        local lst;
        
        /* 
         *   get a list of all of the objects in the table - the objects
         *   are the keys, so we just want a list of the keys 
         */
        lst = infoTab.keysToList();

        /* subset the list for the given condition */
        lst = lst.subset(cond);

        /* 
         *   Sort the list in ascending order of specialDescOrder, and
         *   return the result.  Where the specialDescOrder is the same,
         *   and one object is inside another, put the outer object earlier
         *   in the list; this will ensure that we'll generally list
         *   objects before their contents, except when the special desc
         *   list order specifically says otherwise.  
         */
        return lst.sort(SortAsc, new function(a, b) {
            /* if the list orders are different, go by list order */
            if (a.specialDescOrder != b.specialDescOrder)
                return a.specialDescOrder - b.specialDescOrder;

            /* 
             *   the list order is the same; if one is inside the other,
             *   list the outer one first 
             */
            if (a.isIn(b))
                return 1;
            else if (b.isIn(a))
                return -1;
            else
                return 0;
        });
    }
    

    /* -------------------------------------------------------------------- */
    /*
     *   "Read" 
     */
    dobjFor(Read)
    {
        preCond = [objVisible]
        verify()
        {
            /* 
             *   reduce the likelihood that they want to read an ordinary
             *   item, but allow it 
             */
            logicalRank(50, 'not readable');

            /* give slight preference to an object being held */
            if (!isIn(gActor))
                logicalRank(80, 'not held');
        }
        action()
        {
            /* simply show the ordinary description */
            actionDobjExamine();
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Look in" 
     */
    dobjFor(LookIn)
    {
        preCond = [objVisible]
        verify()
        {
            /* give slight preference to an object being held */
            if (!isIn(gActor))
                logicalRank(80, 'not held');
        }
        action()
        {
            /* show our LOOK IN description */
            lookInDesc;
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Search".  By default, we make "search obj" do the same thing as
     *   "look in obj".  
     */
    dobjFor(Search) asDobjFor(LookIn)

    /* -------------------------------------------------------------------- */
    /*
     *   "Look under" 
     */
    dobjFor(LookUnder)
    {
        preCond = [objVisible]
        verify() { }
        action() { mainReport(&nothingUnderMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Look behind" 
     */
    dobjFor(LookBehind)
    {
        preCond = [objVisible]
        verify() { }
        action() { mainReport(&nothingBehindMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Look through" 
     */
    dobjFor(LookThrough)
    {
        verify() { }
        action() { mainReport(&nothingThroughMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Listen to"
     */
    dobjFor(ListenTo)
    {
        preCond = [objAudible]
        verify() { }
        action()
        {
            /* show our "listen" description explicitly */
            fromPOV(gActor, gActor, &basicExamineListen, true);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Smell"
     */
    dobjFor(Smell)
    {
        preCond = [objSmellable]
        verify() { }
        action()
        {
            /* 
             *   show our 'smell' description, explicitly showing our
             *   default description if we don't have an Odor association 
             */
            fromPOV(gActor, gActor, &basicExamineSmell, true);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Taste"
     */
    dobjFor(Taste)
    {
        /* to taste an object, we have to be able to touch it */
        preCond = [touchObj]
        verify()
        {
            /* you *can* taste anything, but for most things it's unlikely */
            logicalRank(50, 'not edible');
        }
        action()
        {
            /* show our "taste" description */
            fromPOV(gActor, gActor, &basicExamineTaste);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Feel" 
     */
    dobjFor(Feel)
    {
        /* to feel an object, we have to be able to touch it */
        preCond = [touchObj]
        verify() { }
        action()
        {
            /* show our "feel" description */
            fromPOV(gActor, gActor, &basicExamineFeel);
        }
    }
    

    /* -------------------------------------------------------------------- */
    /*
     *   "Take" action
     */

    dobjFor(Take)
    {
        preCond = [touchObj, objNotWorn, roomToHoldObj]
        verify()
        {
            /*      
             *   if the object is already being held by the actor, it
             *   makes no sense at the moment to take it 
             */
            if (isDirectlyIn(gActor))
            {
                /* I'm already holding it, so this is not logical */
                illogicalAlready(&alreadyHoldingMsg);
            }
            else
            {
                local carrier;
                
                /*
                 *   If the object isn't being held, it's logical to take
                 *   it.  However, rank objects being carried as less
                 *   likely than objects not being carried, because in an
                 *   ambiguous situation, it's more likely that an actor
                 *   would want to take something not being carried at all
                 *   than something already being carried inside another
                 *   object.  
                 */
                if (isIn(gActor))
                    logicalRank(70, 'already in');

                /*
                 *   If the object is being carried by another actor,
                 *   reduce the likelihood, since taking something from
                 *   another actor is usually less likely than taking
                 *   something out of one's own possessions or from the
                 *   location.  
                 */
                carrier = getCarryingActor();
                if (carrier != nil && carrier != gActor)
                    logicalRank(60, 'other owner');
            }

            /* 
             *   verify transfer from the current container hierarchy to
             *   the new container hierarchy 
             */
            verifyMoveTo(gActor);
        }
    
        action()
        {
            /* move me into the actor's direct contents */
            moveInto(gActor);

            /* issue our default acknowledgment of the command */
            defaultReport(&okayTakeMsg);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Remove" processing.  We'll treat "remove dobj" as meaning "take
     *   dobj from <something>", where <something> is elided and must be
     *   determined.
     *   
     *   This should be overridden in certain cases.  For clothing,
     *   "remove dobj" should be the same as "doff dobj".  For removable
     *   components, this should imply removing the component from its
     *   container.  
     */
    dobjFor(Remove)
    {
        preCond = [touchObj, objNotWorn, roomToHoldObj]
        verify()
        {
            /* if we're already held, there's nothing to remove me from */
            if (isHeldBy(gActor))
                illogicalNow(&cannotRemoveHeldMsg);
        }
        action() { askForIobj(TakeFrom); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Take from" processing 
     */
    dobjFor(TakeFrom)
    {
        preCond = [touchObj, objNotWorn, roomToHoldObj]
        verify()
        {
            /* 
             *   we can only take something from something else if the thing
             *   is inside the other thing 
             */
            if (gIobj != nil && !self.isIn(gIobj))
                illogicalAlready(gIobj.takeFromNotInMessage);

            /* treat this otherwise like a regular "take" */
            verifyDobjTake();
        }
        check()
        {
            /* apply the same checks as for a regular "take" */
            checkDobjTake();
        }
        action()
        {
            /* we've passed our checks, so process it as a regular "take" */
            replaceAction(Take, self);
        }
    }
    iobjFor(TakeFrom)
    {
        verify()
        {
            /* check what we know about the dobj */
            if (gDobj == nil)
            {
                /* 
                 *   We haven't yet resolved the direct object; check the
                 *   tentative direct object list, and count us as
                 *   illogical if none of the possible direct objects are
                 *   in me.  
                 */
                if (gTentativeDobj.indexWhich({x: x.obj_.isIn(self)}) == nil)
                    illogicalAlready(takeFromNotInMessage);
                else if (gTentativeDobj.indexWhich(
                    {x: x.obj_.isDirectlyIn(self)}) != nil)
                    logicalRank(150, 'directly in');
            }
            else if (!gDobj.isIn(self))
            {
                /* 
                 *   the dobj isn't in me, so it's obviously not logical
                 *   to take the dobj out of me 
                 */
                illogicalAlready(takeFromNotInMessage);
            }
            else if (gDobj.isDirectlyIn(self))
            {
                /* 
                 *   it's slightly more likely that they want to remove
                 *   the object from its direct container 
                 */
                logicalRank(150, 'directly in');
            }
        }
    }

    /* general message for "take from" when an object isn't in me */
    takeFromNotInMessage = &takeFromNotInMsg

    /* -------------------------------------------------------------------- */
    /*
     *   "Drop" verb processing 
     */
    dobjFor(Drop)
    {
        preCond = [objHeld]
        verify()
        {
            /* the object must be held by the actor, at least indirectly */
            if (isIn(gActor))
            {
                /* 
                 *   it's being held, so dropping it makes sense; verify
                 *   transfer from the current container hierarchy to the
                 *   new one 
                 */
                verifyMoveTo(gActor.getDropDestination(self, nil));
            }
            else
            {
                /* it's not being held, so this is simply not logical */
                illogicalAlready(&notCarryingMsg);
            }
        }

        action()
        {
            /* send the object to the actor's drop destination */
            gActor.getDropDestination(self, nil)
                .receiveDrop(self, dropTypeDrop);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Put In" verb processing.  Default objects cannot contain other
     *   objects, but they can be put in arbitrary containers.  
     */
    dobjFor(PutIn)
    {
        preCond = [objHeld]
        verify()
        {
            /* 
             *   It makes no sense to put us in a container we're already
             *   directly in.  (It's fine to put it in something it's
             *   indirectly in, though - doing so takes it out of the
             *   intermediate container and moves it directly into the
             *   indirect object.) 
             */
            if (gIobj != nil && isDirectlyIn(gIobj))
                illogicalAlready(&alreadyPutInMsg);

            /* can't put in self, obviously */
            if (gIobj == self)
                illogicalSelf(&cannotPutInSelfMsg);

            /* verify the transfer */
            verifyMoveTo(gIobj);
        }
    }
    iobjFor(PutIn)
    {
        preCond = [touchObj]
        verify()
        {
            /* by default, objects cannot be put in this object */
            illogical(&notAContainerMsg);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Put On" processing.  Default objects cannot have other objects
     *   put on them, but they can be put on surfaces.
     */
    dobjFor(PutOn)
    {
        preCond = [objHeld]
        verify()
        {
            /* it makes no sense to put us on a surface we're already on */
            if (gIobj != nil && isDirectlyIn(gIobj))
                illogicalAlready(&alreadyPutOnMsg);

            /* can't put on self, obviously */
            if (gIobj == self)
                illogicalSelf(&cannotPutOnSelfMsg);

            /* verify the transfer */
            verifyMoveTo(gIobj);
        }
    }

    iobjFor(PutOn)
    {
        preCond = [touchObj]
        verify()
        {
            /* by default, objects cannot be put on this object */
            illogical(&notASurfaceMsg);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "PutUnder" action 
     */
    dobjFor(PutUnder)
    {
        preCond = [objHeld]
        verify() { }
    }

    iobjFor(PutUnder)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPutUnderMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "PutBehind" action 
     */
    dobjFor(PutBehind)
    {
        preCond = [objHeld]
        verify() { }
    }

    iobjFor(PutBehind)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPutBehindMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Wear" action 
     */
    dobjFor(Wear)
    {
        verify() { illogical(&notWearableMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Doff" action 
     */
    dobjFor(Doff)
    {
        verify() { illogical(&notDoffableMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Kiss" 
     */
    dobjFor(Kiss)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not kissable'); }
        action() { mainReport(&cannotKissMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Ask for" action 
     */
    dobjFor(AskFor)
    {
        verify() { illogical(&notAddressableMsg, self); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Talk to" 
     */
    dobjFor(TalkTo)
    {
        verify() { illogical(&notAddressableMsg, self); }
    }

    /* -------------------------------------------------------------------- */
    /* 
     *   "Give to" action 
     */
    dobjFor(GiveTo)
    {
        preCond = [objHeld]
        verify()
        {
            /* 
             *   if the intended recipient already has the object, there's
             *   no point in trying this 
             */
            if (gIobj != nil && isHeldBy(gIobj))
                illogicalAlready(&giveAlreadyHasMsg);

            /* inherit any further processing */
            inherited();
        }
    }
    iobjFor(GiveTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotGiveToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /* 
     *   "Show to" action 
     */
    dobjFor(ShowTo)
    {
        preCond = [objHeld]
        verify()
        {
            /* it's more likely that we want to show something held */
            if (isHeldBy(gActor))
            {
                /* I'm being held - use the default logical ranking */
            }
            else if (isIn(gActor))
            {
                /* 
                 *   the actor isn't hold me, but I am in the actor's
                 *   inventory, so reduce the likelihood only slightly 
                 */
                logicalRank(80, 'not held');
            }
            else
            {
                /* 
                 *   the actor isn't even carrying me, so reduce our
                 *   likelihood even more 
                 */
                logicalRank(70, 'not carried');
            }
        }
        check()
        {
            /*
             *   The direct object must be visible to the indirect object
             *   in order for the indirect object to be shown the direct
             *   object. 
             */
            if (!gIobj.canSee(self))
            {
                reportFailure(&actorCannotSeeMsg, gIobj, self);
                exit;
            }

            /*
             *   The actor performing the showing must also be visible to
             *   the indirect object, otherwise the actor wouldn't be able
             *   to attract the indirect object's attention to do the
             *   showing.  
             */
            if (!gIobj.canSee(gActor))
            {
                reportFailure(&actorCannotSeeMsg, gIobj, gActor);
                exit;
            }
        }
    }
    iobjFor(ShowTo)
    {
        verify() { illogical(&cannotShowToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Ask about" action 
     */
    dobjFor(AskAbout)
    {
        verify() { illogical(&notAddressableMsg, self); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Tell about" action 
     */
    dobjFor(TellAbout)
    {
        verify() { illogical(&notAddressableMsg, self); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   Vague "ask" and "tell" - these are for syntactically incorrect ASK
     *   and TELL phrasings, so that we can provide better error feedback.
     */
    dobjFor(AskVague)
    {
        verify() { illogical(&askVagueMsg); }
    }
    dobjFor(TellVague)
    {
        verify() { illogical(&tellVagueMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Follow" action 
     */
    dobjFor(Follow)
    {
        verify()
        {
            /* make sure I'm followable to start with */
            if (!verifyFollowable())
                return;

            /* it makes no sense to follow myself */
            if (gActor == gDobj)
                illogical(&cannotFollowSelfMsg);

            /* ask the actor to verify following the object */
            gActor.actorVerifyFollow(self);
        }
        action()
        {
            /* ask the actor to carry out the follow */
            gActor.actorActionFollow(self);
        }
    }

    /*
     *   Verify that I'm a followable object.  By default, it's not
     *   logical to follow an arbitrary object.  If I'm not followable,
     *   this routine will generate an appropriate illogical() explanation
     *   and return nil.  If I'm followable, we'll return true.  
     */
    verifyFollowable()
    {
        illogical(&notFollowableMsg);
        return nil;
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Attack" action.
     */
    dobjFor(Attack)
    {
        preCond = [touchObj]
        verify() { }
        action() { mainReport(&uselessToAttackMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Attack with" action - attack with (presumably) a weapon.
     */
    dobjFor(AttackWith)
    {
        preCond = [touchObj]

        /* 
         *   it makes as much sense to attack any object as any other, but
         *   by default attacking an object has no effect 
         */
        verify() { }
        action() { mainReport(&uselessToAttackMsg); }
    }
    iobjFor(AttackWith)
    {
        preCond = [objHeld]
        verify() { illogical(&notAWeaponMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Throw" action.  By default, we'll simply re-route this to a
     *   "throw at" action.  Objects that can meaningfully be thrown
     *   without any specific target can override this.
     *   
     *   Note that we don't apply an preconditions or verification, since
     *   we don't really do anything with the action ourselves.  If an
     *   object overrides this, it should add any preconditions and
     *   verifications that are appropriate.  
     */
    dobjFor(Throw)
    {
        verify() { }
        action() { askForIobj(ThrowAt); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Throw <direction>".  By default, we simply reject this and
     *   explain that the command to use is THROW AT.  With one exception:
     *   we treat THROW <down> as equivalent to THROW AT FLOOR, and use the
     *   default library message for that command instead.  
     */
    dobjFor(ThrowDir)
    {
        verify()
        {
            if (gAction.getDirection() == downDirection)
                illogicalAlready(&notCarryingMsg);
        }
        action()
        {
            /* 
             *   explain that we should use THROW AT (or DROP, in the case
             *   of THROW DOWN) 
             */
            reportFailure(gAction.getDirection() == downDirection
                          ? &shouldNotThrowAtFloorMsg
                          : &dontThrowDirMsg);
        }
    }


    /* -------------------------------------------------------------------- */
    /*
     *   "Throw at" action 
     */
    dobjFor(ThrowAt)
    {
        preCond = [objHeld]
        verify()
        {
            /* by default, we can throw something if we can drop it */
            verifyMoveTo(gActor.getDropDestination(self, nil));

            /* can't throw something at itself */
            if (gIobj == self)
                illogicalSelf(&cannotThrowAtSelfMsg);

            /* can't throw dobj at iobj if iobj is in dobj */
            if (gIobj != nil && gIobj.isIn(self))
                illogicalNow(&cannotThrowAtContentsMsg);
        }
        action()
        {
            /* 
             *   process a 'throw' operation, finishing with hitting the
             *   target if we get that far 
             */
            processThrow(gIobj, &throwTargetHitWith);
        }
    }
    iobjFor(ThrowAt)
    {
        /* by default, anything can be a target */
        verify() { }
    }

    /*
     *   Process a 'throw' command.  This is common handling that can be
     *   used for any sort of throwing (throw at, throw to, throw in,
     *   etc).  The projectile is self, and 'target' is the thing we're
     *   throwing at or to.  'hitProp' is the property to call on 'target'
     *   if we reach the target.  
     */
    processThrow(target, hitProp)
    {
        local path;
        local stat;
        
        /* get the throw path */
        path = getThrowPathTo(target);
        
        /* traverse the path with throwViaPath */
        stat = traversePath(
            path, {obj, op: throwViaPath(obj, op, target, path)});
        
        /*
         *   If we made it all the way through the path without complaint,
         *   process hitting the target.  If something along the path
         *   finished the traversal (by returning nil), we'll consider the
         *   action complete - the object along that path that canceled
         *   the traversal is responsible for having displayed an
         *   appropriate message.  
         */
        if (stat)
            target.(hitProp)(self, path);
    }

    /*
     *   Carry out a 'throw' operation along a path.  'self' is the
     *   projectile; 'obj' is the path element being traversed, and 'op' is
     *   the operation being used to traverse the element.  'target' is the
     *   object we're throwing 'self' at.  'path' is the projectile's full
     *   path (in getThrowPathTo format).
     *   
     *   By default, we'll use the standard canThrowViaPath handling (which
     *   invokes the even more basic checkThrowViaPath) to determine if we
     *   can make this traversal.  If so, we'll proceed with the throw;
     *   otherwise, we'll stop the throw by calling stopThrowViaPath() and
     *   returning the result.  
     */
    throwViaPath(obj, op, target, path)
    {
        /*
         *   By default, if we can throw the object through self, return
         *   true to allow the caller to proceed; otherwise, describe the
         *   object as hitting this element and falling to the appropriate
         *   point.  
         */
        if (obj.canThrowViaPath(self, target, op))
        {
            /* no objection - allow the traversal to proceed */
            return true;
        }
        else
        {
            /* can't do it - stop the throw and return the result */
            return obj.stopThrowViaPath(self, path);
        }
    }

    /*
     *   Process the effect of throwing the object 'projectile' at the
     *   target 'self'.  By default, we'll move the projectile to the
     *   target's drop location, and display a message saying that there
     *   was no effect other than the projectile dropping to the floor (or
     *   whatever it drops to).  'path' is the path we took to reach the
     *   target, as returned from getThrowPathTo(); this lets us determine
     *   how we approached the target.  
     */
    throwTargetHitWith(projectile, path)
    {
        /* 
         *   figure out where we fall to when we hit this object, then send
         *   the object being thrown to that location 
         */
        getHitFallDestination(projectile, path)
            .receiveDrop(projectile, new DropTypeThrow(self, path));
    }

    /*
     *   Stop a 'throw' operation along a path.  'self' is the object in
     *   the path that is impassable by 'projectile' according to
     *   canThrowViaPath(), and 'path' is the getThrowPathTo-style path of
     *   objects traversed in the projectile's trajectory.
     *   
     *   The return value is taken as a path traversal continuation
     *   indicator: nil means to stop the traversal, which is to say that
     *   the 'throw' command finishes here.  If we don't really want to
     *   stop the traversal, we can return 'true' to let the traversal
     *   continue.
     *   
     *   By default, we'll stop the throw by doing the same thing we would
     *   have done if we had successfully thrown the object at 'self' - the
     *   whole reason we're stopping the throw is that we're in the way, so
     *   the effect is the same as though we were the intended target to
     *   begin with.  This is the normal handling when we can't throw
     *   through 'obj' because 'obj' is a closed container or is otherwise
     *   impassable by self when thrown.  This can be overridden to provide
     *   different handling if needed.  
     */
    stopThrowViaPath(projectile, path)
    {
        /* we've been hit */
        throwTargetHitWith(projectile, path);
        
        /* tell the caller to stop the traversal  */
        return nil;
    }
    
    /*
     *   Get the "hit-and-fall" destination for a thrown object.  This is
     *   called when we interrupt a thrown object's trajectory because
     *   we're in the way of its trajectory.
     *   
     *   For example, if the actor is inside a cage, and tries to throw a
     *   projectile at an object outside the cage, and the cage blocks the
     *   projectile's passage, then this routine is called on the cage to
     *   determine where the projectile ends up.  The projectile's ultimate
     *   destination is the hit-and-fall destination for the cage: it's
     *   where the project ends up when it hits me and then falls to the
     *   ground, its trajectory cut short.
     *   
     *   'thrownObj' is the projectile thrown, 'self' is the target object,
     *   and 'path' is the path the projectile took to reach us.  The path
     *   is of the form returned by getThrowPathTo().  Note that the path
     *   could extend beyond 'self', because the original target might have
     *   been a different object - we could simply have interrupted the
     *   projectile's course.  
     */
    getHitFallDestination(thrownObj, path)
    {
        local prvCont;
        local prvOp;
        local idx;
        local dest;
        local common;

        /* find myself in the path */
        idx = path.indexOf(self);

        /* 
         *   get the container traversed just before us in the path (it's
         *   two positions before us in the path list, because the path
         *   consists of alternating objects and operators) 
         */
        prvCont = path[idx - 2];
        
        /* get the operation that got from the last container to us */
        prvOp = path[idx - 1];

        /* 
         *   If the previous container is within us, we're throwing from
         *   inside to the outside, so the object falls within me;
         *   otherwise, the object bounces off the outside and falls
         *   outside me.
         *   
         *   If the projectile traversed *inward* from the previous item in
         *   the path, then we should simply land in the drop destination
         *   of the previous item itself, since we're coming in through
         *   that item.  If the previous item is a peer of ours, though, we
         *   traversed *across* the item, so land in our common direct
         *   container.
         *   
         *   Note that in certain cases, the previous "container" will be
         *   the projectile itself; this happens when the thrower is
         *   throwing the object at itself (as in "throw it at me").  In
         *   these cases, we'll assume that the the thrower is *holding*
         *   rather than containing the object, in which case the object
         *   isn't actually inside the thrower, in which case we want to
         *   use the location's drop destination.  
         */
        if (prvCont == thrownObj)
        {
            /* throwing object at self - land in my location's drop dest */
            dest = (location != nil
                    ? location.getDropDestination(thrownObj, path)
                    : self);
        }
        else if (prvCont.isIn(self))
        {
            /* throwing from within - land in my own drop destination */
            dest = getDropDestination(thrownObj, path);
        }
        else if (prvOp == PathPeer
                 && (common = getCommonDirectContainer(prvCont)) != nil)
        {
            /* 
             *   We're coming over from a peer, and we found a container in
             *   common with the peer.  Land in the common container's drop
             *   destination.  
             */
            dest = common.getDropDestination(thrownObj, path);
        }
        else
        {
            /*
             *   Either we're coming in from a container, or we weren't
             *   able to find a container in common with the peer.  In
             *   either case, the projectile lands in the 'drop'
             *   destination of the previous object in the path.  
             */
            dest = prvCont.getDropDestination(thrownObj, path);
        }

        /*
         *   Whatever we found, give the destination itself a chance to
         *   make any necessary adjustments.  
         */
        return dest.adjustThrowDestination(thrownObj, path);
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Throw to" action 
     */
    dobjFor(ThrowTo)
    {
        preCond = [objHeld]
        verify()
        {
            /* 
             *   by default, we can throw an object to someone if we can
             *   throw it at them 
             */
            verifyDobjThrowAt();
        }
        action()
        {
            /* 
             *   process a 'throw' operation, finishing with the target
             *   trying to catch the object if we get that far
             */
            processThrow(gIobj, &throwTargetCatch);
        }
    }

    iobjFor(ThrowTo)
    {
        verify()
        {
            /* by default, we don't want to catch anything */
            illogical(&cannotThrowToMsg);
        }
    }

    /*
     *   Process the effect of throwing the object 'obj' to the catcher
     *   'self'.  By default, we'll simply move the projectile into self.
     */
    throwTargetCatch(obj, path)
    {
        /* take the object */
        obj.moveInto(self);

        /* generate the default message if we successfully took the object */
        if (obj.isDirectlyIn(self))
            mainReport(&throwCatchMsg, obj, self);
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Dig" action - by default, simply re-reoute to dig-with, since we
     *   generally need a digging implement to dig in anything.  Some
     *   objects might want to override this to allow digging without any
     *   implement; a sandy beach, for example, might allow digging in the
     *   sand without a shovel.  
     */
    dobjFor(Dig)
    {
        preCond = [touchObj]
        verify() { }
        action() { askForIobj(DigWith); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "DigWith" action 
     */
    dobjFor(DigWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotDigMsg); }
    }
    iobjFor(DigWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotDigWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "jump over" 
     */
    dobjFor(JumpOver)
    {
        verify() { illogical(&cannotJumpOverMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "jump off" 
     */
    dobjFor(JumpOff)
    {
        verify() { illogical(&cannotJumpOffMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Push" action 
     */
    dobjFor(Push)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not pushable'); }
        action() { reportFailure(&pushNoEffectMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Pull" action 
     */
    dobjFor(Pull)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not pullable'); }
        action() { reportFailure(&pullNoEffectMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Move" action 
     */
    dobjFor(Move)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not movable'); }
        action() { reportFailure(&moveNoEffectMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "MoveWith" action 
     */
    dobjFor(MoveWith)
    {
        preCond = [iobjTouchObj]
        verify() { logicalRank(50, 'not movable'); }
        action() { reportFailure(&moveNoEffectMsg); }
    }
    iobjFor(MoveWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotMoveWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "MoveTo" action 
     */
    dobjFor(MoveTo)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not movable'); }
        action() { reportFailure(&moveToNoEffectMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Turn" action 
     */
    dobjFor(Turn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTurnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Turn to" action 
     */
    dobjFor(TurnTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTurnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "TurnWith" action 
     */
    dobjFor(TurnWith)
    {
        preCond = [iobjTouchObj]
        verify() { illogical(&cannotTurnMsg); }
    }
    iobjFor(TurnWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotTurnWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Set" action
     */
    dobjFor(Set)
    {
        verify() { }
        action() { askForIobj(PutOn); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "SetTo" action 
     */
    dobjFor(SetTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotSetToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Consult" action 
     */
    dobjFor(Consult)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotConsultMsg); }
    }

    dobjFor(ConsultAbout)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotConsultMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Type on" action 
     */
    dobjFor(TypeOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTypeOnMsg); }

        /* 
         *   if the verifier is overridden to allow typing on this object,
         *   by default just ask for a missing literal phrase, since we
         *   need something to type on this object 
         */
        action() { askForLiteral(TypeLiteralOn); }
    }

    /*
     *   "Type <literal> on" action 
     */
    dobjFor(TypeLiteralOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTypeOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Enter on" action 
     */
    dobjFor(EnterOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotEnterOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Switch" action 
     */
    dobjFor(Switch)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotSwitchMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Flip" action 
     */
    dobjFor(Flip)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotFlipMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "TurnOn" action 
     */
    dobjFor(TurnOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTurnOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "TurnOff" action 
     */
    dobjFor(TurnOff)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotTurnOffMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Light" action.  By default, we treat this as equivalent to
     *   "burn".  
     */
    dobjFor(Light) asDobjFor(Burn)

    /* -------------------------------------------------------------------- */
    /*
     *   "Burn".  By default, we ask for something to use to burn the
     *   object, since most objects are not self-igniting.  
     */
    dobjFor(Burn)
    {
        preCond = [touchObj]
        verify()
        {
            /* 
             *   although we can in principle burn anything, most things
             *   are unlikely choices for burning
             */
            logicalRank(50, 'not flammable');
        }
        action()
        {
            /* rephrase this as a "burn with" command */
            askForIobj(BurnWith);
        }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Burn with" 
     */
    dobjFor(BurnWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotBurnMsg); }
    }
    iobjFor(BurnWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotBurnWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Extinguish" 
     */
    dobjFor(Extinguish)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotExtinguishMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "AttachTo" action 
     */
    dobjFor(AttachTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotAttachMsg); }
    }
    iobjFor(AttachTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotAttachToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "DetachFrom" action 
     */
    dobjFor(DetachFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotDetachMsg); }
    }
    iobjFor(DetachFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotDetachFromMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Detach" action 
     */
    dobjFor(Detach)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotDetachMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Break" action 
     */
    dobjFor(Break)
    {
        preCond = [touchObj]
        verify() { illogical(&shouldNotBreakMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Cut with" action 
     */
    dobjFor(CutWith)
    {
        preCond = [touchObj]
        verify() { logicalRank(50, 'not cuttable'); }
        action() { reportFailure(&cutNoEffectMsg); }
    }

    iobjFor(CutWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotCutWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Climb", "climb up", and "climb down" actions
     */
    dobjFor(Climb)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotClimbMsg); }
    }

    dobjFor(ClimbUp)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotClimbMsg); }
    }

    dobjFor(ClimbDown)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotClimbMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Open" action 
     */
    dobjFor(Open)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotOpenMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Close" action 
     */
    dobjFor(Close)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotCloseMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Lock" action 
     */
    dobjFor(Lock)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotLockMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Unlock" action 
     */
    dobjFor(Unlock)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnlockMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "LockWith" action 
     */
    dobjFor(LockWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotLockMsg); }
    }
    iobjFor(LockWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotLockWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "UnlockWith" action 
     */
    dobjFor(UnlockWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnlockMsg); }
    }
    iobjFor(UnlockWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotUnlockWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Eat" action 
     */
    dobjFor(Eat)
    {
        /* 
         *   generally, an object must be held to be eaten; this can be
         *   overridden on an object-by-object basis as desired 
         */
        preCond = [objHeld]
        verify() { illogical(&cannotEatMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Drink" action 
     */
    dobjFor(Drink)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotDrinkMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Pour" 
     */
    dobjFor(Pour)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPourMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Pour into" 
     */
    dobjFor(PourInto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPourMsg); }
    }
    iobjFor(PourInto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPourIntoMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Pour onto" 
     */
    dobjFor(PourOnto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPourMsg); }
    }
    iobjFor(PourOnto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPourOntoMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Clean" action 
     */
    dobjFor(Clean)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotCleanMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "CleanWith" action 
     */
    dobjFor(CleanWith)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotCleanMsg); }
    }
    iobjFor(CleanWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotCleanWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "SitOn" action 
     */
    dobjFor(SitOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotSitOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "LieOn" action 
     */
    dobjFor(LieOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotLieOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "StandOn" action 
     */
    dobjFor(StandOn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotStandOnMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Board" action 
     */
    dobjFor(Board)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotBoardMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Get out of" (unboard) action 
     */
    dobjFor(GetOutOf)
    {
        verify() { illogical(&cannotUnboardMsg); }
    }

    /*
     *   "Get off of" action 
     */
    dobjFor(GetOffOf)
    {
        verify() { illogical(&cannotGetOffOfMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Fasten" action 
     */
    dobjFor(Fasten)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotFastenMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Fasten to" action 
     */
    dobjFor(FastenTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotFastenMsg); }
    }
    iobjFor(FastenTo)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotFastenToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Unfasten" action 
     */
    dobjFor(Unfasten)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnfastenMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Unfasten from" action 
     */
    dobjFor(UnfastenFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnfastenMsg); }
    }
    iobjFor(UnfastenFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnfastenFromMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "PlugIn" action 
     */
    dobjFor(PlugIn)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPlugInMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "PlugInto" action 
     */
    dobjFor(PlugInto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPlugInMsg); }
    }
    iobjFor(PlugInto)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPlugInToMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Unplug" action 
     */
    dobjFor(Unplug)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnplugMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "UnplugFrom" action 
     */
    dobjFor(UnplugFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnplugMsg); }
    }
    iobjFor(UnplugFrom)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnplugFromMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Screw" action 
     */
    dobjFor(Screw)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotScrewMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "ScrewWith" action 
     */
    dobjFor(ScrewWith)
    {
        preCond = [iobjTouchObj]
        verify() { illogical(&cannotScrewMsg); }
    }
    iobjFor(ScrewWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotScrewWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Unscrew" action 
     */
    dobjFor(Unscrew)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotUnscrewMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "UnscrewWith" action 
     */
    dobjFor(UnscrewWith)
    {
        preCond = [iobjTouchObj]
        verify() { illogical(&cannotUnscrewMsg); }
    }
    iobjFor(UnscrewWith)
    {
        preCond = [objHeld]
        verify() { illogical(&cannotUnscrewWithMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   "Enter" 
     */
    dobjFor(Enter)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotEnterMsg); }
    }

    /* 
     *   "Go through" 
     */
    dobjFor(GoThrough)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotGoThroughMsg); }
    }

    /* -------------------------------------------------------------------- */
    /*
     *   Push in Direction action - this is for commands like "push
     *   boulder north" or "drag sled into cave". 
     */
    dobjFor(PushTravel)
    {
        preCond = [touchObj]
        verify() { illogical(&cannotPushTravelMsg); }
    }

    /*   
     *   For all of the two-object forms, map these using our general
     *   push-travel mapping.  We do all of this mapping here, rather than
     *   in the action definition, so that individual objects can change
     *   the meanings of these verbs for special cases as appropriate.  
     */
    mapPushTravelHandlers(PushTravelThrough, GoThrough)
    mapPushTravelHandlers(PushTravelEnter, Enter)
    mapPushTravelHandlers(PushTravelGetOutOf, GetOutOf)
    mapPushTravelHandlers(PushTravelClimbUp, ClimbUp)
    mapPushTravelHandlers(PushTravelClimbDown, ClimbDown)
;

