extras.t

documentation
 #charset "us-ascii"
 
 /* 
  *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
  *   
  *   TADS 3 Library - extras: special-purpose object classes
  *   
  *   This module defines classes for specialized simulation objects.
  *   
  *   Portions are based on original work by Eric Eve, incorporated by
  *   permission.  
  */
 
 /* include the library header */
 #include "adv3.h"
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "complex" container is an object that can have multiple kinds of
  *   contents simultaneously.  For example, a complex container could act
  *   as both a surface, so that some objects are sitting on top of it, and
  *   simultaneously as a container, with objects inside.
  *   
  *   The standard containment model only allows one kind of containment per
  *   container, because the nature of the containment is a feature of the
  *   container itself.  The complex container handles multiple simultaneous
  *   containment types by using one or more sub-containers: for example, if
  *   we want to be able to act as both a surface and a regular container,
  *   we use two sub-containers, one of class Surface and one of class
  *   Container, to hold the different types of contents.  When we need to
  *   perform an operation specific to a certain containment type, we
  *   delegate the operation to the sub-container of the appropriate type.
  *   
  *   Note that the complex container itself treats its direct contents as
  *   components, so any component parts can be made direct contents of the
  *   complex container object.
  *   
  *   If you want to include objects in your source code that are initially
  *   located within the component sub-containers, define them as directly
  *   within the ComplexContainer object, but give each one a 'subLocation'
  *   property set to the property of the component sub-container that will
  *   initially contain it.  For example, here's how you'd place a blanket
  *   inside a washing machine, and a laundry basket on top of it:
  *   
  *.  + washingMachine: ComplexContainer 'washing machine' 'washing machine'
  *.    subContainer: ComplexComponent, Container { etc }
  *.    subSurface: ComplexComponent, Surface { etc }
  *.  ;
  *.  
  *.  ++ Thing 'big cotton blanket' 'blanket'
  *.    subLocation = &subContainer
  *.  ;
  *.  
  *.  ++ Container 'laundry basket' 'laundry basket'
  *.    subLocation = &subSurface
  *.  ;
  *   
  *   The subLocation setting is only used for initialization, and we
  *   automatically set it to nil right after we use it to set up the
  *   initial location.  If you want to move something into one of the
  *   sub-containers on the fly, simply refer to the desired component
  *   directly:
  *   
  *   pants.moveInto(washingMachine.subContainer); 
  */
 class ComplexContainer: Thing
     /*
      *   Our inner container, if any.  This is a "secret" object (in other
      *   words, it doesn't appear to players as a separate named object)
      *   that we use to store the contents that are meant to be within the
      *   complex container.  If this is to be used, it should be set to a
      *   Container object - the most convenient way to do this is by using
      *   the nested object syntax to define a ComplexComponent Container
      *   instance, like so:
      *   
      *   washingMachine: ComplexContainer
      *.    subContainer: ComplexComponent, Container { etc }
      *.  ;
      *   
      *   Note that we use the ComplexComponent class (as well as
      *   Container) for the sub-container object.  This makes the
      *   sub-container automatically use the name of its enclosing object
      *   in messages (in this case, the sub-container will use the same
      *   name as the washing machine).
      *   
      *   Note that the sub-containers don't have to be of class
      *   ComplexComponent, but using that class makes your job a little
      *   easier because the class sets the location and naming
      *   automatically.  If you prefer to define your sub-containers as
      *   separate objects, not nested in the ComplexContainer's
      *   definition, there's no need to make them ComplexComponents; just
      *   make them ordinary Component objects.
      *   
      *   If this property is left as nil, then we don't have an inner
      *   container.  
      */
     subContainer = nil
 
     /*
      *   Our inner surface, if any.  This is a secret object like the
      *   inner container; this object acts as our surface. 
      */
     subSurface = nil
 
     /*
      *   Our underside, if any.  This is a secret object like the inner
      *   container; this object can act as the space underneath us, or as
      *   our bottom surface. 
      */
     subUnderside = nil
 
     /*
      *   Our rear surface or container, if any.  This is a secret internal
      *   object like the inner container; this object can act as our back
      *   surface, or as the space just behind us.  
      */
     subRear = nil
 
     /* 
      *   Show our status.  We'll show the status for each of our
      *   sub-objects, so that we list any contents of our sub-container or
      *   sub-surface along with our description. 
      */
     examineStatus()
     {
         /* if we have a sub-container, show its status */
         if (subContainer != nil)
             subContainer.examineStatus();
 
         /* if we have a sub-surface, show its status */
         if (subSurface != nil)
             subSurface.examineStatus();
 
         /* if we have a sub-rear, show its status */
         if (subRear != nil)
             subRear.examineStatus();
 
         /* if we have a sub-underside, show its status */
         if (subUnderside != nil)
             subUnderside.examineStatus();
     }
 
     /* 
      *   In most cases, the open/closed and locked/unlocked status of a
      *   complex container refer to the status of the sub-container.  
      */
     isOpen = (subContainer != nil ? subContainer.isOpen : inherited)
     isLocked = (subContainer != nil ? subContainer.isLocked : inherited)
 
     makeOpen(stat)
     {
         if (subContainer != nil)
             subContainer.makeOpen(stat);
         else
             inherited(stat);
     }
 
     makeLocked(stat)
     {
         if (subContainer != nil)
             subContainer.makeLocked(stat);
         else
             inherited(stat);
     }
         
 
     /* 
      *   route all commands that treat us as a container to our
      *   sub-container object 
      */
     dobjFor(Open) maybeRemapTo(subContainer != nil, Open, subContainer)
     dobjFor(Close) maybeRemapTo(subContainer != nil, Close, subContainer)
     dobjFor(LookIn) maybeRemapTo(subContainer != nil, LookIn, subContainer)
     iobjFor(PutIn) maybeRemapTo(subContainer != nil,
                                 PutIn, DirectObject, subContainer)
     dobjFor(Lock) maybeRemapTo(subContainer != nil, Lock, subContainer)
     dobjFor(LockWith) maybeRemapTo(subContainer != nil,
                                    LockWith, subContainer, IndirectObject)
     dobjFor(Unlock) maybeRemapTo(subContainer != nil, Unlock, subContainer)
     dobjFor(UnlockWith) maybeRemapTo(subContainer != nil,
                                      UnlockWith, subContainer, IndirectObject)
 
     /* route commands that treat us as a surface to our sub-surface */
     iobjFor(PutOn) maybeRemapTo(subSurface != nil,
                                 PutOn, DirectObject, subSurface)
 
     /* route commands that affect our underside to our sub-underside */
     iobjFor(PutUnder) maybeRemapTo(subUnderside != nil,
                                    PutUnder, DirectObject, subUnderside)
     dobjFor(LookUnder) maybeRemapTo(subUnderside != nil,
                                     LookUnder, subUnderside)
 
     /* route commands that affect our rear to our sub-rear-side */
     iobjFor(PutBehind) maybeRemapTo(subRear != nil,
                                     PutBehind, DirectObject, subRear)
     dobjFor(LookBehind) maybeRemapTo(subRear != nil, LookBehind, subRear)
 
     /*
      *   Get a list of objects suitable for matching ALL in TAKE ALL FROM
      *   <self>.  By default, if we have a sub-surface and/or
      *   sub-container, we return everything in scope that's inside either
      *   one of those.  Otherwise, if we have a sub-rear-surface and/or an
      *   underside, we'll return everything from those.  
      */
     getAllForTakeFrom(scopeList)
     {
         local containers;
 
         /* 
          *   Make a list of the containers in which we're going to look.
          *   If we have a sub-container or sub-surface, look only in those.
          *   Otherwise, if we have a rear surface or underside, look in
          *   those. 
          */
         containers = [];
         if (subContainer != nil)
             containers += subContainer;
         if (subSurface != nil)
             containers += subSurface;
         if (containers == [])
         {
             if (subRear != nil)
                 containers += subRear;
             if (subUnderside != nil)
                 containers += subUnderside;
         }
 
         /* 
          *   return the list of everything in scope that's directly in one
          *   of the selected containers, but isn't a component of its
          *   direct container 
          */
         return scopeList.subset(
             {x: (x != self
                  && containers.indexOf(x) == nil
                  && containers.indexWhich(
                      {c: x.isDirectlyIn(c) && !x.isComponentOf(c)}) != nil)});
     }
 
     /*
      *   Add an object to my contents.  If the object has a subLocation
      *   setting, take it as indicating which of my subcontainers is to
      *   contain the object.  
      */
     addToContents(obj)
     {
         local sub;
         
         /* 
          *   if the object has a subLocation, add it to my appropriate
          *   component object; if not, add to my own contents as usual 
          */
         if ((sub = obj.subLocation) != nil)
         {
             /* 
              *   It specifies a subLocation - add it to the corresponding
              *   component's contents.  Note that subLocation is a property
              *   pointer - &subContainer for my container component,
              *   &subSurface for my surface component, etc.  
              */
             self.(sub).addToContents(obj);
 
             /* 
              *   The object's present location is merely for set-up
              *   purposes, so that the '+' object definition notation can
              *   be used to give the object its initial location.  The
              *   object really wants to be in the sub-container, to whose
              *   contents list we've just added it.  Set its location to
              *   the sub-container.  
              */
             obj.location = self.(sub);
 
             /*
              *   Now that we've moved the object into its sub-location,
              *   forget the subLocation setting, since this property is
              *   only for initialization. 
              */
             obj.subLocation = nil;
         }
         else
         {
             /* there's no subLocation, so use the default handling */
             inherited(obj);
         }
     }
 
     /*
      *   If we have any SpaceOverlay children, abandon the contents of the
      *   overlaid spaces as needed. 
      */
     mainMoveInto(newCont)
     {
         /* 
          *   If any of our components are SpaceOverlays, notify them.  We
          *   only worry about the rear and underside components, since it's
          *   never appropriate for our container and surface components to
          *   act as space overlays. 
          */
         notifyComponentOfMove(subRear);
         notifyComponentOfMove(subUnderside);
 
         /* do the normal work */
         inherited(newCont);
     }
 
     /* 
      *   if we're being pushed into a new location (as a PushTraveler),
      *   abandon the contents of any SpaceOverlay components 
      */
     beforeMovePushable(traveler, connector, dest)
     {
         /* 
          *   notify our SpaceOverlay components that we're being moved, if
          *   we're going to end up in a new location 
          */
         if (dest != location)
         {
             /* notify our rear and underside components of the move */
             notifyComponentOfMove(subRear);
             notifyComponentOfMove(subUnderside);
         }
         
         /* do the normal work */
         inherited(traveler, connector, dest);
     }
 
     /* 
      *   if the given component is a SpaceOverlay, notify it that we're
      *   moving, so that it can abandon its contents as needed 
      */
     notifyComponentOfMove(sub)
     {
         /* if it's a SpaceOverlay, abandon its contents if necessary */
         if (sub != nil && sub.ofKind(SpaceOverlay))
             sub.abandonContents();
     }
 
     /* pass bag-of-holding operations to our sub-container */
     tryPuttingObjInBag(target)
     {
         /* if we have a subcontainer, let it handle the operation */
         return (subContainer != nil
                 ? subContainer.tryPuttingObjInBag(target)
                 : nil);
     }
 
     /* pass implicit PUT x IN self operations to our subcontainer */
     tryMovingObjInto(obj)
     {
         /* if we have a subcontainer, let it handle the operation */
         return (subContainer != nil
                 ? subContainer.tryMovingObjInto(obj)
                 : nil);
     }
 ;
 
 /* 
  *   we don't actually define any subLocation property values anywhere, so
  *   declare it to make sure the compiler knows it's a property name 
  */
 property subLocation;
 
 /*
  *   A component object of a complex container.  This class can be used as
  *   a mix-in for sub-objects of a complex container (the subContainer or
  *   subSurface) defined as nested objects.
  *   
  *   This class is based on Component, which is suitable for complex
  *   container sub-objects because it makes them inseparable from the
  *   complex container.  It's also based on NameAsParent, which makes the
  *   object automatically use the same name (in messages) as the lexical
  *   parent object.  This is usually what one wants for a sub-object of a
  *   complex container, because it makes the sub-object essentially
  *   invisible to the user by referring to the sub-object in messages as
  *   though it were the complex container itself: "The washing machine
  *   contains...".
  *   
  *   This class also automatically initializes our location to our lexical
  *   parent, during the pre-initialization process.  Any of these that are
  *   dynamically created at run-time (using 'new') must have their
  *   locations set manually, because initializeLocation() won't be called
  *   automatically in those cases.  
  */
 class ComplexComponent: Component, NameAsParent
     initializeLocation()
     {
         /* set our location to our lexical parent */
         location = lexicalParent;
 
         /* inherit default so we initialize our container's 'contents' list */
         inherited();
     }
 
     /* 
      *   Get our "identity" object.  We take our identity from our parent
      *   object, if we have one.  Note that our identity isn't simply our
      *   parent, but rather is our parent's identity, recursively defined.
      */
     getIdentityObject()
     {
         return (location != nil ? location.getIdentityObject() : self);
     }
 
     /* don't participate in 'all', since we're a secret internal object */
     hideFromAll(action) { return true; }
 ;
 
 /*
  *   A container door.  This is useful for cases where you want to create
  *   the door to a container as a separate object in its own right.  
  */
 class ContainerDoor: Component
     /*
      *   In most cases, you should create a ContainerDoor as a component of
      *   a ComplexContainer.  It's usually necessary to use a
      *   ComplexContainer in order to use a door, since the door has to go
      *   somewhere, and it can't go inside the container it controls
      *   (because if it were inside, it wouldn't be accessible when the
      *   container is closed).
      *   
      *   By default, we assume that our immediate location is a complex
      *   container, and its subContainer is the actual container for which
      *   we're the door.  You can override this property to create a
      *   different relationship if necessary.  
      */
     subContainer = (location.subContainer)
 
     /* we're open if our associated sub-container is open */
     isOpen = (subContainer.isOpen)
 
     /* our status description mentions our open status */
     examineStatus()
     {
         /* add our open status */
         say(isOpen
             ? gLibMessages.currentlyOpen : gLibMessages.currentlyClosed);
 
         /* add the base class behavior */
         inherited();
     }
 
     /* looking in or behind a door is like looking inside the container */
     dobjFor(LookIn) remapTo(LookIn, subContainer)
     dobjFor(LookBehind) remapTo(LookIn, subContainer)
 
     /* door-like operations on the door map to the container */
     dobjFor(Open) remapTo(Open, subContainer)
     dobjFor(Close) remapTo(Close, subContainer)
     dobjFor(Lock) remapTo(Lock, subContainer)
     dobjFor(LockWith) remapTo(LockWith, subContainer, IndirectObject)
     dobjFor(Unlock) remapTo(Unlock, subContainer)
     dobjFor(UnlockWith) remapTo(UnlockWith, subContainer, IndirectObject)
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "space overlay" is a special type of container whose contents are
  *   supposed to be adjacent to the container object (i.e., self), but are
  *   not truly contained in the usual sense.  This is used to model spatial
  *   relationships such as UNDER and BEHIND, which aren't directly
  *   supported in the normal containment model.
  *   
  *   The special feature of a space overlay is that the contents aren't
  *   truly attached to the container object, so they don't move with it the
  *   way that the contents of an ordinary container do.  For example,
  *   suppose we have a space overlay representing a bookcase and the space
  *   behind it, so that we can hide a painting behind the bookcase: in this
  *   case, moving the bookcase should leave the painting where it was,
  *   because it was just sitting there in that space.  In the real world,
  *   of course, the painting was sitting on the floor all along, so moving
  *   the bookcase would have no effect on it; but our spatial relationship
  *   model isn't quite as good as reality's, so we have to resort to an
  *   extra fix-up step.  Specifically, when we move a space overlay, we
  *   always check to see if its contents need to be relocated to the place
  *   where they were really supposed to be all along.  
  */
 class SpaceOverlay: BulkLimiter
     /* 
      *   If we move this object, the objects we contain might stay put
      *   rather than moving along with the container.  For example, if we
      *   represent the space behind a bookcase, moving the bookcase would
      *   leave objects that were formerly behind the bookcase just sitting
      *   on the floor (or attached to the wall, or whatever).  
      */
     mainMoveInto(newContainer)
     {
         /* check to see if our objects need to be left behind */
         abandonContents();
 
         /* now do the normal work */
         inherited(newContainer);
     }
 
     /* 
      *   when we're being pushed to a new location via push-travel, abandon
      *   our contents before we're moved 
      */
     beforeMovePushable(traveler, connector, dest)
     {
         /* check to see if our objects need to be left behind */
         if (dest != getIdentityObject().location)
             abandonContents();
 
         /* do the normal work */
         inherited(traveler, connector, dest);
     }
 
     /* 
      *   abandonLocation is where the things under me end up when I'm
      *   moved.
      *   
      *   An Underside or RearContainer represents an object that has a
      *   space underneath or behind it, respectively, but the space itself
      *   isn't truly part of the container object (i.e., self).  This
      *   means that when the container moves, the objects under/behind it
      *   shouldn't move.  For example, if there's a box under a bed,
      *   moving the bed out of the room should leave the box sitting on
      *   the floor where the bed used to be.
      *   
      *   By default, our abandonLocation is simply the location of our
      *   "identity object" - that is, the location of our nearest
      *   enclosing object that isn't a component.
      *   
      *   This can be overridden if the actual abandonment location should
      *   be somewhere other than our assembly location.  In addition, you
      *   can set this to nil to indicate that objects under/behind me will
      *   NOT be abandoned when I move; instead, they'll simply stay with
      *   me, as though they're attached to my underside/back surface.  
      */
     abandonLocation = (getIdentityObject().location)
     
     /* 
      *   By default we list our direct contents the first time we're
      *   moved, and ONLY the first time.  If alwaysListOnMove is
      *   overridden to true, then we'll list our contents EVERY time we're
      *   moved.  If neverListOnMove is set to true, then we'll NEVER list
      *   our contents automatically when moved; this can be used in cases
      *   where the game wants to produce its own listing explicitly,
      *   rather than using the default listing we generate.  (Obviously,
      *   setting both 'always' and 'never' is meaningless, but in case
      *   you're wondering, 'never' overrides 'always' in this case.)
      *   
      *   Setting abandonLocation to nil overrules alwaysListOnMove: if
      *   there's no abandonment, then we consider nothing to be revealed
      *   when we're moved, since my contents move along with me.  
      */
     alwaysListOnMove = nil
     neverListOnMove = nil
 
     /*
      *   The lister we use to describe the objects being revealed when we
      *   move the SpaceOverlay object and abandon the contents.  Each
      *   concrete kind of SpaceOverlay must provide a lister that uses
      *   appropriate language; the list should be roughly of the form
      *   "Moving the armoire reveals a rusty can underneath."  Individual
      *   objects can override this to customize the message further.  
      */
     abandonContentsLister = nil
 
     /*
      *   Abandon my contents when I'm moved.  This is called whenever we're
      *   moved to a new location, to take care of leaving behind the
      *   objects that were formerly under me.
      *   
      *   We'll move my direct contents into abandonLocation, unless that's
      *   set to nil.  We don't move any Component objects within me, since
      *   we assume those to be attached.  
      */
     abandonContents()
     {
         local dest;
         
         /* 
          *   if there's no abandonment location, our contents move with us,
          *   so there's nothing to do 
          */
         if ((dest = abandonLocation) == nil)
             return;
 
         /* 
          *   If we've never been moved before, or we always reveal my
          *   contents when moved, list our contents now.  In any case, if
          *   we *never* list on move, don't generate the listing.  
          */
         if ((alwaysListOnMove || !getIdentityObject().moved)
             && !neverListOnMove)
         {
             local marker1, marker2;
             
             /*
              *   We want to generate a listing of what is revealed by
              *   moving the object, which we can do by generating a
              *   listing of what we would normally see by looking in the
              *   overlay interior.  We want the listing as it stands now,
              *   but in most cases, we don't actually want to generate the
              *   list quite yet, because we want the action that's moving
              *   the object to complete and show all of its messages first.
              *   
              *   However, if the action leaves the actor in a new
              *   location, do generate the listing before the rest of the
              *   action output, since the listing won't make any sense
              *   after we've moved to (and displayed the description of)
              *   the new location.
              *   
              *   To accomplish all of this, generate the listing now,
              *   before the rest of the action output, but insert special
              *   report markers into the transcript before and after the
              *   listing.  Then, register to receive after-action
              *   notification; at the end of the action, we'll go back and
              *   move the range of transcript output between the markers
              *   to the end of the command's transcript entries, if we
              *   haven't moved to a new room.
              *   
              *   One final complication: we don't want our listing here to
              *   hide any default report from the main command, so run it
              *   as a sub-action.  A sub-action doesn't override the
              *   visibility of its parent's default report.  
              */
 
             /* first, add a marker to the transcript before the listing */
             marker1 = gTranscript.addMarker();
 
             /* generate the listing, using a generic sub-action context */
             withActionEnv(Action, gActor, {: listContentsForMove() });
 
             /* add another transcript marker after the listing */
             marker2 = gTranscript.addMarker();
 
             /* 
              *   create our special handler object to receive notification
              *   at the end of the command - it'll move the reports to the
              *   end of the command output if need be 
              */
             new SpaceOverlayAbandonFinisher(marker1, marker2);
         }
 
         /* now move my non-Component contents to the abandonment location */
         foreach(obj in contents)
         {
             /* if it's not a component, move it */
             if(!obj.ofKind(Component))
                 obj.moveInto(dest);
         }
     }
 
     /* 
      *   List our contents for moving the object.  By default, we examine
      *   our interior using our abandonContentsLister.  
      */
     listContentsForMove()
     {
         /* examine our contents with the abandonContentsLister */
         examineInteriorWithLister(abandonContentsLister);
     }
 ;
 
 /* 
  *   Space Overlay Abandon Finisher - this is an internal object that we
  *   create in SpaceOverlay.abandonContents().  Its purpose is to receive
  *   an afterAction notification and clean up the report order if
  *   necessary. 
  */
 class SpaceOverlayAbandonFinisher: object
     construct(m1, m2)
     {
         /* remember the markers */
         marker1 = m1;
         marker2 = m2;
 
         /* remember the actor's starting location */
         origLocation = gActor.location;
 
         /* register for afterAction notification */
         gAction.addBeforeAfterObj(self);
     }
 
     /* the transcript markers identifying the listing reports */
     marker1 = nil
     marker2 = nil
 
     /* the actor's location at the time we generated the listing */
     origLocation = nil
 
     /* receive our after-action notification */
     afterAction()
     {
         /* 
          *   If the actor hasn't changed locations, move the reports we
          *   generated for the listing to the end of the transcript. 
          */
         if (gActor.location == origLocation)
             gTranscript.moveRangeAppend(marker1, marker2);
     }
 ;
 
 /*
  *   An "underside" is a special type of container that describes its
  *   contents as being under the object.  This is appropriate for objects
  *   that have a space underneath, such as a bed or a table.  
  */
 class Underside: SpaceOverlay
     /*
      *   Can actors put new objects under self, using the PUT UNDER
      *   command?  By default, we allow it.  Override this property to nil
      *   if new objects cannot be added by player commands.  
      */
     allowPutUnder = true
 
     /* we need to LOOK UNDER this object to see its contents */
     nestedLookIn() { nestedAction(LookUnder, self); }
 
     /* use custom contents listers, for our special "under" wording */
     contentsLister = undersideContentsLister
     descContentsLister = undersideDescContentsLister
     lookInLister = undersideLookUnderLister
     inlineContentsLister = undersideInlineContentsLister
     abandonContentsLister = undersideAbandonContentsLister
     
     /* customize the message for taking something from that's not under me */
     takeFromNotInMessage = &takeFromNotUnderMsg
  
     /* customize the message indicating another object is already in me */
     circularlyInMessage = &circularlyUnderMsg
  
     /* message phrase for objects put under me */
     putDestMessage = &putDestUnder
  
     /* message when we don't have room to put another object under me */
     tooFullMsg = &undersideTooFull
 
     /* message when an object is too large (all by itself) to fit under me */
     tooLargeForContainerMsg = &tooLargeForUndersideMsg
 
     /* can't put self under self */
     cannotPutInSelfMsg = &cannotPutUnderSelfMsg
 
     /* can't put something under me when it's already under me */
     alreadyPutInMsg = &alreadyPutUnderMsg
 
 
     /* -------------------------------------------------------------------- */
     /*
      *   Handle putting things under me 
      */
     iobjFor(PutUnder)
     {
         verify()
         {
             /* use the standard put-in-interior verification */
             verifyPutInInterior();
         }
         check()
         {
             /* only allow it if PUT UNDER commands are allowed */
             if (!allowPutUnder)
             {
                 reportFailure(&cannotPutUnderMsg);
                 exit;
             }
         }
         action()
         {
             /* move the direct object onto me */
             gDobj.moveInto(self);
              
             /* issue our default acknowledgment */
             defaultReport(&okayPutUnderMsg);
         }
     }
     
     /*
      *   Looking "under" a surface simply shows the surface's contents. 
      */
     dobjFor(LookUnder)
     {
         verify() { }
         action() { examineInterior(); }
     }
 ;
 
 /*
  *   A special kind of Underside that only accepts specific contents.
  */
 class RestrictedUnderside: RestrictedHolder, Underside
     /* 
      *   A message that explains why the direct object can't be put under
      *   this object.  In most cases, the rather generic default message
      *   should be overridden to provide a specific reason that the dobj
      *   can't be put under me.  The rejected object is provided as a
      *   parameter in case the message needs to vary by object, but we
      *   ignore this and just use a single blanket failure message by
      *   default.  
      */
     cannotPutUnderMsg(obj) { return &cannotPutUnderRestrictedMsg; }
 
     /* override PutUnder to enforce our contents restriction */
     iobjFor(PutUnder) { check() { checkPutDobj(&cannotPutUnderMsg); } }
 ;
 
 /*
  *   A "rear container" is similar to an underside: it models the space
  *   behind an object.  
  */
 class RearContainer: SpaceOverlay
     /*
      *   Can actors put new objects behind self, using the PUT BEHIND
      *   command?  By default, we allow it.  Override this property to nil
      *   if new objects cannot be added by player commands.  
      */
     allowPutBehind = true
      
     /* we need to LOOK BEHIND this object to see its contents */
     nestedLookIn() { nestedAction(LookBehind, self); }
 
     /* use custom contents listers */
     contentsLister = rearContentsLister
     descContentsLister = rearDescContentsLister
     lookInLister = rearLookBehindLister
     inlineContentsLister = rearInlineContentsLister 
     abandonContentsLister = rearAbandonContentsLister
 
     /* the message for taking things from me that aren't behind me */
     takeFromNotInMessage = &takeFromNotBehindMsg
 
     /* 
      *   my message indicating that another object x cannot be put into me
      *   because I'm already in x 
      */
     circularlyInMessage = &circularlyBehindMsg
  
     /* message phrase for objects put under me */
     putDestMessage = &putDestBehind
  
     /* message when we're too full for another object */
     tooFullMsg = &rearTooFullMsg
 
     /* message when object is too large to fit behind me */
     tooLargeForContainerMsg = &tooLargeForRearMsg
 
     /* customize the verification messages */
     cannotPutInSelfMsg = &cannotPutBehindSelfMsg
     alreadyPutInMsg = &alreadyPutBehindMsg
 
     /* -------------------------------------------------------------------- */
     /*
      *   Handle the PUT UNDER command
      */
     iobjFor(PutBehind)
     {
         verify()  { verifyPutInInterior(); }
         check()
         {
             /* only allow it if PUT BEHIND commands are allowed */
             if (!allowPutBehind)
             {
                 reportFailure(&cannotPutBehindMsg);
                 exit;
             }
         }
         action()
         {
             /* move the direct object behind me */
             gDobj.moveInto(self);
             
             /* issue our default acknowledgment */
             defaultReport(&okayPutBehindMsg);
         }
     }
     
     /*
      *   Looking "behind" a surface simply shows the surface's contents. 
      */
     dobjFor(LookBehind)
     {
         verify() { }
         action() { examineInterior(); }
     }
 ;
 
 /*
  *   A special kind of RearContainer that only accepts specific contents.
  */
 class RestrictedRearContainer: RestrictedHolder, RearContainer
     /* 
      *   A message that explains why the direct object can't be put behind
      *   this object.  In most cases, the rather generic default message
      *   should be overridden to provide a specific reason that the dobj
      *   can't be put behind me.  The rejected object is provided as a
      *   parameter in case the message needs to vary by object, but we
      *   ignore this and just use a single blanket failure message by
      *   default.  
      */
     cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
 
     /* override PutBehind to enforce our contents restriction */
     iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
 ;
 
 /*
  *   A "rear surface" is essentially the same as a "rear container," but
  *   models the contents as being attached to the back of the object rather
  *   than merely sitting behind it.
  *   
  *   The only practical difference between the "container" and the
  *   "surface" is that moving a surface moves its contents along with it,
  *   whereas moving a container abandons the contents, leaving them behind
  *   where the container used to be.  
  */
 class RearSurface: RearContainer
     /*
      *   We're a surface, not a space, so our contents stay attached when
      *   we move.  
      */
     abandonLocation = nil
 ;
 
 /*
  *   A restricted-contents RearSurface
  */
 class RestrictedRearSurface: RestrictedHolder, RearSurface
     /* explain the problem */
     cannotPutBehindMsg(obj) { return &cannotPutBehindRestrictedMsg; }
 
     /* override PutBehind to enforce our contents restriction */
     iobjFor(PutBehind) { check() { checkPutDobj(&cannotPutBehindMsg); } }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A "stretchy container."  This is a simple container subclass whose
  *   external bulk changes according to the bulks of the contents.  
  */
 class StretchyContainer: Container
     /* 
      *   Our minimum bulk.  This is the minimum bulk we'll report, even
      *   when the aggregate bulks of our contents are below this limit. 
      */
     minBulk = 0
 
     /* get my total external bulk */
     getBulk()
     {
         local tot;
 
         /* start with my own intrinsic bulk */
         tot = bulk;
 
         /* add the bulk contribution from my contents */
         tot += getBulkForContents();
 
         /* return the total, but never less than the minimum */
         return tot >= minBulk ? tot : minBulk;
     }
 
     /*
      *   Calculate the contribution to my external bulk of my contents.
      *   The default for a stretchy container is to conform exactly to the
      *   contents, as though the container weren't present at all, hence
      *   we simply sum the bulks of our contents.  Subclasses can override
      *   this to define other aggregate bulk effects as needed.  
      */
     getBulkForContents()
     {
         local tot;
         
         /* sum the bulks of the items in our contents */
         tot = 0;
         foreach (local cur in contents)
             tot += cur.getBulk();
 
         /* return the total */
         return tot;
     }
 
     /*
      *   Check what happens when a new object is inserted into my
      *   contents.  This is called with the new object already tentatively
      *   added to my contents, so we can examine our current status to see
      *   if everything works.
      *   
      *   Since we can change our own size when a new item is added to our
      *   contents, we'll trigger a full bulk change check. 
      */
     checkBulkInserted(insertedObj)
     {
         /* 
          *   inherit the normal handling to ensure that the new object
          *   fits within this container 
          */
         inherited(insertedObj);
 
         /* 
          *   since we can change our own shape when items are added to our
          *   contents, trigger a full bulk check on myself 
          */
         checkBulkChange();
     }
 
     /* 
      *   Check a bulk change of one of my direct contents.  Since my own
      *   bulk changes whenever the bulk of one of my contents changes, we
      *   must propagate the bulk change of our contents as a change in our
      *   own bulk. 
      */
     checkBulkChangeWithin(changingObj)
     {
         /* 
          *   This might cause a change in my own bulk, since my bulk
          *   depends on the bulks of my contents.  When this is called,
          *   obj is already set to indicate its new bulk; since we
          *   calculate our own bulk by looking at our contents' bulks,
          *   this means that our own getBulk will now report the latest
          *   value including obj's new bulk. 
          */
         checkBulkChange();
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   "Bag of Holding."  This is a mix-in that actively moves items from the
  *   holding actor's direct inventory into itself when the actor's hands
  *   are too full.
  *   
  *   The bag of holding offers a solution to the conflict between "realism"
  *   and playability.  On the one hand, in real life, you can only hold so
  *   many items at once, so at first glance it seems a simulation ought to
  *   have such a limit in order to be more realistic.  On the other hand,
  *   most players justifiably hate having to deal with a carrying limit,
  *   because it forces the player to spend a lot of time doing tedious
  *   inventory management.
  *   
  *   The Bag of Holding is a compromise solution.  The concept is borrowed
  *   from live role-playing games, where it's usually a magical item that
  *   can hold objects of unlimited size and weight, thereby allowing
  *   characters to transport impossibly large objects.  In text IF, a bag
  *   of holding isn't usually magical - it's usually just something like a
  *   large backpack, or a trenchcoat with lots of pockets.  And it usually
  *   isn't meant as a solution to an obvious puzzle; rather, it's meant to
  *   invisibly prevent inventory management from becoming a puzzle in the
  *   first place, by shuffling objects out of the PC's hands automatically
  *   to free up space as needed.
  *   
  *   This Bag of Holding implementation works by automatically moving
  *   objects from an actor's hands into the bag object, whenever the actor
  *   needs space to pick up a new item.  Whenever an action has a
  *   "roomToHoldObj" precondition, the precondition will automatically look
  *   for a BagOfHolding object within the actor's inventory, and then move
  *   as many items as necessary from the actor's hands to the bag.  
  */
 class BagOfHolding: object
     /*
      *   Get my bags of holding.  Since we are a bag of holding, we'll add
      *   ourselves to the vector, then we'll inherit the normal handling
      *   to pick up our contents. 
      */
     getBagsOfHolding(vec)
     {
         /* we're a bag of holding */
         vec.append(self);
 
         /* inherit the normal handling */
         inherited(vec);
     }
 
     /*
      *   Get my "affinity" for the given object.  This is an indication of
      *   how strongly this bag wants to contain the object.  The affinity
      *   is a number in arbitrary units; higher numbers indicate stronger
      *   affinities.  An affinity of zero means that the bag does not want
      *   to contain the object at all.
      *   
      *   The purpose of the affinity is to support specialized holders
      *   that are designed to hold only specific types of objects, and
      *   allow these specialized holders to implicitly gather their
      *   specific objects.  For example, a key ring might only hold keys,
      *   so it would have a high affinity for keys and a zero affinity for
      *   everything else.  A lunchbox might have a higher affinity for
      *   things like sandwiches than for anything else, but might be
      *   willing to serve as a general container for other small items as
      *   well.
      *   
      *   The units of affinity are arbitrary, but the library uses the
      *   following values for its own classes:
      *   
      *   0 - no affinity at all; the bag cannot hold the object
      *   
      *   50 - willing to hold the object, but not of the preferred type
      *   
      *   100 - default affinity; willing and able to hold the object, but
      *   just as willing to hold most other things
      *   
      *   200 - special affinity; this object is of a type that we
      *   especially want to hold
      *   
      *   We intentionally space these loosely so that games can use
      *   intermediate levels if desired.
      *   
      *   When we are looking for bags of holding to consolidate an actor's
      *   directly-held inventory, note that we always move the object with
      *   the highest bag-to-object affinity out of all of the objects
      *   under consideration.  So, if you want to give a particular kind
      *   of bag priority so that the library uses that bag before any
      *   other bag, make this routine return a higher affinity for the
      *   bag's objects than any other bags do.
      *   
      *   By default, we'll return the default affinity of 100.
      *   Specialized bags that don't hold all types of objects must
      *   override this to return zero for objects they can't hold.  
      */
     affinityFor(obj)
     {
         /* 
          *   my affinity for myself is zero, for obvious reasons; for
          *   everything else, use the default affinity 
          */
         return (obj == self ? 0 : 100);
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Keyring - a place to stash keys
  *   
  *   Keyrings have some special properties:
  *   
  *   - A keyring is a bag of holding with special affinity for keys.
  *   
  *   - A keyring can only contain keys.
  *   
  *   - Keys are considered to be on the outside of the ring, so a key can
  *   be used even if attached to the keyring (in other words, if the ring
  *   itself is held, a key attached to the ring is also considered held).
  *   
  *   - If an actor in possession of a keyring executes an "unlock" command
  *   without specifying what key to use, we will automatically test each
  *   key on the ring to find the one that works.
  *   
  *   - When an actor takes one of our keys, and the actor is in possession
  *   of this keyring, we'll automatically attach the key to the keyring
  *   immediately.  
  */
 class Keyring: BagOfHolding, Thing
     /* lister for showing our contents in-line as part of a list entry */
     inlineContentsLister = keyringInlineContentsLister
 
     /* lister for showing our contents as part of "examine" */
     descContentsLister = keyringExamineContentsLister
 
     /* 
      *   Determine if a key fits our keyring.  By default, we will accept
      *   any object of class Key.  However, subclasses might want to
      *   override this to associate particular keys with particular
      *   keyrings rather than having a single generic keyring.  To allow
      *   only particular keys onto this keyring, override this routine to
      *   return true only for the desired keys.  
      */
     isMyKey(key)
     {
         /* accept any object of class Key */
         return key.ofKind(Key);
     }
 
     /* we have high affinity for our keys */
     affinityFor(obj)
     {
         /* 
          *   if the object is one of my keys, we have high affinity;
          *   otherwise we don't accept it at all 
          */
         if (isMyKey(obj))
             return 200;
         else
             return 0;
     }
 
     /* implicitly put a key on the keyring */
     tryPuttingObjInBag(target)
     {
         /* we're a container, so use "put in" to get the object */
         return tryImplicitActionMsg(&announceMoveToBag, PutOn, target, self);
     }
 
     /* on taking the keyring, attach any loose keys */
     dobjFor(Take)
     {
         action()
         {
             /* do the normal work */
             inherited();
             
             /* get the list of loose keys */
             local lst = getLooseKeys(gActor);
 
             /* consider only the subset that are my valid keys */
             lst = lst.subset({x: isMyKey(x)});
 
             /* if there are any, move them onto the keyring */
             if (lst.length() != 0)
             {
                 /* put each loose key on the keyring */
                 foreach (local cur in lst)
                     cur.moveInto(self);
 
                 /* announce what happened */
                 extraReport(&movedKeysToKeyringMsg, self, lst);
             }
         }
     }
 
     /* 
      *   Get the loose keys in the given actor's possession.  On taking the
      *   keyring, we'll attach these loose keys to the keyring
      *   automatically.  By default, we return any keys the actor is
      *   directly holding. 
      */
     getLooseKeys(actor) { return actor.contents; }
 
     /* allow putting a key on the keyring */
     iobjFor(PutOn)
     {
         /* we can only put keys on keyrings */
         verify()
         {
             /* we'll only allow our own keys to be attached */
             if (gDobj == nil)
             {
                 /* 
                  *   we don't know the actual direct object yet, but we
                  *   can at least check to see if any of the possible
                  *   dobj's is my kind of key 
                  */
                 if (gTentativeDobj.indexWhich({x: isMyKey(x.obj_)}) == nil)
                     illogical(&objNotForKeyringMsg);
             }
             else if (!isMyKey(gDobj))
             {
                 /* the dobj isn't a valid key for this keyring */
                 illogical(&objNotForKeyringMsg);
             }
         }
         
         /* put a key on me */
         action()
         {
             /* move the key into me */
             gDobj.moveInto(self);
             
             /* show the default "put on" response */
             defaultReport(&okayPutOnMsg);
         }
     }
 
     /* treat "attach x to keyring" as "put x on keyring" */
     iobjFor(AttachTo) remapTo(PutOn, DirectObject, self)
 
     /* treat "detach x from keyring" as "take x from keyring" */
     iobjFor(DetachFrom) remapTo(TakeFrom, DirectObject, self)
 
     /* receive notification before an action */
     beforeAction()
     {
         /*
          *   Note whether or not we want to consider moving the direct
          *   object to the keyring after a "take" command.  We will
          *   consider doing so only if the direct object isn't already on
          *   the keyring - if it is, we don't want to move it back right
          *   after removing it, obviously.
          *   
          *   Skip the implicit keyring attachment if the current command
          *   is implicit, because they must be doing something that
          *   requires holding the object, in which case taking it is
          *   incidental.  It could be actively annoying to attach the
          *   object to the keyring in such cases - for example, if the
          *   command is "put key on keyring," attaching it as part of the
          *   implicit action would render the explicit command redundant
          *   and cause it to fail.  
          */
         moveAfterTake = (!gAction.isImplicit
                          && gDobj != nil
                          && !gDobj.isDirectlyIn(self));
     }
 
     /* flag: consider moving to keyring after this "take" action */
     moveAfterTake = nil
 
     /* receive notification after an action */
     afterAction()
     {
         /*
          *   If the command was "take", and the direct object was a key,
          *   and the actor involved is holding the keyring and can touch
          *   it, and the command succeeded in moving the key to the
          *   actor's direct inventory, then move the key onto the keyring.
          *   Only consider this if we decided to during the "before"
          *   notification.  
          */
         if (moveAfterTake
             && gActionIs(Take)
             && isMyKey(gDobj)
             && isIn(gActor)
             && gActor.canTouch(self)
             && gDobj.isDirectlyIn(gActor))
         {
             /* move the key to me */
             gDobj.moveInto(self);
 
             /* 
              *   Mention what we did.  If the only report for this action
              *   so far is the default 'take' response, then use the
              *   combined taken-and-attached message.  Otherwise, append
              *   our 'attached' message, which is suitable to use after
              *   other messages. 
              */
             if (gTranscript.currentActionHasReport(
                 {x: (x.ofKind(CommandReportMessage)
                      && x.messageProp_ != &okayTakeMsg)}))
             {
                 /* 
                  *   we have a non-default message already, so add our
                  *   message indicating that we added the key to the
                  *   keyring 
                  */
                 reportAfter(&movedKeyToKeyringMsg, self);
             }
             else
             {
                 /* use the combination taken-and-attached message */
                 mainReport(&takenAndMovedToKeyringMsg, self);
             }
         }
     }
 
     /* find among our keys a key that works the direct object */
     findWorkingKey(lock)
     {
         /* try each key on the keyring */
         foreach (local key in contents)
         {
             /* 
              *   if this is the key that unlocks the lock, replace the
              *   command with 'unlock lock with key' 
              */
             if (lock.keyFitsLock(key))
             {
                 /* note that we tried keys and found the right one */
                 extraReport(&foundKeyOnKeyringMsg, self, key);
                 
                 /* return the key */
                 return key;
             }
         }
 
         /* we didn't find the right key - indicate failure */
         reportFailure(&foundNoKeyOnKeyringMsg, self);
         return nil;
     }
 
     /*
      *   Append my directly-held contents to a vector when I'm directly
      *   held.  We consider all of the keys on the keyring to be
      *   effectively at the same containment level as the keyring, so if
      *   the keyring is held, so are its attached keys.  
      */
     appendHeldContents(vec)
     {
         /* append all of our contents, since they're held when we are */
         vec.appendUnique(contents);
     }
 
     /*
      *   Announce myself as a default object for an action.
      *   
      *   Do not announce a keyring as a default for "lock with" or "unlock
      *   with".  Although we can use a keyring as the indirect object of a
      *   lock/unlock command, we don't actually do the unlocking with the
      *   keyring; so, when we're chosen as the default, suppress the
      *   announcement, since it would imply that we're being used to lock
      *   or unlock something.  
      */
     announceDefaultObject(whichObj, action, resolvedAllObjects)
     {
         /* if it's not a lock-with or unlock-with, use the default message */
         if (!action.ofKind(LockWithAction)
             && !action.ofKind(UnlockWithAction))
         {
             /* for anything but our special cases, use the default handling */
             return inherited(whichObj, action, resolvedAllObjects);
         }
 
         /* use no announcement */
         return '';
     }
     
     /* 
      *   Allow locking or unlocking an object with a keyring.  This will
      *   automatically try each key on the keyring to see if it fits the
      *   lock. 
      */
     iobjFor(LockWith)
     {
         verify()
         {
             /* if we don't have any keys, we're not locking anything */
             if (contents.length() == 0)
                 illogical(&cannotLockWithMsg);
 
             /* 
              *   if we know the direct object, and we don't have any keys
              *   that are plausible for the direct object, we're an
              *   unlikely match 
              */
             if (gDobj != nil)
             {
                 local foundPlausibleKey;
                 
                 /* 
                  *   try each of my keys to see if it's plausible for the
                  *   direct object 
                  */
                 foundPlausibleKey = nil;
                 foreach (local cur in contents)
                 {
                     /* 
                      *   if this is a plausible key, note that we have at
                      *   least one plausible key 
                      */
                     if (gDobj.keyIsPlausible(cur))
                     {
                         /* note that we found a plausible key */
                         foundPlausibleKey = true;
 
                         /* no need to look any further - one is good enough */
                         break;
                     }
                 }
 
                 /* 
                  *   If we didn't find a plausible key, we're an unlikely
                  *   match.
                  *   
                  *   If we did find a plausible key, increase the
                  *   likelihood that this is the indirect object so that
                  *   it's greater than the likelihood for any random key
                  *   that's plausible for the lock (which has the default
                  *   likelihood of 100), but less than the likelihood of
                  *   the known good key (which is 150).  This will cause a
                  *   keyring to be taken as a default over any ordinary
                  *   key, but will cause the correct key to override the
                  *   keyring as the default if the correct key is known to
                  *   the player already.  
                  */
                 if (foundPlausibleKey)
                     logicalRank(140, 'keyring with plausible key');
                 else
                     logicalRank(50, 'no plausible key');
             }
         }
 
         action()
         {
             local key;
 
             /* 
              *   Try finding a working key.  If we find one, replace the
              *   command with 'lock <lock> with <key>, so that we have the
              *   full effect of the 'lock with' command using the key
              *   itself. 
              */
             if ((key = findWorkingKey(gDobj)) != nil)
                 replaceAction(LockWith, gDobj, key);
         }
     }
 
     iobjFor(UnlockWith)
     {
         /* verify the same as for LockWith */
         verify()
         {
             /* if we don't have any keys, we're not unlocking anything */
             if (contents.length() == 0)
                 illogical(&cannotUnlockWithMsg);
             else
                 verifyIobjLockWith();
         }
 
         action()
         {
             local key;
 
             /* 
              *   if we can find a working key, run an 'unlock with' action
              *   using the key 
              */
             if ((key = findWorkingKey(gDobj)) != nil)
                 replaceAction(UnlockWith, gDobj, key);
         }
     }
 ;
 
 /*
  *   Key - this is an object that can be used to unlock things, and which
  *   can be stored on a keyring.  The key that unlocks a lock is
  *   identified with a property on the lock, not on the key.
  */
 class Key: Thing
     /*
      *   A key on a keyring that is being held by an actor is considered
      *   to be held by the actor, since the key does not have to be
      *   removed from the keyring in order to be manipulated as though it
      *   were directly held.  
      */
     isHeldBy(actor)
     {
         /* 
          *   if I'm on a keyring, I'm being held if the keyring is being
          *   held; otherwise, use the default definition 
          */
         if (location != nil && location.ofKind(Keyring))
             return location.isHeldBy(actor);
         else
             return inherited(actor);
     }
 
     /*
      *   Try making the current command's actor hold me.  If we're on a
      *   keyring, we'll simply try to make the keyring itself held, rather
      *   than taking the key off the keyring; otherwise, we'll inherit the
      *   default behavior to make ourselves held.  
      */
     tryHolding()
     {
         if (location != nil && location.ofKind(Keyring))
             return location.tryHolding();
         else
             return inherited();
     }
 
     /* -------------------------------------------------------------------- */
     /*
      *   Action processing 
      */
 
     /* treat "detach key" as "take key" if it's on a keyring */
     dobjFor(Detach)
     {
         verify()
         {
             /* if I'm not on a keyring, there's nothing to detach from */
             if (location == nil || !location.ofKind(Keyring))
                 illogical(&keyNotDetachableMsg);
         }
         remap()
         {
             /* if I'm on a keyring, remap to "take self" */
             if (location != nil && location.ofKind(Keyring))
                 return [TakeAction, self];
             else
                 return inherited();
         }
     }
 
     /* "lock with" */
     iobjFor(LockWith)
     {
         verify()
         {
             /* 
              *   if we know the direct object is a LockableWithKey, we can
              *   perform some additional checks on the likelihood of this
              *   key being the intended key for the lock 
              */
             if (gDobj != nil
                 && gDobj.ofKind(LockableWithKey))
             {
                 /*
                  *   If the player should know that we're the key for the
                  *   lock, boost our likelihood so that we'll be picked
                  *   out automatically from an ambiguous set of keys.  
                  */
                 if (gDobj.isKeyKnown(self))
                     logicalRank(150, 'known key');
 
                 /* 
                  *   if this isn't a plausible key for the lockable, it's
                  *   unlikely that this is a match 
                  */
                 if (!gDobj.keyIsPlausible(self))
                     illogical(keyNotPlausibleMsg);
             }
         }
     }
 
     /* 
      *   the message to use when the key is obviously not plausible for a
      *   given lock 
      */
     keyNotPlausibleMsg = &keyDoesNotFitLockMsg
 
     /* "unlock with" */
     iobjFor(UnlockWith)
     {
         verify()
         {
             /* use the same key selection we use for "lock with" */
             verifyIobjLockWith();
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A Dispenser is a container for a special type of item, such as a book
  *   of matches or a box of candy.
  */
 class Dispenser: Container
     /* 
      *   Can we return one of our items to the dispenser once the item is
      *   dispensed?  Books of matches wouldn't generally allow this, since
      *   a match must be torn out to be removed, but simple box dispensers
      *   probably would.  By default, we won't allow returning an item
      *   once dispensed.  
      */
     canReturnItem = nil
 
     /* 
      *   Is the item one of the types of items we dispense?  Normally, we
      *   dispense identical items, so our default implementation simply
      *   determines if the item is an instance of our dispensable class.
      *   If the dispenser can hand out items of multiple, unrelated
      *   classes, this can be overridden to use a different means of
      *   identifying the dispensed items.  
      */
     isMyItem(obj) { return obj.ofKind(myItemClass); }
 
     /*
      *   The class of items we dispense.  This is used by the default
      *   implementation of isMyItem(), so subclasses that inherit that
      *   implementation should provide the appropriate base class here.  
      */
     myItemClass = Dispensable
 
     /* "put in" indirect object handler */
     iobjFor(PutIn)
     {
         verify()
         {
             /* if we know the direct object, consider it further */
             if (gDobj != nil)
             {
                 /* if we don't allow returning our items, don't allow it */
                 if (!canReturnItem && isMyItem(gDobj))
                     illogical(&cannotReturnToDispenserMsg);
 
                 /* if it's not my dispensed item, it can't go in here */
                 if (!isMyItem(gDobj))
                     illogical(&cannotPutInDispenserMsg);
             }
 
             /* inherit default handling */
             inherited();
         }
     }
 ;
 
 /*
  *   A Dispensable is an item that comes from a Dispenser.  This is in
  *   most respects an ordinary item; the only special thing about it is
  *   that if we're still in our dispenser, we're an unlikely match for any
  *   command except "take" and the like.  
  */
 class Dispensable: Thing
     /* 
      *   My dispenser.  This is usually my initial location, so by default
      *   we'll pre-initialize this to our location. 
      */
     myDispenser = nil
 
     /* pre-initialization */
     initializeThing()
     {
         /* inherit the default initialization */
         inherited();
 
         /* 
          *   We're usually in our dispenser initially, so assume that our
          *   dispenser is simply our initial location.  If myDispenser is
          *   overridden in a subclass, don't overwrite the inherited
          *   value.  
          */
         if (propType(&myDispenser) == TypeNil)
             myDispenser = location;
     }
 
     dobjFor(All)
     {
         verify()
         {
             /* 
              *   If we're in our dispenser, and the command isn't "take"
              *   or "take from", reduce our disambiguation likelihood -
              *   it's more likely that the actor is referring to another
              *   equivalent item that they've already removed from the
              *   dispenser.  
              */
             if (isIn(myDispenser)
                 && !gActionIs(Take) && !gActionIs(TakeFrom))
             {
                 /* we're in our dispenser - reduce the likelihood */
                 logicalRank(60, 'in dispenser');
             }
         }
     }
 ;
 
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A Matchbook is a special dispenser for matches. 
  */
 class Matchbook: Collective, Openable, Dispenser
     /* we cannot return a match to a matchbook */
     canReturnItem = nil
 
     /* 
      *   we dispense matches (subclasses can override this if they want to
      *   dispense a specialized match subclass) 
      */
     myItemClass = Matchstick
 
     /*
      *   Act as a collective for any items within me.  This will have no
      *   effect unless we also have a plural name that matches that of the
      *   contained items.
      *   
      *   It is usually desirable for a matchbook to act as a collective
      *   for the contained items, so that a command like "take matches"
      *   will be taken to apply to the matchbook rather than the
      *   individual matches.  
      */
     isCollectiveFor(obj) { return obj.isIn(self); }
 
     /*
      *   Append my directly-held contents to a vector when I'm directly
      *   held.  When the matchbook is open, append our matches, because we
      *   consider the matches to be effectively attached to the matchbook
      *   (rather than contained within it).  
      */
     appendHeldContents(vec)
     {
         /* if we're open, append our contents */
         if (isOpen)
             vec.appendUnique(contents);
     }
 ;
 
 /*
  *   A FireSource is an object that can set another object on fire.  This
  *   is a mix-in class that can be used with other classes.  
  */
 class FireSource: object
     /* 
      *   We can use a fire source to light another object, provided the
      *   fire source is itself burning.  We don't provide any action
      *   handling - we leave that to the direct object.  
      */
     iobjFor(BurnWith)
     {
         preCond = [objHeld, objBurning]
         verify()
         {
             /* don't allow using me to light myself */
             if (gDobj == self)
                 illogicalNow(&cannotBurnDobjWithMsg);
 
             /* 
              *   If we're already lit, make this an especially good choice
              *   for lighting other objects - this will ensure that we
              *   choose this over a match that isn't already lit, which is
              *   what you'd normally want to do to avoid wasting a match.
              *   
              *   Note that our ranking is specifically coordinated with
              *   that used by Matchstick.  We'll use a lit match over any
              *   normal FireSource (rank 160); we'll use a lit FireSource
              *   (rank 150) over an unlit match (rank 140).
              *   
              *   If we're not lit, make the action non-obvious so that
              *   we're not taken as a default to light another object on
              *   fire.  We *could* light something once we're lit, but that
              *   presumes there's a way to light me in the first place,
              *   which might require yet another object (a match, for
              *   example) - so ignore me as a default if we're not already
              *   lit, and go directly to some other object.  This should be
              *   overridden for self-lighting objects such as matches.  
              */
             if (isLit)
                 logicalRank(150, 'fire source');
             else
                 nonObvious;
         }
     }
 ;
 
 /*
  *   A Matchstick is a self-igniting match from a matchbook.  (We use this
  *   lengthy name rather than simply "Match" because the latter is too
  *   generic, and could be taken by a casual reader for an object
  *   representing a successful search result or the like.)  
  */
 class Matchstick: FireSource, LightSource
     /* matches have fairly feeble light */
     brightnessOn = 2
 
     /* not lit initially */
     isLit = nil
 
     /* amount of time we burn, in turns */
     burnLength = 2
 
     /* default long description describes burning status */
     desc()
     {
         if (isLit)
             gLibMessages.litMatchDesc(self);
         else
             gLibMessages.unlitMatchDesc(self);
     }
 
     /* get our state */
     getState = (isLit ? matchStateLit : matchStateUnlit)
 
     /* get a list of all states */
     allStates = [matchStateLit, matchStateUnlit]
     
     /* "burn" action */
     dobjFor(Burn)
     {
         preCond = [objHeld]
         verify()
         {
             /* can't light a match that's already burning */
             if (isLit)
                 illogicalAlready(&alreadyBurningMsg);
         }
         action()
         {
             local t;
             
             /* describe it */
             defaultReport(&okayBurnMatchMsg);
 
             /* make myself lit */
             makeLit(true);
 
             /* get our default burn length */
             t = burnLength;
 
             /* 
              *   if this is an implicit command, reduce the burn length by
              *   one turn - this ensures that the player can't
              *   artificially extend the match's useful life by doing
              *   something that implicitly lights the match 
              */
             if (gAction.isImplicit)
                 --t;
 
             /* start our burn-out timer going */
             new SenseFuse(self, &matchBurnedOut, t, self, sight);
         }
     }
 
     iobjFor(BurnWith)
     {
         verify()
         {
             /*
              *   Whether or not a match is burning, it's an especially
              *   good choice to light something else on fire.  Make it
              *   even more likely when it's burning already.
              *   
              *   Note that this is specifically coordinated with the base
              *   FireSource ranking.  We'll pick a lit match (160) over an
              *   ordinary lit FireSource (150), but we'll pick a lit
              *   FireSource (150) over an unlit match (140).  This will
              *   avoid consuming a match that's not already lit when
              *   another fire source is already available.  
              */
             logicalRank(isLit ? 160 : 140, 'fire source');
         }
     }
 
     /* "extinguish" */
     dobjFor(Extinguish)
     {
         verify()
         {
             /* can't extinguish a match that isn't burning */
             if (!isLit)
                 illogicalAlready(&matchNotLitMsg);
         }
         action()
         {
             /* describe the match going out */
             defaultReport(&okayExtinguishMatchMsg);
 
             /* no longer lit */
             makeLit(nil);
 
             /* remove the match from the game */
             moveInto(nil);
         }
     }
 
     /* fuse handler for burning out */
     matchBurnedOut()
     {
         /* 
          *   if I'm not still burning, I must have been extinguished
          *   explicitly already, so there's nothing to do 
          */
         if (!isLit)
             return;
         
         /* make sure we separate any output from other commands */
         "<.p>";
 
         /* report that we're done burning */
         gLibMessages.matchBurnedOut(self);
 
         /* 
          *   remove myself from the game (for simplicity, a match simply
          *   disappears when it's done burning) 
          */
         moveInto(nil);
     }
 
     /* matches usually come in bunches of equivalents */
     isEquivalent = true
 ;
 
 /*
  *   A light source that produces light using a fuel supply.  This kind of
  *   light source uses a daemon to consume fuel whenever it's lit.
  */
 class FueledLightSource: LightSource
     /* provide a bright light by default */
     brightnessOn = 3
 
     /* not lit initially */
     isLit = nil
 
     /*
      *   Our fuel source object.  If desired, this can be set to a
      *   separate object to model the fuel supply separately from the
      *   light source itself; for example, you could set this to point to
      *   a battery, or to a vial of oil.  By default, for simplicity, the
      *   fuel supply and light source are the same object.
      *   
      *   The fuel supply object must expose two methods: getFuelLevel()
      *   and consumeFuel().  
      */
     fuelSource = (self)
 
     /* 
      *   Get my fuel level, and consume fuel.  We use these methods only
      *   when we're our own fuelSource (which we are by default).  When
      *   we're not our own fuel source, the fuel source object must
      *   provide these methods instead of us.
      *   
      *   Our fuel level is the number of turns that we can continue to
      *   burn.  Each turn we're lit, we'll reduce the fuel level by one.
      *   We'll automatically extinguish ourself when the fuel level
      *   reaches zero.
      *   
      *   If the light source can burn forever, simply return nil as the
      *   fuel level.  
      */
     getFuelLevel() { return fuelLevel; }
     consumeFuel(amount) { fuelLevel -= amount; }
 
     /* our fuel level - we use this when we're our own fuel source */
     fuelLevel = 20
 
     /* light or extinguish */
     makeLit(lit)
     {
         /* if the current fuel level is zero, we can't be lit */
         if (lit && fuelSource.getFuelLevel() == 0)
             return;
 
         /* inherit the default handling */
         inherited(lit);
 
         /* if we're lit, activate our daemon; otherwise, stop our daemon */
         if (isLit)
         {
             /* start our burn daemon going */
             burnDaemonObj =
                 new SenseDaemon(self, &burnDaemon, 1, self, sight);
         }
         else
         {
             /* stop our daemon */
             eventManager.removeEvent(burnDaemonObj);
 
             /* forget out daemon */
             burnDaemonObj = nil;
         }
     }
 
     /* burn daemon - this is called on each turn while we're burning */
     burnDaemon()
     {
         local level = fuelSource.getFuelLevel();
         
         /* if we use fuel, consume one increment of fuel for this turn */
         if (level != nil)
         {
             /* 
              *   If our fuel level has reached zero, stop burning.  Note
              *   that the daemon is called on the first turn after we
              *   start burning, so we must go through a turn with the fuel
              *   level at zero before we stop burning.  
              */
             if (level == 0)
             {
                 /* make sure we separate any output from other commands */
                 "<.p>";
 
                 /* mention that the candle goes out */
                 sayBurnedOut();
 
                 /* 
                  *   Extinguish the candle.  Note that we do this *after*
                  *   we've already displayed the message about the candle
                  *   burning out, because that message is displayed in our
                  *   own sight context.  If we're the only light source
                  *   present, then we're invisible once we're not providing
                  *   light, so our message about burning out would be
                  *   suppressed if we displayed it after cutting off our
                  *   own light.  To make sure we can see the message, wait
                  *   until after the message to cut off our light.  
                  */
                 makeLit(nil);
             }
             else
             {
                 /* reduce our fuel level by one */
                 fuelSource.consumeFuel(1);
             }
         }
     }
 
     /* mention that we've just burned out */
     sayBurnedOut() { gLibMessages.objBurnedOut(self); }
 
     /* our daemon object, valid while we're burning */
     burnDaemonObj = nil
 ;
 
 /*
  *   A candle is an item that can be set on fire for a controlled burn.
  *   Although we call this a candle, this class can be used for other types
  *   of fuel burners, such as torches and oil lanterns.
  *   
  *   Ordinary candles are usually fire sources as well, in that you can
  *   light one candle with another once the first one is lit.  To get this
  *   effect, mix FireSource into the superclass list (but put it before
  *   Candle, since FireSource is specifically designed as a mix-in class).
  */
 class Candle: FueledLightSource
     /* 
      *   The message we display when we try to light the candle and we're
      *   out of fuel.  This message can be overridden by subclasses that
      *   don't fit the default message.  
      */
     outOfFuelMsg = &candleOutOfFuelMsg
 
     /* the message we display when we successfully light the candle */
     okayBurnMsg = &okayBurnCandleMsg
 
     /* show a message when the candle runs out fuel while burning */
     sayBurnedOut()
     {
         /* by default, show our standard library message */
         gLibMessages.candleBurnedOut(self);
     }
 
     /* 
      *   Determine if I can be lit with the specific indirect object.  By
      *   default, we'll allow any object to light us if the object passes
      *   the normal checks applied by its own iobjFor(BurnWith) handlers.
      *   This can be overridden if we can only be lit with specific
      *   sources of fire; for example, a furnace with a deeply-recessed
      *   burner could refuse to be lit by anything but particular long
      *   matches, or a particular type of fuel could refuse to be lit
      *   except by certain especially hot flames.  
      */
     canLightWith(obj) { return true; }
 
     /* 
      *   Default long description describes burning status.  In most
      *   cases, this should be overridden to provide more details, such as
      *   information on our fuel level. 
      */
     desc()
     {
         if (isLit)
             gLibMessages.litCandleDesc(self);
         else
             inherited();
     }
 
     /* "burn with" action */
     dobjFor(BurnWith)
     {
         preCond = [touchObj]
         verify()
         {
             /* can't light it if it's already lit */
             if (isLit)
                 illogicalAlready(&alreadyBurningMsg);
         }
         check()
         {
             /* 
              *   make sure the object being used to light us is a valid
              *   source of fire for us 
              */
             if (!canLightWith(obj))
             {
                 reportFailure(&cannotBurnDobjWithMsg);
                 exit;
             }
 
             /* if the fuel level is zero, we can't be lit */
             if (fuelSource.getFuelLevel() == 0)
             {
                 reportFailure(outOfFuelMsg);
                 exit;
             }
         }
         action()
         {
             /* make myself lit */
             makeLit(true);
 
             /* describe it */
             defaultReport(okayBurnMsg);
         }
     }
 
     /* "extinguish" */
     dobjFor(Extinguish)
     {
         verify()
         {
             /* can't extinguish a match that isn't burning */
             if (!isLit)
                 illogicalAlready(&candleNotLitMsg);
         }
         action()
         {
             /* describe the match going out */
             defaultReport(&okayExtinguishCandleMsg);
 
             /* no longer lit */
             makeLit(nil);
         }
     }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   "Tour Guide" is a mix-in class for Actors.  This class can be
  *   multiply inherited by objects along with Actor or a subclass of
  *   Actor.  This mix-in makes the Follow action, when applied to the tour
  *   guide, initiate travel according to where the tour guide wants to go
  *   next.  So, if the tour guide is here and is waving us through the
  *   door, FOLLOW GUIDE will initiate travel through the door.
  *   
  *   This class should appear in the superclass list ahead of Actor or the
  *   Actor subclass.  
  */
 class TourGuide: object
     dobjFor(Follow)
     {
         verify()
         {
             /*
              *   If the actor can see us, and we're in a "guided tour"
              *   state, we can definitely perform the travel.  Otherwise,
              *   use the standard "follow" behavior. 
              */
             if (gActor.canSee(self) && getTourDest() != nil)
             {
                 /* 
                  *   we're waiting to show the actor to the next stop on
                  *   the tour, so we can definitely proceed with this
                  *   action 
                  */
             }
             else
             {
                 /* we're not in a tour state, so use the standard handling */
                 inherited();
             }
         }
 
         action()
         {
             local dest;
             
             /* 
              *   if we're in a guided tour state, initiate travel to our
              *   escort destination; otherwise, use the standard handling 
              */
             if (gActor.canSee(self) && (dest = getTourDest()) != nil)
             {
                 /* initiate travel to our destination */
                 replaceAction(TravelVia, dest);
                 return;
             }
             else
             {
                 /* no tour state; use the standard handling */
                 inherited();
             }
         }
     }
 
     /*
      *   Get the travel connector that takes us to our next guided tour
      *   destination.  By default, this returns the escortDest from our
      *   current actor state if our state is a guided tour state, or nil
      *   if our state is any other kind of state.  Subclasses must
      *   override this if they use other kinds of states to represent
      *   guided tours, since we'll only detect that we're in a guided tour
      *   state if our current actor state object is of class
      *   GuidedTourState (or any subclass).  
      */
     getTourDest()
     {
         return (curState.ofKind(GuidedTourState)
                 ? curState.escortDest
                 : nil);
     }
 ;
 
 /*
  *   Guided Tour state.  This provides a simple way of defining a "guided
  *   tour," which is a series of locations to which we try to guide the
  *   player character.  We don't force the player character to travel as
  *   specified; we merely try to lead the player.  The actual travel is up
  *   to the player.
  *   
  *   Here's how this works.  For each location on the guided tour, create
  *   one of these state objects.  Set escortDest to the travel connector
  *   to which we're attempting to guide the player character from the
  *   current location.  Set stateAfterEscort to the state object for the
  *   next location on the tour.  Set stateDesc to something indicating
  *   that we're trying to show the player to the next stop - something
  *   along the lines of "Bob waits for you by the door."  Set
  *   arrivingWithDesc to a message indicating that we just showed up in
  *   the current location and are ready to show the player to the next -
  *   "Bob goes to the door and waits for you to follow him."  
  */
 class GuidedTourState: AccompanyingState
     /* the travel connector we're trying to show the player into */
     escortDest = nil
 
     /* 
      *   The next state for our actor to assume after the travel.  This
      *   should be overridden and set to the state object for the next
      *   stop on the tour. 
      */
     stateAfterEscort = nil
 
     /* the actor we're escorting - this is usually the player character */
     escortActor = (gPlayerChar)
 
     /* 
      *   The class we use for our actor state during the escort travel.
      *   By default, we use the basic guided-tour accompanying travel
      *   state class, but games will probably want to use a customized
      *   subclass of this basic class in most cases.  The main reason to
      *   use a custom subclass is to provide customized messages to
      *   describe the departure of the escorting actor.  
      */
     escortStateClass = GuidedInTravelState
     
     /* 
      *   we should accompany the travel if the actor we're guiding will be
      *   traveling, and they're traveling to the next stop on our tour 
      */
     accompanyTravel(traveler, conn)
     {
         return (traveler.isActorTraveling(escortActor) && conn == escortDest);
     }
 
     /* 
      *   get our accompanying state object - we'll create an instance of
      *   the class specified in our escortStateClass property 
      */
     getAccompanyingTravelState(traveler, conn)
     {
         return escortStateClass.createInstance(
             location, gActor, stateAfterEscort);
     }
 ;
 
 /*
  *   A subclass of the basic accompanying travel state specifically
  *   designed for guided tours.  This is almost the same as the basic
  *   accompanying travel state, but provides customized messages to
  *   describe the departure of our associated actor, which is the actor
  *   serving as the tour guide.  
  */
 class GuidedInTravelState: AccompanyingInTravelState
     sayDeparting(conn)
         { gLibMessages.sayDepartingWithGuide(location, leadActor); }
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   An Attachable is an object that can be attached to another, using an
  *   ATTACH X TO Y command.  This is a mix-in class that is meant to be
  *   combined with a Thing-derived class to create an attachable object.
  *   
  *   Attachment is symmetrical: we can only attach to other Attachable
  *   objects.  As a result, the verb handling for ATTACH can be performed
  *   symmetrically - ATTACH X TO Y is handled the same way as ATTACH Y TO
  *   X.  Sometimes reversing the roles makes the command nonsensical, but
  *   when the reversal makes sense, it seems unlikely that it'll ever
  *   change the meaning of the command.  This makes it program the verb
  *   handling, because it means that we can designate one of X or Y as the
  *   handler for the verb, and just write the code once there.  Refer to
  *   the handleAttach() method to see how this works.
  *   
  *   There's an important detail that we leave to instances, because
  *   there's no good general rule we can implement.  Specifically, there's
  *   the matter of imposing appropriate constraints on the relative
  *   locations of objects once they're attached to one another.  There are
  *   numerous anomalies that become possible once two objects are attached.
  *   Consider the example of a battery connected to a jumper cable that's
  *   in turn connected to a lamp:
  *   
  *   - if we put the battery in a box but leave the lamp outside the box,
  *   we shouldn't be able to close the lid of the box all the way without
  *   breaking the cables
  *   
  *   - if we're carrying the battery but not the lamp, traveling to a new
  *   room should drag the lamp along
  *   
  *   - if we drop the battery down a well, the lamp should be dragged down
  *   with it
  *   
  *   Our world model isn't sophisticated enough to properly model an
  *   attachment relationship, so it can't deal with these contingencies by
  *   proper physical simulation.  Which is why we have to leave these for
  *   the game to handle.
  *   
  *   There are two main strategies you can apply to handle these problems.
  *   
  *   First, you can impose limits that prevent these sorts of situations
  *   from coming up in the first place, either by carefully designing the
  *   scenario so they simply don't come up, or by imposing more or less
  *   artificial constraints.  For example, you could solve all of the
  *   problems above by eliminating the jumper cable and attaching the lamp
  *   directly to the battery, or by making the jumper cable very short.
  *   Anything attached to the battery would effectively become located "in"
  *   the battery, so it would move everywhere along with the battery
  *   automatically.  Detaching the lamp would move the lamp back outside
  *   the battery, and conversely, moving the lamp out of the battery would
  *   detach the objects.
  *   
  *   Second, you can detect the anomalous cases and handle them explicitly
  *   with special-purpose code.  You could use beforeAction and afterAction
  *   methods on one of the attached objects, for example, to detect the
  *   various problematic actions, either blocking them or implementing
  *   appropriate consequences.
  *   
  *   Given the number of difficult anomalies possible with rope-like
  *   objects, the second approach is challenging on its own.  However, it
  *   often helps to combine it with the first approach, limiting the
  *   scenario.  In other words, you'd limit the scenario to some extent,
  *   but not totally: rather than completely excising the difficult
  *   behavior, you'd narrow it down to a manageable subset of the full
  *   range of real-world possibilities; then, you'd deal with the remaining
  *   anomalies on a case-by-case basis.  For example, you could make the
  *   battery too heavy to carry, which would guarantee that it would never
  *   be put in a box, thrown down a well, or carried out of the room.  That
  *   would only leave a few issues: walking away while carrying the plugged
  *   in lamp, which could be handled with an afterAction that severs the
  *   attachment; putting the lamp in a box and closing the box, which could
  *   be handled with a beforeAction by blocking Close actions whenever the
  *   lamp is inside the object being closed.  
  */
 class Attachable: object
     /* 
      *   The list of objects I'm currently attached to.  Note that each of
      *   the objects in this list must usually be an Attachable, and we
      *   must be included in the attachedObjects list in each of these
      *   objects.  
      */
     attachedObjects = []
 
     /*
      *   Perform programmatic attachment, without any notifications.  This
      *   simply updates my attachedObjects list and the other object's list
      *   to indicate that we're attached to the other object (and vice
      *   versa). 
      */
     attachTo(obj)
     {
         attachedObjects += obj;
         obj.attachedObjects += self;
     }
 
     /* perform programmatic detachment, without any notifications */
     detachFrom(obj)
     {
         attachedObjects -= obj;
         obj.attachedObjects -= self;
     }
 
     /* get the subset of my attachments that are non-permanent */
     getNonPermanentAttachments()
     {
         /* return the subset of objects not permanently attached */
         return attachedObjects.subset({x: !isPermanentlyAttachedTo(x)});
     }
 
     /* am I attached to the given object? */
     isAttachedTo(obj)
     {
         /* we are attached to the other object if it's in our list */
         return (attachedObjects.indexOf(obj) != nil);
     }
 
     /*
      *   Am I the "major" item in my attachment relationship to the given
      *   object?  This affects how our relationship is described in our
      *   status message: in an asymmetrical relationship, where one object
      *   is the "major" item, we will always describe the minor item as
      *   being attached to the major item rather than vice versa.  This
      *   allows you to ensure that the message is always "the sign is
      *   attached to the wall", and never "the wall is attached to the
      *   sign": the wall is the major item in this relationship, so it's
      *   always the sign that's attached to it.
      *   
      *   By default, we always return nil here, which means that
      *   attachment relationships are symmetrical by default.  In a
      *   symmetrical relationship, we'll describe the other things as
      *   attached to 'self' when describing self.  
      */
     isMajorItemFor(obj) { return nil; }
 
     /*
      *   Am I *listed* as attached to the given object?  If this is true,
      *   then our examineStatus() will list 'obj' among the things I'm
      *   attached to: "Self is attached to obj."  If this is nil, I'm not
      *   listed as attached.
      *   
      *   By default, we're listed if (1) we're not permanently attached to
      *   'obj', AND (2) we're not the "major" item in the attachment
      *   relationship.  The reason we're not listed if we're permanently
      *   attached is that the attachment information is presumably better
      *   handled via the fixed description of the object rather than in
      *   the extra status message; this is analogous to the way immovable
      *   items (such as Fixtures) aren't normally listed in the
      *   description of a room.  The reason we're not listed if we're the
      *   "major" item in the relationship is that the "major" status
      *   reverses the relationship: when we're the major item, the other
      *   item is described as attached to *us*, rather than vice versa.  
      */
     isListedAsAttachedTo(obj)
     {
         /* 
          *   only list the item if it's not permanently attached, and
          *   we're not the "major" item for the object 
          */
         return (!isPermanentlyAttachedTo(obj) && !isMajorItemFor(obj));
     }
 
     /*
      *   Is 'obj' listed as attached to me when I'm described?  If this is
      *   true, then our examineStatus() will list 'obj' among the things
      *   attached to me: "Attached to self is obj."  If this is nil, then
      *   'obj' is not listed among the things attached to me when I'm
      *   described.
      *   
      *   This routine is simply the "major" list counterpart of
      *   isListedAsAttachedTo().
      *   
      *   By default, we list 'obj' among my attachments if (1) I'm the
      *   "major" item for 'obj', AND (2) 'obj' is listed as attached to
      *   me, as indicated by obj.isListedAsAttachedTo(self).  We only list
      *   our minor attachments here, because we list all of our other
      *   listable attachments separately, as the things I'm attached to.
      *   We also only list items that are themselves listable as
      *   attachments, for obvious reasons.  
      */
     isListedAsMajorFor(obj)
     {
         /* 
          *   only list the item if we're the "major" item for the object,
          *   and the object is itself listable as an attachment 
          */
         return (isMajorItemFor(obj) && obj.isListedAsAttachedTo(self));
     }
     
     /*
      *   Can I attach to the given object?  This returns true if the other
      *   object is allowable as an attachment, nil if not.
      *   
      *   By default, we look to see if the other side is an Attachable, and
      *   if so, if it overrides canAttachTo(); if so, we'll call its
      *   canAttachTo to ask whether it thinks it can attach to us.  If the
      *   other side doesn't override this, we'll simply return nil.  This
      *   arrangement is convenient because it means that only one side of
      *   an attachable pair needs to implement this; the other side will
      *   automatically figure it out by calling the first side and relying
      *   on the symmetry of the relationship.  
      */
     canAttachTo(obj)
     {
         /* 
          *   if the other side's an Attachable, and it overrides this
          *   method, call the override; if not, it's by default not one of
          *   our valid attachments 
          */
         if (overrides(obj, Attachable, &canAttachTo))
         {
             /* 
              *   the other side is an Attachable that defines a specific
              *   attachment rule, so ask the other side if it thinks we're
              *   one of its attachments; by the symmetry of the
              *   relationship, if we're one of its attachments, then it's
              *   one of ours 
              */
             return obj.canAttachTo(self);
         }
         else
         {
             /* 
              *   the other side doesn't want to tell us, so we're on our
              *   own; we don't recognize any attachments on our own, so
              *   it's not a valid attachment 
              */
             return nil;
         }
     }
 
     /*
      *   Explain why we can't attach to the given object.  This should
      *   simply display an appropriate mesage.  We use reportFailure to
      *   flag it as a failure report, but that's not actually required,
      *   since we call this from our 'check' routine, which will mark the
      *   action as having failed even if we don't here.  
      */
     explainCannotAttachTo(obj) { reportFailure(&wrongAttachmentMsg); }
 
     /*
      *   Is it possible for me to detach from the given object?  This asks
      *   whether a given attachment relationship can be dissolved with
      *   DETACH FROM. 
      *   
      *   By default, we'll use similar logic to canAttachTo: if the other
      *   object overrides canDetachFrom(), we'll let it make the
      *   determination.  Otherwise, we'll return nil if one or the other
      *   side is a PermanentAttachment, true if not.  This lets you prevent
      *   detachment by overriding canDetachFrom() on just one side of the
      *   relationship.  
      */
     canDetachFrom(obj)
     {
         /* if the other object overrides canDetachFrom, defer to it */
         if (overrides(obj, Attachable, &canDetachFrom))
         {
             /* let the other side make the judgment */
             return obj.canDetachFrom(self);
         }
         else
         {
             /* 
              *   the other side doesn't override it, so assume we can
              *   detach unless one or the other side is a
              *   PermanentAttachment 
              */
             return !isPermanentlyAttachedTo(obj);
         }
     }
 
     /*
      *   Am I permanently attached to the other object?  This returns true
      *   if I'm a PermanentAttachment or the other object is.  
      */
     isPermanentlyAttachedTo(obj)
     {
         /* 
          *   if either one of us is a PermanentAttachment, we're
          *   permanently attached to each other 
          */
         return ofKind(PermanentAttachment) || obj.ofKind(PermanentAttachment);
     }
     
 
     /* 
      *   A message explaining why we can't detach from the given object.
      *   Note that 'obj' can be nil, because we could be attempting a
      *   DETACH command with no indirect object.  
      */
     cannotDetachMsg(obj)
     {
         /* 
          *   if we have an object, it must be the wrong one; otherwise, we
          *   simply can't detach generically, since the object to detach
          *   from wasn't specified, and there's nothing obvious we can
          *   detach from 
          */
         return obj != nil ? &wrongDetachmentMsg : &cannotDetachMsg;
     }
 
     /*
      *   Process attachment to a new object.  This routine is called on
      *   BOTH the direct and indirect object during the attachment process
      *   - that is, it's called on the direct object with the indirect
      *   object as the argument, and then it's called on the indirect
      *   object with the direct object as the argument.
      *   
      *   This symmetrical handling makes it easy to handle the frequent
      *   cases where the player might say ATTACH X TO Y or ATTACH Y TO X
      *   and mean the same thing either way.  Because this method is called
      *   for both X and Y in either phrasing, you can simply choose to
      *   write the handler code in either X or Y - you only have to write
      *   it once, because the handler will be called on each of the
      *   objects, regardless of the phrasing.  So, if you choose to
      *   designate X as the official ATTACH handler, write a handleAttach()
      *   method on X, and leave the one on Y doing nothing: during
      *   execution, the X method will do its work, and the Y method will do
      *   nothing, so regardless of phrasing order, the net result will be
      *   the same.
      *   
      *   By default we do nothing.  Each instance should override this to
      *   display any extra message and take any extra action needed to
      *   process the attachment status change.  Note that the override
      *   doesn't need to worry about managing the attachedObjects list, as
      *   the main action handler does that automatically.
      *   
      *   Note that handleAttach() is always called after both objects have
      *   updated their attachedObjects lists.  This means that you can turn
      *   right around and detach the objects here, if you don't want to
      *   leave them attached.  
      */
     handleAttach(other)
     {
         /* do nothing by default */
     }
 
     /*
      *   Receive notification that this object or one of its attachments
      *   is being moved to a new container.  When an attached object is
      *   moved, we'll call this on the object being moved AND on every
      *   object attached to it.  'movedObj' is the object being moved, and
      *   'newCont' is the new container it's being moved into.
      *   
      *   By default we do nothing.  Instances can override this as needed.
      *   For example, if you wish to enforce a rule that this object and
      *   all of its attached objects share a common direct container, you
      *   could either block the move (by displaying an error and using
      *   'exit') or run a nested DetachFrom action to sever the attachment
      *   with the object being moved.  
      */
     moveWhileAttached(movedObj, newCont)
     {
         /* do nothing by default */
     }
 
     /*
      *   Receive notification that this object or one of its attachments is
      *   being moved in the course of an actor traveling to a new location.
      *   Whenever anyone travels while carrying an attachable object
      *   (directly or indirectly), we'll call this on the object being
      *   moved AND on every object attached to it.  'movedObj' is the
      *   object being carried by the traveling actor, 'traveler' is the
      *   Traveler performing the travel, and 'connector' is the
      *   TravelConnector that the traveler is traversing.
      *   
      *   By default, we do nothing.  Instances can override this as needed.
      */
     travelWhileAttached(movedObj, traveler, connector)
     {
         /* do nothing by default */
     }
 
     /*
      *   Handle detachment.  This works like handleAttach(), in that this
      *   routine is invoked symmetrically for both sides of a DETACH X FROM
      *   Y commands.
      *   
      *   As with handleAttach(), we do nothing by default, so instances
      *   should override as needed.  Note that the override doesn't need to
      *   worry about managing the attachedObjects list, as the main action
      *   handler does that automatically.  As with handleAttach(), this is
      *   called after the attachedObjects lists for both objects are
      *   updated.  
      */
     handleDetach(other)
     {
         /* do nothing by default */
     }
 
     /* the Lister we use to show our list of attached objects */
     attachmentLister = perInstance(new SimpleAttachmentLister(self))
 
     /* 
      *   the Lister we use to list the items attached to us (i.e., the
      *   items for which we're the "major" item in the attachment
      *   relationship) 
      */
     majorAttachmentLister = perInstance(new MajorAttachmentLister(self))
 
     /* add a list of our attachments to the desription */
     examineStatus()
     {
         local tab;
         
         /* inherit the normal status description */
         inherited();
 
         /* get the actor's visual sense table */
         tab = gActor.visibleInfoTable();
 
         /* add our list of attachments */
         attachmentLister.showList(gActor, self, attachedObjects,
                                   0, 0, tab, nil);
 
         /* add our list of major attachments */
         majorAttachmentLister.showList(gActor, self, attachedObjects,
                                        0, 0, tab, nil);
     }
 
     /* 
      *   Move into a new container.  If I'm attached to anything, we'll
      *   notify ourself and our attachments. 
      */
     mainMoveInto(newCont)
     {
         /* if I'm attached to anything, notify everyone */
         if (attachedObjects.length() != 0)
         {
             /* notify myself */
             moveWhileAttached(self, newCont);
 
             /* notify my attachments */
             attachedObjects.forEach({x: x.moveWhileAttached(self, newCont)});
         }
 
         /* inherit the base handling */
         inherited(newCont);
     }
 
     /*
      *   Receive notification of travel.  If I'm involved in the travel,
      *   and I'm attached to anything, we'll notify ourself and our
      *   attachments. 
      */
     beforeTravel(traveler, connector)
     {
         /* 
          *   If we're traveling with the traveler, and we're attached to
          *   anything, notify everything that's attached.
          */
         if (attachedObjects.length() != 0
             && traveler.isTravelerCarrying(self))
         {
             /* notify myself */
             travelWhileAttached(self, traveler, connector);
 
             /* notify each of my attachments */
             attachedObjects.forEach(
                 {x: x.travelWhileAttached(self, traveler, connector)});
         }
     }
 
     /* 
      *   during initialization, make sure the attachedObjects list is
      *   symmetrical for both sides of the attachment relationship 
      */
     initializeThing()
     {
         /* do the normal work */
         inherited();
 
         /* 
          *   check to make sure that each of our attached objects points
          *   back at us 
          */
         foreach (local cur in attachedObjects)
         {
             /* 
              *   if we're not in this one's attachedObjects list, add
              *   ourselves to the list, so that everyone's consistent
              */
             if (cur.attachedObjects.indexOf(self) == nil)
                 cur.attachedObjects += self;
         }
     }
 
     /* handle attachment on the direct object side */
     dobjFor(AttachTo)
     {
         /* require that the actor can touch the direct object */
         preCond = [touchObj]
         
         verify()
         {
             /* 
              *   it makes sense to attach to anything but myself, or things
              *   we're already attached to 
              */
             if (gIobj != nil)
             {
                 if (isAttachedTo(gIobj))
                     illogicalAlready(&alreadyAttachedMsg);
                 else if (gIobj == self)
                     illogicalSelf(&cannotAttachToSelfMsg);
             }
         }
         
         check()
         {
             /* only allow it if we can attach to the other object */
             if (!canAttachTo(gIobj))
             {
                 explainCannotAttachTo(gIobj);
                 exit;
             }
         }
         
         action()
         {
             /* add the other object to our list of attached objects */
             attachedObjects += gIobj;
 
             /* add our default acknowledgment */
             defaultReport(&okayAttachToMsg);
 
             /* fire the handleAttach event if we're ready */
             maybeHandleAttach(gIobj);
         }
     }
 
     /* handle attachment on the indirect object side */
     iobjFor(AttachTo)
     {
         /*
          *   Require that the direct object can touch the indirect object.
          *   This ensures that the two objects to be attached can touch
          *   one another.  Note that we don't also require that the actor
          *   be able to touch the indirect object directly, since it's
          *   good enough that (1) the actor can touch the direct object
          *   (which we enforce with the dobj precondition), and (2) the
          *   direct object can touch the indirect object.  This allows for
          *   odd things like plugging something into a recessed outlet,
          *   where the recessed bit can't be reached directly but can be
          *   reached using the plug.  
          */
         preCond = [dobjTouchObj]
         
         verify()
         {
             /* 
              *   it makes sense to attach to anything but myself, or things
              *   we're already attached to 
              */
             if (gDobj != nil)
             {
                 if (isAttachedTo(gDobj))
                     illogicalAlready(&alreadyAttachedMsg);
                 else if (gDobj == self)
                     illogicalSelf(&cannotAttachToSelfMsg);
             }
         }
         
         check()
         {
             /* only allow it if we can attach to the other object */
             if (!canAttachTo(gDobj))
             {
                 explainCannotAttachTo(gDobj);
                 exit;
             }
         }
         
         action()
         {
             /* add the other object to our list of attached objects */
             attachedObjects += gDobj;
 
             /* fire the handleAttach event if we're ready */
             maybeHandleAttach(gIobj);
         }
     }
 
     /* 
      *   Fire the handleAttach event - we'll notify both sides as soon as
      *   both sides are hooked up with each other.  This ensures that both
      *   lists are updated before we notify either side, so the ordering
      *   doesn't depend on whether we handle the dobj or iobj first. 
      */
     maybeHandleAttach(other)
     {
         /* if both lists are hooked up, send the notifications */
         if (attachedObjects.indexOf(other) != nil
             && other.attachedObjects.indexOf(self) != nil)
         {
             /* notify our side */
             handleAttach(other);
 
             /* notify the other side */
             other.handleAttach(self);
         }
     }
 
     /* handle simple, unspecified detachment (DETACH OBJECT) */
     dobjFor(Detach)
     {
         verify()
         {
             /* if I'm not attached to anything, this is illogical */
             if (attachedObjects.length() == 0)
                 illogicalAlready(cannotDetachMsg(nil));
         }
         action()
         {
             local lst;
 
             /* get the non-permanent attachment subset */
             lst = getNonPermanentAttachments();
 
             /* check what that leaves us */
             if (lst.length() == 0)
             {
                 /* 
                  *   we're not attached to anything that we can detach
                  *   from, so simply report that we can't detach
                  *   generically 
                  */
                 reportFailure(cannotDetachMsg(nil));
             }
             else if (lst.length() == 1)
             {
                 /* 
                  *   we have exactly one attached object from which we can
                  *   detach, so they must want to detach from that -
                  *   process this as DETACH FROM my one attached object 
                  */
                 replaceAction(DetachFrom, self, lst[1]);
             }
             else
             {
                 /* 
                  *   we have more than one detachable attachment, so ask
                  *   which one they mean 
                  */
                 askForIobj(DetachFrom);
             }
         }
     }
 
     /* handle detaching me from a specific other object */
     dobjFor(DetachFrom)
     {
         verify()
         {
             /* it only makes sense to try detaching us from our attachments */
             if (gIobj != nil && !isAttachedTo(gIobj))
                 illogicalAlready(&notAttachedToMsg);
         }
         check()
         {
             /* make sure I'm allowed to detach from the given object */
             if (!canDetachFrom(gIobj))
             {
                 reportFailure(cannotDetachMsg(gIobj));
                 exit;
             }
         }
         action()
         {
             /* remove the other object from our list of attached objects */
             attachedObjects -= gIobj;
 
             /* add our default acknowledgment */
             defaultReport(&okayDetachFromMsg);
 
             /* fire the handleDetach event if appropriate */
             maybeHandleDetach(gIobj);
         }
     }
 
     /* handle detachment on the indirect object side */
     iobjFor(DetachFrom)
     {
         verify()
         {
             /* it only makes sense to try detaching my attachments */
             if (gDobj == nil)
             {
                 /* 
                  *   we don't know the dobj yet, but we can check the
                  *   tentative list for the possible set 
                  */
                 if (gTentativeDobj
                     .indexWhich({x: isAttachedTo(x.obj_)}) == nil)
                     illogicalAlready(&notAttachedToMsg);
             }
             else if (gDobj != nil && !isAttachedTo(gDobj))
                 illogicalAlready(&notAttachedToMsg);
         }
         check()
         {
             /* make sure I'm allowed to detach from the given object */
             if (!canDetachFrom(gDobj))
             {
                 reportFailure(cannotDetachMsg(gDobj));
                 exit;
             }
         }
         action()
         {
             /* remove the other object from our list of attached objects */
             attachedObjects -= gDobj;
 
             /* fire the handleDetach event if appropriate */
             maybeHandleDetach(gDobj);
         }
     }
 
     /* 
      *   Fire the handleDetach event - we'll notify both sides as soon as
      *   both sides are un-hooked up.  This ensures that both lists are
      *   updated before we notify either side, so the ordering doesn't
      *   depend on whether we handle the dobj or iobj first.  
      */
     maybeHandleDetach(other)
     {
         /* if both lists are un-hooked up, send the notifications */
         if (attachedObjects.indexOf(other) == nil
             && other.attachedObjects.indexOf(self) == nil)
         {
             /* notify our side */
             handleDetach(other);
 
             /* notify the other side */
             other.handleDetach(self);
         }
     }
 
     /* 
      *   TAKE X FROM Y is the same as DETACH X FROM Y for things we're
      *   attached to, but use the inherited handling otherwise 
      */
     dobjFor(TakeFrom)
     {
         verify()
         {
             /* 
              *   use the inherited handling only if we're not attached -
              *   if we're attached, consider it logical, overriding any
              *   containment relationship check we might otherwise make 
              */
             if (gIobj == nil || !isAttachedTo(gIobj))
                 inherited();
         }
         check()
         {
             /* inherit the default check only if we're not attached */
             if (!isAttachedTo(gIobj))
                 inherited();
         }
         action()
         {
             /* 
              *   if we're attached, change this into a DETACH FROM action;
              *   otherwise, use the inherited TAKE FROM handling 
              */
             if (isAttachedTo(gIobj))
                 replaceAction(DetachFrom, self, gIobj);
             else
                 inherited();
         }
     }
     iobjFor(TakeFrom)
     {
         verify()
         {
             /* use the inherited handling only if we're not attached */
             if (gDobj == nil || !isAttachedTo(gDobj))
                 inherited();
         }
         check()
         {
             /* inherit the default check only if we're not attached */
             if (!isAttachedTo(gDobj))
                 inherited();
         }
         action()
         {
             /* inherit the default action only if we're not attached */
             if (!isAttachedTo(gDobj))
                 inherited();
         }
     }
 ;
 
 /*
  *   An Attachable-specific precondition: the Attachable isn't already
  *   attached to something else.  This can be added to the preCond list for
  *   an Attachable (for iobjFor(AttachTo) and dobjFor(AttachTo)) to ensure
  *   that any existing attachment is removed before a new attachment is
  *   formed.  This is useful when the Attachable can connect to only one
  *   thing at a time.  
  */
 objNotAttached: PreCondition
     checkPreCondition(obj, allowImplicit)
     {
         /* 
          *   if we don't already have any non-permanent attachments, we're
          *   fine (as we don't require removing permanent attachments) 
          */
         if (obj.attachedObjects.indexWhich(
             {x: !obj.isPermanentlyAttachedTo(x)}) == nil)
             return nil;
 
         /* 
          *   Try an implicit Detach command.  It should be safe to use the
          *   form that doesn't specify what we're detaching from, since the
          *   whole point of this condition is that the object can have only
          *   one non-permanent attachment, hence the vague Detach handler
          *   should be able to figure out what we mean.  
          */
         if (allowImplicit && tryImplicitAction(Detach, obj))
         {
             /* if we're still attached to anything, we failed, so abort */
             if (obj.attachedObjects.indexWhich(
                 {x: !obj.isPermanentlyAttachedTo(x)}) != nil)
                 exit;
 
             /* tell the caller we executed an implied action */
             return true;
         }
 
         /* we must detach first */
         reportFailure(&mustDetachMsg, obj);
         exit;
     }
 ;
 
 /*
  *   A "nearby" attachable is a subclass of Attachable that adds a
  *   requirement that the attached objects be in a given location.  By
  *   default, we simply require that they have a common immediate
  *   container, but this can be overridden so that each object's location
  *   is negotiated separately.  This is a simple and effective pattern that
  *   avoids many of the potential anomalies with attachment (see the
  *   Attachable comments for examples).
  *   
  *   In AttachTo actions, we enforce the nearby requirement with a
  *   precondition requiring the direct object to be in the same immediate
  *   container as the indirect object, and vice versa.  In
  *   moveWhileAttached(), we enforce the rule by detaching the objects if
  *   one is being moved away from the other's immediate container.  
  */
 class NearbyAttachable: Attachable
     dobjFor(AttachTo)
     {
         /* require that the objects be in the negotiated locations */
         preCond = (inherited() + nearbyAttachableCond)
     }
     iobjFor(AttachTo)
     {
         /* require that the objects be in the negotiated locations */
         preCond = (inherited() + nearbyAttachableCond)
     }
 
     /*
      *   Get the target locations for attaching to the given other object.
      *   The "target locations" are the locations where the objects are
      *   required to be in order to carry out the ATTACH command to attach
      *   this object to the other object (or vice versa). 
      *   
      *   This method returns a list with three elements.  The first
      *   element is the target location for 'self', and the second is the
      *   target location for 'other', the object we're attaching to.  The
      *   third element is an integer giving the priority; a higher number
      *   means higher priority.
      *   
      *   The priority is an arbitrary value that we use to determine which
      *   of the two objects involved in the attach gets to decide on the
      *   target locations.  We call this method on both of the two objects
      *   being attached to one another, then we use the target locations
      *   returned by the object that claims the higher priority.  If the
      *   two priorities are equal, we pick one arbitrarily.
      *   
      *   The default implementation chooses my own immediate container as
      *   the target location for both objects.  However, if the other
      *   object is non-portable, we'll choose its immediate location
      *   instead, since we obviously can't move it to our container.  
      */
     getNearbyAttachmentLocs(other)
     {
         /* 
          *   If the other object is portable, use our immediate container
          *   as the proposed location for both objects; otherwise, use the
          *   other object's immediate container.  In any case, use a low
          *   priority, since we're just the default base class
          *   implementation; any override will generally have higher
          *   priority. 
          */
         if (other.ofKind(NonPortable))
         {
             /* the other can't be moved, so use its location */
             return [other.location, other.location, 0];
         }
         else
         {
             /* 
              *   the other can be moved, so use our own location, in a
              *   paraphrase of the realty agent's favorite mantra 
              */
             return [location, location, 0];
         }
     }
 
     /* when an attached object is being moved, detach the objects */
     moveWhileAttached(movedObj, newCont)
     {
         /* 
          *   If I'm the one being moved, detach me from all of my
          *   non-permanent attachments; otherwise, just detach me from the
          *   other object, since it's the only one of my attachments being
          *   moved.  
          */
         if (movedObj == self)
         {
             /* I'm being moved - detach from everything */
             foreach (local cur in attachedObjects)
             {
                 /* 
                  *   If we're not permanently attached to this one, and
                  *   it's not inside me, detach from it.  We don't need to
                  *   detach from objects inside this one, because they'll
                  *   be moved along with us automatically.  
                  */
                 if (!cur.isIn(self) && !isPermanentlyAttachedTo(cur))
                     nestedDetachFrom(cur);
             }
         }
         else
         {
             /* just detach from the one object */
             nestedDetachFrom(movedObj);
         }
     }
 
     /* perform a nested DetachFrom action on the given object */
     nestedDetachFrom(obj)
     {
         /* run the nested DetachFrom as an implied action */
         tryImplicitAction(DetachFrom, self, obj);
 
         /* 
          *   if we're still attached to this object, the implied command
          *   must have failed, so abort the entire action 
          */
         if (attachedObjects.indexOf(obj) != nil)
             exit;
     }
 ;
 
 /*
  *   Precondition for nearby-attachables.  This ensures that the two
  *   objects being attached are in their negotiated locations.  
  */
 class nearbyAttachableCond: PreCondition
     /* carry out the precondition */
     checkPreCondition(obj, allowImplicit)
     {
         local dobjProposal, iobjProposal;
         local dobjTargetLoc, iobjTargetLoc;
         local iobjRet, dobjRet;
 
         /*
          *   Ask each of the NearbyAttachable objects (the direct and
          *   indirect objects) what it thinks.  If an object isn't a
          *   NearbyAttachable, it won't have any opinion, so use a
          *   placeholder result with an extremely negative priority
          *   (ensuring that it won't be chosen).  In order for this
          *   precondition to have been triggered, one or the other of the
          *   objects must have been a nearby-attachable.  
          */
         dobjProposal = (gDobj.ofKind(NearbyAttachable)
                         ? gDobj.getNearbyAttachmentLocs(gIobj)
                         : [nil, nil, -2147483648]);
         iobjProposal = (gIobj.ofKind(NearbyAttachable)
                         ? gIobj.getNearbyAttachmentLocs(gDobj)
                         : [nil, nil, -2147483648]);
 
         /* 
          *   If the direct object claims higher priority, use its
          *   attachment locations; otherwise, use the direct object's
          *   locations.  (This means that we take the indirect object's
          *   proposed locations if the priorites are equal.)  
          */
         if (dobjProposal[3] > iobjProposal[3])
         {
             /* the direct object claims higher priority, so use its results */
             dobjTargetLoc = dobjProposal[1];
             iobjTargetLoc = dobjProposal[2];
         }
         else
         {
             /* 
              *   The direct object doesn't have a higher priority, so use
              *   the indirect object's results.  Note that the iobj
              *   results list has the iobj in the first position, since it
              *   was the 'self' when we asked it for its proposal.  
              */
             dobjTargetLoc = iobjProposal[2];
             iobjTargetLoc = iobjProposal[1];
         }
         
         /* carry out the pair of moves as needed */
         dobjRet = moveObject(gDobj, dobjTargetLoc, allowImplicit);
         iobjRet = moveObject(gIobj, iobjTargetLoc, allowImplicit);
 
         /* 
          *   Return the indication of whether or not we carried out an
          *   implied command.  (Note that we can't call moveObject in this
          *   'return' expression directly, because of the short-circuit
          *   behavior of the '||' operator.  We must call both, even if
          *   both carry out an action.)  
          */
         return (dobjRet || iobjRet);
     }
 
     /* carry out an implied action to move an object to a location */
     moveObject(obj, loc, allowImplicit)
     {
         /* if the object is already there, we have nothing to do */
         if (obj.location == loc)
             return nil;
 
         /* try the implied move */
         if (allowImplicit && loc.tryMovingObjInto(obj))
         {
             /* make sure it worked */
             if (obj.location != loc)
                 exit;
 
             /* we performed an implied action */
             return true;
         }
 
         /* we can't move it - report failure and abort */
         loc.mustMoveObjInto(obj);
         exit;
     }
 ;
 
 /*
  *   A PlugAttachable is a mix-in class that turns PLUG INTO into ATTACH TO
  *   and UNPLUG FROM into DETACH FROM.  This can be combined with
  *   Attachable or an Attachable subclass for objects that can be attached
  *   with PLUG INTO commands.  
  */
 class PlugAttachable: object
     /* PLUG IN - to what? */
     dobjFor(PlugIn)
     {
         verify() { }
         action() { askForIobj(PlugInto); }
     }
 
     /* PLUG INTO is the same as ATTACH TO for us */
     dobjFor(PlugInto) remapTo(AttachTo, self, IndirectObject)
     iobjFor(PlugInto) remapTo(AttachTo, DirectObject, self)
 
     /* UNPLUG FROM is the same as DETACH FROM */
     dobjFor(Unplug) remapTo(Detach, self)
     dobjFor(UnplugFrom) remapTo(DetachFrom, self, IndirectObject)
     iobjFor(UnplugFrom) remapTo(DetachFrom, DirectObject, self)
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   Permanent attachments.  This class is for things that are described
  *   in the story text as attached to one another, but which can never be
  *   separated.  This is a mix-in class that can be combined with a Thing
  *   subclass.
  *   
  *   Descriptions of attachment tend to invite the player to try detaching
  *   the parts; the purpose of this class is to provide responses that are
  *   better than the defaults.  A good custom message for this class
  *   should usually acknowledge the attachment relationship, and explain
  *   why the parts can't be separated.
  *   
  *   There are two ways to express the attachment relationship.
  *   
  *   First, the more flexible way: in each PermanentAttachment object,
  *   define the 'attachedObjects' property to contain a list of the
  *   attached objects.  All of those other attached objects should usually
  *   be PermanentAttachment objects themselves, because the real-world
  *   relationship we're modeling is obviously symmetrical.  Because of the
  *   symmetrical relationship, it's only necessary to include the list
  *   entry on one side of a pair of attached objects - each side will
  *   automatically link itself to the other at start-up if it appears in
  *   the other's attachedObjects list.
  *   
  *   Second, the really easy way: if one of the attached objects is
  *   directly inside the other (which often happens for permanent
  *   attachments, because one is a component of the other), make the
  *   parent a PermanentAttachment, make the inner one a
  *   PermanentAttachmentChild, and you're done.  The two will
  *   automatically link up their attachment lists at start-up.
  *   
  *   Note that this is a subclass of Attachable.  Note also that a
  *   PermanentAttachment can be freely combined with a regular Attachable;
  *   for example, you could create a rope with a hook permanently
  *   attached, but stil allow the rope to be attached to other things as
  *   well: you'd make the rope a regular Attachable, and make the hook a
  *   PermanentAttachment.  The hook would be unremovable because of its
  *   permanent status, and this would symmetrical prevent the rope from
  *   being removed from the hook.  But the rope could still be attached to
  *   and detached from other objects.  
  */
 class PermanentAttachment: Attachable
     /*
      *   Get the message explaining why we can't detach from 'obj'.
      *   
      *   By default, if our container is also a PermanentAttachment, and
      *   we're attached to it, we'll simply return its message.  This
      *   makes it really easy to define symmetrical permanent attachment
      *   relationships using containment, since all you have to do is make
      *   the container and the child both be PermanentAttachments, and
      *   then just define the cannot-detach message in the container.  If
      *   the container isn't a PermanentAttachment, or we're not attached
      *   to it, we'll return our default library message.  
      */
     cannotDetachMsg(obj)
     {
         if (location != nil
             && location.ofKind(PermanentAttachment)
             && isAttachedTo(location))
             return location.cannotDetachMsg(obj);
         else
             return baseCannotDetachMsg;
     }
 
     /* basic message to use when we try to detach something from self */
     baseCannotDetachMsg = &cannotDetachPermanentMsg
 ;
 
 /*
  *   A permanent attachment "child" - this is an attachment that's
  *   explicitly attached to its container object.  This is a convenient
  *   way of setting up an attachment relationship between container and
  *   contents when the contents object isn't a Component.  
  */
 class PermanentAttachmentChild: PermanentAttachment
     /* we're attached directly to our container */
     attachedObjects = perInstance([location])
 ;
 
 /* ------------------------------------------------------------------------ */
 /*
  *   A mix-in class for objects that don't come into play until some
  *   future event.  This class lets us initialize these objects with their
  *   *eventual* location, using the standard '+' syntax, but they won't
  *   actually appear in the given location until later in the game.
  *   During pre-initialization, we'll remember the starting location, then
  *   set the actual location to nil; later, the object can be easily moved
  *   to its eventual location by calling makePresent().  
  */
 class PresentLater: object
     /*
      *   My "key" - this is an optional property you can add to a
      *   PresentLater object to associate it with a group of objects.  You
      *   can then use makePresentByKey() to move every object with a given
      *   key into the game world at once.  This is useful when an event
      *   triggers a whole set of objects to come into the game world:
      *   rather than having to write a method that calls makePresent() on
      *   each of the related objects individually, you can simply give each
      *   related object the same key value, then call makePresentByKey() on
      *   that key.
      *   
      *   You don't need to define this for an object unless you want to use
      *   makePresentByKey() with the object.  
      */
     plKey = nil
 
     /*
      *   Flag: are we present initially?  By default, we're only present
      *   later, as that's the whole point.  In some cases, though, we have
      *   objects that come and go, but start out present.  Setting this
      *   property to true makes the object present initially, but still
      *   allows it to come and go using the standard PresentLater
      *   mechanisms.  
      */
     initiallyPresent = nil
 
     initializeLocation()
     {
         /* 
          *   Save the initial location for later, and then clear out the
          *   current location.  We want to start out being out of the game,
          *   but remember where we'll appear when called upon.  To
          *   accommodate MultiLoc objects, check locationList first.  
          */
         if (locationList != nil)
         {
             /* save the location list */
             eventualLocation = locationList;
 
             /* 
              *   clear my location list if I'm not initially present; if I
              *   am initially present, inherit the normal initialization 
              */
             if (!initiallyPresent)
                 locationList = [];
             else
                 inherited();
         }
         else
         {
             /* save my eventual location */
             eventualLocation = location;
 
             /* 
              *   clear my location if I'm not initially present; if I am
              *   present initially, inherit the normal set-up 
              */
             if (!initiallyPresent)
                 location = nil;
             else
                 inherited();
         }
     }
 
     /* bring the object into the game world in its eventual location(s) */
     makePresent()
     {
         local pc;
         
         /* 
          *   If we have a list, add ourself to each location in the list;
          *   otherwise, simply move ourself to the single location.  
          */
         if (eventualLocation != nil && eventualLocation.ofKind(Collection))
             eventualLocation.forEach({loc: moveIntoAdd(loc)});
         else
             moveInto(eventualLocation);
 
         /* if the player character can now see me, mark me as seen */
         pc = gPlayerChar;
         if (pc.canSee(self))
         {
             /* mark me as seen */
             pc.setHasSeen(self);
 
             /* mark my visible contents as seen */
             setContentsSeenBy(pc.visibleInfoTable(), pc);
         }
     }
 
     /* 
      *   make myself present if the given condition is true; otherwise,
      *   remove me from the game world (i.e. move me into nil)
      */
     makePresentIf(cond)
     {
         if (cond)
             makePresent();
         else
             moveInto(nil);
     }
 
     /* 
      *   Bring every PresentLater object with the given key into the game.
      *   Note that this is a "class" method that you call on PresentLater
      *   itself:
      *   
      *   PresentLater.makePresentByKey('foo'); 
      */
     makePresentByKey(key)
     {
         /* 
          *   scan every PresentLater object, and move each one with the
          *   given key into the game 
          */
         forEachInstance(PresentLater, new function(obj) {
             if (obj.plKey == key)
                 obj.makePresent();
         });
     }
 
     /* 
      *   Bring every PresentLater object with the given key into the game,
      *   or move every one out of the game, according to the condition
      *   'cond'.
      *   
      *   If 'cond' is a function pointer, we'll invoke it once per object
      *   with the given key, passing the object as the parameter, and use
      *   the return value as the in game/out of game setting.  For example,
      *   if you wanted to show every object with key 'foo' AND with the
      *   property 'showObj' set to true, you could write this:
      *   
      *   PresentLater.makePresentByKeyIf('foo', {x: x.showObj});
      *   
      *   Note that this is a "class" method that you call on PresentLater
      *   itself.  
      */
     makePresentByKeyIf(key, cond)
     {
         /* 
          *   scan every PresentLater object, check each one's key, and make
          *   each one with the given key present 
          */
         forEachInstance(PresentLater, new function(obj) {
             /* consider this object if its key matches */
             if (obj.plKey == key)
             {
                 local flag = cond;
                 
                 /* 
                  *   evaluate the condition - if it's a function pointer,
                  *   invoke it on the current object, otherwise just take
                  *   it as a pre-evaluated condition value 
                  */
                 if (dataTypeXlat(cond) == TypeFuncPtr)
                     flag = (cond)(obj);
 
                 /* show or hide the object according to the condition */
                 obj.makePresentIf(flag);
             }
         });
     }
 
     /* our eventual location */
     eventualLocation = nil
 ;
 
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11