thing.t

documentation
 #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)
 
     /*
      *   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_[name] == self)
             langMessageBuilder.nameTable_.removeElement(name);
 
         /* 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 = getPOVActor();
         
         /* 
          *   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(getPOVActor()); }
 
     /*
      *   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 let our location handle it, if we have one.
      */
     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);
     }
 
     /*
      *   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)
 
     /*
      *   By default, an object containing the player character will forward
      *   the roomDaemon() call up to the container. 
      */
     roomDaemon()
     {
         if (location != nil)
             location.roomDaemon();
     }
 
     /* 
      *   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 */
         return location.getDropDestination(obj, path);
     }
 
     /*
      *   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. 
                  */
                 oldList.append(saveLocation());
                 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 make 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);
     }
 
     /*
      *   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 = getPOV().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() != getPOV().getOutermostRoom()
             && propDefined(&remoteDesc))
         {
             /* we're remote, so show our remote description */
             remoteDesc(getPOV());
         }
         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, getPOV()))
         {
             /* 
              *   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 = getPOV().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, getPOV()))
         {
             /* 
              *   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 = getPOV().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 = getPOV().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, getPOV()))
         {
             /* 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 = getPOV().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()
         {
             /* treat this like a regular "take" */
             checkDobjTake();
         }
         action()
         {
             /* treat this like a regular "take" */
             actionDobjTake();
         }
     }
     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)
 ;
 
TADS 3 Library Manual
Generated on 9/15/2006 from TADS version 3.0.12