# 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

        /* a list of all of our component objects */
        allSubLocations = [subContainer, subSurface, subUnderside, subRear]

        /*
         *   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)

        /* route commands relevant to nested rooms to our components */
        dobjFor(StandOn) maybeRemapTo(getNestedRoomDest(StandOnAction) != nil,
                                      StandOn, getNestedRoomDest(StandOnAction))
        dobjFor(SitOn) maybeRemapTo(getNestedRoomDest(SitOnAction) != nil,
                                    SitOn, getNestedRoomDest(SitOnAction))
        dobjFor(LieOn) maybeRemapTo(getNestedRoomDest(LieOnAction) != nil,
                                    LieOn, getNestedRoomDest(LieOnAction))
        dobjFor(Board) maybeRemapTo(getNestedRoomDest(BoardAction) != nil,
                                    Board, getNestedRoomDest(BoardAction))
        dobjFor(Enter) maybeRemapTo(getNestedRoomDest(EnterAction) != nil,
                                    Enter, getNestedRoomDest(EnterAction))

        /* map GET OUT/OFF to whichever complex component we're currently in */
        dobjFor(GetOutOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
                                       GetOutOf, getNestedRoomSource(gActor))
        dobjFor(GetOffOf) maybeRemapTo(getNestedRoomSource(gActor) != nil,
                                       GetOffOf, getNestedRoomSource(gActor))

        /*
         *   Get the destination for nested travel into this object.  By
         *   default, we'll look at the sub-container and sub-surface
         *   components to see if either is a nested room, and if so, we'll
         *   return that.  The surface takes priority if both are nested rooms.
         *
         *   You can override this to differentiate by verb, if desired; for
         *   example, you could have SIT ON and LIE ON refer to the sub-surface
         *   component, while ENTER and BOARD refer to the sub-container
         *   component.
         *
         *   Note that if you do need to override this method to distinguish
         *   between a sub-container ("IN") and a sub-surface ("ON") for nested
         *   room purposes, there's a subtlety to watch out for.  The English
         *   library maps "sit on" and "sit in" to the single Action SitOn;
         *   likewise with "lie in/on" for LieOn and "stand in/on" for StandOn.
         *   If you're distinguishing the sub-container from the sub-surface,
         *   you'll probably want to distinguish SIT IN from SIT ON (and
         *   likewise for LIE and STAND).  Fortunately, even though the action
         *   class is the same for both phrasings, you can still find out
         *   exactly which preposition the player typed using
         *   action.getEnteredVerbPhrase().
         */
        getNestedRoomDest(action)
        {
            /*
             *   check the sub-surface first to see if it's a nested room;
             *   failing that, check the sub-container; failing that, we don't
             *   have a suitable component destination
             */
            if (subSurface != nil && subSurface.ofKind(NestedRoom))
                return subSurface;
            else if (subContainer != nil && subContainer.ofKind(NestedRoom))
                return subContainer;
            else
                return nil;
        }

        /*
         *   Get the source for nested travel out of this object.  This is used
         *   for GET OUT OF <self> - we figure out which nested room component
         *   the actor is in, so that we can remap the command to GET OUT OF
         *   <that component>.
         */
        getNestedRoomSource(actor)
        {
            /* figure out which child the actor is in */
            foreach (local chi in allSubLocations)
            {
                if (chi != nil && actor.isIn(chi))
                    return chi;
            }

            /* the actor doesn't appear to be in one of our component locations */
            return nil;
        }

        /*
         *   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; }

        /*
         *   In case this component is being used to implement a nested room of
         *   some kind (a platform, booth, etc), use the complex container's
         *   location as the staging location.  Normally our staging location
         *   would be our direct container, but as with other aspects of
         *   complex containers, the container/component are meant to act as a
         *   single combined object, so we'd want to bypass the complex
         *   container and move directly between the enclosing location and
         *   'self'.
         */
        stagingLocations = [lexicalParent.location]
    ;

    /*
     *   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);
            }
        }

        /*
         *   My weight does NOT include my "contents" if we abandon our
         *   contents on being moved.  Our contents are not attached to us as
         *   they are in a normal sort of container; instead, they're merely
         *   colocated, so when we're moved, that colocation relationship ends.
         */
        getWeight()
        {
            /*
             *   if we abandon our contents on being moved, our weight doesn't
             *   include them, because they're not attached; otherwise, they do
             *   act like they're attached, and hence must be included in our
             *   weight as for any ordinary container
             */
            if (abandonLocation != nil)
            {
                /*
                 *   our contents are not included in our weight, so our total
                 *   weight is simply our own intrinsic weight
                 */
                return weight;
            }
            else
            {
                /* our contents are attached, so include their weight as normal */
                return inherited();
            }
        }

        /*
         *   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

        /* our implied containment verb is PUT UNDER */
        tryMovingObjInto(obj) { return tryImplicitAction(PutUnder, obj, self); }

        /* -------------------------------------------------------------------- */
        /*
         *   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

        /* our implied containment verb is PUT BEHIND */
        tryMovingObjInto(obj) { return tryImplicitAction(PutBehind, obj, self); }

        /* -------------------------------------------------------------------- */
        /*
         *   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)
        {
            /*
             *   Do any inherited work, in case we have a limit on our own
             *   internal bulk.
             */
            inherited(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.
         */
        cannotDetachMsgFor(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(cannotDetachMsgFor(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(cannotDetachMsgFor(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(cannotDetachMsgFor(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(cannotDetachMsgFor(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.
     */
    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.
         */
        cannotDetachMsgFor(obj)
        {
            if (location != nil
                && location.ofKind(PermanentAttachment)
                && isAttachedTo(location))
                return location.cannotDetachMsgFor(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, 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, 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 8/21/2012 from TADS version 3.1.2