# actor.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library - actors
     *
     *   This module provides definitions related to actors, which represent
     *   characters in the game.
     */

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

    /* ------------------------------------------------------------------------ */
    /*
     *   Implied command modes
     */
    enum ModePlayer, ModeNPC;

    /* ------------------------------------------------------------------------ */
    /*
     *   A Topic is an object representing some piece of knowledge in the
     *   story.  Actors can use Topic objects in commands such as "ask" and
     *   "tell".
     *
     *   A physical simulation object can be a Topic through multiple
     *   inheritance.  In addition, a game can define Topic objects for
     *   abstract conversation topics that don't correspond to simulation
     *   objects; for example, a topic could be created for "the meaning of
     *   life" to allow a command such as "ask guru about meaning of life."
     *
     *   The key distinction between Topic objects and regular objects is that
     *   a Topic can represent an abstract, non-physical concept that isn't
     *   connected to any "physical" object in the simulation.
     */
    class Topic: VocabObject
        /*
         *   Is the topic known?  If this is true, the topic is in scope for
         *   actions that operate on topics, such as "ask about" and "tell
         *   about."  If this is nil, the topic isn't known.
         *
         *   By default, we mark all topics as known to begin with, which
         *   allows discussion of any topic at any time.  Some authors prefer
         *   to keep track of which topics the player character actually has
         *   reason to know about within the context of the game, making topics
         *   available for conversation only after they become known for some
         *   good reason, such as another character mentioning them in
         *   conversation.
         *
         *   Note that, as with Thing.isKnown, this is only the DEFAULT 'known'
         *   property.  Each actor can have its own separate 'known' property
         *   by defining the actor's 'knownProp' to a different property name.
         */
        isKnown = true

        /*
         *   Topics are abstract objects, so they can't be sensed with any of
         *   the physical senses, even if they're ever included as part of a
         *   containment hierarchy (which might be convenient in some cases
         *   for purposes of associating a topic with a physical object, for
         *   example).
         */
        canBeSensed(sense, trans, ambient) { return nil; }

        /* a topic cannot by default be used to resolve a possessive phrase */
        canResolvePossessive = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   FollowInfo - this is an object that tracks an actor's knowledge of
     *   the objects that the actor can follow, which are objects that actor
     *   has witnessed leaving the current location.  We keep track of each
     *   followable object and the direction we saw it depart.
     */
    class FollowInfo: object
        /* the object we can follow */
        obj = nil

        /* the TravelConnector the object traversed to leave */
        connector = nil

        /*
         *   The source location - this is the location we saw the object
         *   depart.  We keep track of this because an actor can follow an
         *   object only if the actor is starting from the same location where
         *   the actor saw the object depart.
         */
        sourceLocation = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Postures.  A posture describes how an actor is internally positioned:
     *   standing, lying, sitting.  We represent postures with objects of
     *   class Posture to make it easier to add new game-specific postures.
     */
    class Posture: object
        /*
         *   Try getting the current actor into this posture within the given
         *   location, by running an appropriate implied command.
         */
        tryMakingPosture(loc) { }

        /* put the actor into our posture via a nested action */
        setActorToPosture(actor, loc) { }
    ;

    /*
     *   Standing posture - this is the default posture, which an actor
     *   normally uses for travel.  Actors are generally in this posture any
     *   time they are not sitting on something, lying on something, or
     *   similar.
     */
    standing: Posture
        tryMakingPosture(loc) { return tryImplicitAction(StandOn, loc); }
        setActorToPosture(actor, loc) { nestedActorAction(actor, StandOn, loc); }
    ;

    /*
     *   Sitting posture.
     */
    sitting: Posture
        tryMakingPosture(loc) { return tryImplicitAction(SitOn, loc); }
        setActorToPosture(actor, loc) { nestedActorAction(actor, SitOn, loc); }
    ;

    /*
     *   Lying posture.
     */
    lying: Posture
        tryMakingPosture(loc) { return tryImplicitAction(LieOn, loc); }
        setActorToPosture(actor, loc) { nestedActorAction(actor, LieOn, loc); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Conversation manager output filter.  We look for special tags in the
     *   output stream:
     *
     *   <.reveal key> - add 'key' to the knowledge token lookup table.  The
     *   'key' is an arbitrary string, which we can look up in the table to
     *   determine if the key has even been revealed.  This can be used to make
     *   a response conditional on another response having been displayed,
     *   because the key will only be added to the table when the text
     *   containing the <.reveal key> sequence is displayed.
     *
     *   <.convnode name> - switch the current responding actor to conversation
     *   node 'name'.
     *
     *   <.convstay> - keep the responding actor in the same conversation node
     *   as it was in at the start of the current response
     *
     *   <.topics> - schedule a topic inventory for the end of the turn (just
     *   before the next command prompt)
     */
    conversationManager: OutputFilter, PreinitObject
        /*
         *   Custom extended tags.  Games and library extensions can add their
         *   own tag processing as needed, by using 'modify' to extend this
         *   object.  There are two things you have to do to add your own tags:
         *
         *   First, add a 'customTags' property that defines a regular
         *   expression for your added tags.  This will be incorporated into
         *   the main pattern we use to look for tags.  Simply specify a
         *   string that lists your tags separated by "|" characters, like
         *   this:
         *
         *   customTags = 'foo|bar'
         *
         *   Second, define a doCustomTag() method to process the tags.  The
         *   filter routine will call your doCustomTag() method whenever it
         *   finds one of your custom tags in the output stream.
         */
        customTags = nil
        doCustomTag(tag, arg) { /* do nothing by default */ }

        /* filter text written to the output stream */
        filterText(ostr, txt)
        {
            local start;

            /* scan for our special tags */
            for (start = 1 ; ; )
            {
                local match;
                local arg;
                local actor;
                local sp;
                local tag;
                local nxtOfs;

                /* scan for the next tag */
                match = rexSearch(tagPat, txt, start);

                /* if we didn't find it, we're done */
                if (match == nil)
                    break;

                /* note the next offset */
                nxtOfs = match[1] + match[2];

                /* get the argument (the third group from the match) */
                arg = rexGroup(3);
                if (arg != nil)
                    arg = arg[3];

                /* pick out the tag */
                tag = rexGroup(1)[3].toLower();

                /* check which tag we have */
                switch (tag)
                {
                case 'reveal':
                    /* reveal the key by adding it to our database */
                    setRevealed(arg);
                    break;

                case 'convbegin':
                    /*
                     *   Internal tag - starting a conversational response for
                     *   an actor, identified by an index in our idToActor
                     *   vector.  Get the actor.
                     */
                    actor = idToActor[toInteger(arg)];

                    /*
                     *   since we're just starting a response, clear the flag
                     *   in the actor indicating that a ConvNode has been set
                     *   in the course of this response
                     */
                    actor.responseSetConvNode = nil;

                    /* remember the new responding actor */
                    respondingActor = actor;

                    /* done */
                    break;

                case 'convend':
                    /*
                     *   Ending a conversational response for a given actor,
                     *   identified by the first argument, which is an index in
                     *   our idToActor vector.
                     */
                    sp = arg.find(' ');
                    actor = idToActor[toInteger(arg.substr(1, sp - 1))];

                    /* the rest of the argument is the default new ConvNode */
                    arg = arg.substr(sp + 1);

                    /* if the new ConvNode is empty, it means no ConvNode */
                    if (arg == '')
                        arg = nil;

                    /*
                     *   if we didn't explicitly set a new ConvNode in the
                     *   course of this response, apply the default
                     */
                    if (!actor.responseSetConvNode)
                        actor.setConvNodeReason(arg, 'convend');

                    /*
                     *   Since we've just finished showing a message that
                     *   specifically refers to this actor, the player should
                     *   be able to refer to this actor using a pronoun on the
                     *   next command.  Set the responding actor as the
                     *   antecedent for the appropriate singular pronouns for
                     *   the player character.  Note that we do this at the end
                     *   of the response, so that the antecedent is the last
                     *   one if we have more than one.
                     */
                    gPlayerChar.setPronounObj(actor);

                    /* done */
                    break;

                case 'convnode':
                    /*
                     *   If there's a current responding actor, set its current
                     *   conversation node.
                     */
                    if (respondingActor != nil)
                    {
                        /*
                         *   Set the new node.  While we're working, capture
                         *   any output that occurs so that we can insert it
                         *   into the output stream just after the <.convnode>
                         *   tag, so that any text displayed within the
                         *   ConvNode's activation method (noteActive) is
                         *   displayed in the proper order.
                         */
                        local ctxt = mainOutputStream.captureOutput(
                            {: respondingActor.setConvNodeReason(arg, 'convnode') });

                        /* re-insert any text we captured */
                        txt = txt.substr(1, nxtOfs - 1)
                            + ctxt
                            + txt.substr(nxtOfs);
                    }
                    break;

                case 'convstay':
                    /*
                     *   leave the responding actor in the old conversation
                     *   node - we don't need to change the ConvNode, but we do
                     *   need to note that we've explicitly set it
                     */
                    if (respondingActor != nil)
                        respondingActor.responseSetConvNode = true;
                    break;

                case 'topics':
                    /* schedule a topic inventory listing */
                    scheduleTopicInventory();
                    break;

                default:
                    /* check for an extended tag */
                    doCustomTag(tag, arg);
                    break;
                }

                /* continue the search after this match */
                start = nxtOfs;
            }

            /*
             *   remove the tags from the text by replacing every occurrence
             *   with an empty string, and return the result
             */
            return rexReplace(tagPat, txt, '', ReplaceAll);
        }

        /* regular expression pattern for our tags */
        tagPat = static new RexPattern(
            '<nocase><langle><dot>'
            + '(reveal|convbegin|convend|convnode|convstay|topics'
            + (customTags != nil ? '|' + customTags : '')
            + ')'
            + '(<space>+(<^rangle>+))?'
            + '<rangle>')

        /*
         *   Schedule a topic inventory request.  Game code can call this at
         *   any time to request that the player character's topic inventory
         *   be shown automatically just before the next command prompt.  In
         *   most cases, game code won't call this directly, but will request
         *   the same effect using the <.topics> tag in topic response text.
         */
        scheduleTopicInventory()
        {
            /* note that we have a request for a prompt-time topic inventory */
            pendingTopicInventory = true;
        }

        /*
         *   Show or schedule a topic inventory request.  If the current
         *   action has a non-default command report, schedule it; otherwise,
         *   show it now.
         *
         *   If there's a non-default report, don't suggest the topics now;
         *   instead, schedule a topic inventory for the end of the turn.
         *   When we have a non-default report, the report could change the
         *   ConvNode for the actor, so we don't want to show the topic
         *   inventory until we've had a chance to process all of the reports.
         */
        showOrScheduleTopicInventory(actor, otherActor)
        {
            /* check for a non-default command report in the current action */
            if (gTranscript.currentActionHasReport(
                {x: x.ofKind(MainCommandReport)}))
            {
                /* we have a non-default report - defer the topic inventory */
                scheduleTopicInventory();
            }
            else
            {
                /* we have only a default report, so show the inventory now */
                actor.suggestTopicsFor(otherActor, nil);
            }
        }

        /*
         *   Note that an actor is about to give a response through a
         *   TopicEntry object.  We'll remember the actor so that we'll know
         *   which actor is involved in a <.convnode> operation.
         */
        beginResponse(actor)
        {
            /* if the actor doesn't have an ID yet, assign one */
            if (actor.convMgrID == nil)
            {
                /* add the actor to our vector of actors */
                idToActor.append(actor);

                /* the ID is simply the index in this vector */
                actor.convMgrID = idToActor.length();
            }

            /* output a <.convbegin> for the actor */
            gTranscript.addReport(new ConvBeginReport(actor.convMgrID));
        }

        /*
         *   Finish the response - call this after we finish handling the
         *   response.  There must be a subsequent matching call to this
         *   routine whenever beginResponse() is called.
         *
         *   'node' is the default new ConvNode the actor for the responding
         *   actor.  If another ConvNode was explicitly set in the course of
         *   handling the response, this is ignored, since the explicit
         *   setting overrides this default.
         */
        finishResponse(actor, node)
        {
            local prv;
            local oldNode;

            /* if the node is a ConvNode object, use its name */
            if (node != nil && node.ofKind(ConvNode))
                node = node.name;

            /*
             *   if the previous report was our ConvBeginReport, the
             *   conversation display was empty, so ignore the whole thing
             */
            if ((prv = gTranscript.getLastReport()) != nil
                && prv.ofKind(ConvBeginReport)
                && prv.actorID == actor.convMgrID)
            {
                /* remove the <.convbegin> report - we're canceling it out */
                gTranscript.deleteLastReport();

                /* we're done - do not generate the <.convend> */
                return;
            }

            /*
             *   if the actor has a current ConvNode, and our default next
             *   node is nil, and the current node is marked as "sticky," stay
             *   in the current node rather than switching to a nil default
             */
            if (node == nil
                && (oldNode = actor.curConvNode) != nil
                && oldNode.isSticky)
            {
                /* it's sticky, so stay at this node */
                node = oldNode.name;
            }

            /* output a <.convend> for the actor */
            gTranscript.addReport(new ConvEndReport(actor.convMgrID, node));
        }

        /*
         *   The current responding actor.  Actors should set this when they're
         *   about to show a response to an ASK, TELL, etc.
         */
        respondingActor = nil

        /*
         *   Mark a tag as revealed.  This adds an entry for the tag to the
         *   revealedNameTab table.  We simply set the table entry to 'true';
         *   the presence of the tag in the table constitutes the indication
         *   that the tag has been revealed.
         *
         *   (Games and library extensions can use 'modify' to override this
         *   and store more information in the table entry.  For example, you
         *   could store the time when the information was first revealed, or
         *   the location where it was learned.  If you do override this, just
         *   be sure to set the revealedNameTab entry for the tag to a non-nil
         *   and non-zero value, so that any code testing the presence of the
         *   table entry will see that the slot is indeed set.)
         */
        setRevealed(tag)
        {
            revealedNameTab[tag] = true;
        }

        /*
         *   The global lookup table of all revealed keys.  This table is keyed
         *   by the string naming the revelation; the value associated with
         *   each key is not used (we always just set it to true).
         */
        revealedNameTab = static new LookupTable(32, 32)

        /* a vector of actors, indexed by their convMgrID values */
        idToActor = static new Vector(32)

        /* preinitialize */
        execute()
        {
            /* add every ConvNode object to our master table */
            forEachInstance(ConvNode,
                            { obj: obj.getActor().convNodeTab[obj.name] = obj });

            /*
             *   set up the prompt daemon that makes automatic topic inventory
             *   suggestions when appropriate
             */
            new PromptDaemon(self, &topicInventoryDaemon);
        }

        /*
         *   Prompt daemon: show topic inventory when appropriate.  When a
         *   response explicitly asks us to show a topic inventory using the
         *   <.topics> tag, or when other game code asks us to show topic
         *   inventory by calling scheduleTopicInventory(), we'll show the
         *   inventory just before the command input prompt.
         */
        topicInventoryDaemon()
        {
            /* if we have a topic inventory scheduled, show it now */
            if (pendingTopicInventory)
            {
                /*
                 *   Show the player character's topic inventory.  This is not
                 *   an explicit inventory request, since the player didn't ask
                 *   for it.
                 */
                gPlayerChar.suggestTopics(nil);

                /* we no longer have a pending inventory request */
                pendingTopicInventory = nil;
            }
        }

        /* flag: we have a pending prompt-time topic inventory request */
        pendingTopicInventory = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A plug-in topic database.  The topic database is a set of TopicEntry
     *   objects that specify the responses to queries on particular topics.
     *   The exact nature of the queries that a particular topic database
     *   handles is up to the database subclass to define; we just provide the
     *   abstract mechanism for finding and displaying responses.
     *
     *   This is a "plug-in" database in that it's meant to be added into other
     *   classes using multiple inheritance.  This isn't meant to be used as a
     *   stand-alone abstract topic entry container.
     */
    class TopicDatabase: object
        /*
         *   Is the topic group active?  A TopicEntry always checks with its
         *   container to see if the children of the container are active.  By
         *   default, everything in the database is active.
         */
        topicGroupActive = true

        /*
         *   Get the score adjustment for all topic entries contained within.
         *   The default adjustment is zero; TopicGroup objects can use this to
         *   adjust the score for their nested entries.
         */
        topicGroupScoreAdjustment = 0

        /*
         *   Handle a topic.  Look up the topic in our topic list for the
         *   given conversational action type.  If we find a match, we'll
         *   invoke the matching topic list entry to handle it.  We'll return
         *   true if we find a match, nil if not.
         */
        handleTopic(fromActor, topic, convType, path)
        {
            local resp;

            /* find the best response */
            resp = findTopicResponse(fromActor, topic, convType, path);

            /* if we found a match, let it handle the topic */
            if (resp != nil)
            {
                /* show the response */
                showTopicResponse(fromActor, topic, resp);

                /* tell the caller we handled it */
                return true;
            }
            else
            {
                /* tell the caller we didn't handle it */
                return nil;
            }
        }

        /* show the response we found for a topic */
        showTopicResponse(fromActor, topic, resp)
        {
            /* let the response object handle it */
            resp.handleTopic(fromActor, topic);
        }

        /*
         *   find the best response (a TopicEntry object) for the given topic
         *   (a ResolvedTopic object)
         */
        findTopicResponse(fromActor, topic, convType, path)
        {
            local topicList;
            local best, bestScore;

            /*
             *   Get the list of possible topics for this conversation type.
             *   The topic list is contained in one of our properties; exactly
             *   which property is determined by the conversation type.
             */
            topicList = self.(convType.topicListProp);

            /* if the topic list is nil, we obviously won't find the topic */
            if (topicList == nil)
                return nil;

            /* scan our topic list for the best match(es) */
            best = new Vector();
            bestScore = nil;
            foreach (local cur in topicList)
            {
                /* get this item's score */
                local score = cur.adjustScore(cur.matchTopic(fromActor, topic));

                /*
                 *   If this item has a score at all, and the topic entry is
                 *   marked as active, and it's best (or only) score so far,
                 *   note it.  Ignore topics marked as not active, since
                 *   they're in the topic database only provisionally.
                 */
                if (score != nil
                    && cur.checkIsActive()
                    && (bestScore == nil || score >= bestScore))
                {
                    /* clear the vector if we've found a better score */
                    if (bestScore != nil && score > bestScore)
                        best = new Vector();

                    /* add this match to the list of ties for this score */
                    best.append(cur);

                    /* note the new best score */
                    bestScore = score;
                }
            }

            /*
             *   If the best-match list is empty, we have no matches.  If
             *   there's just one match, we have a winner.  If we found more
             *   than one match tied for first place, we need to pick one
             *   winner.
             */
            if (best.length() == 0)
            {
                /* no matches at all */
                best = nil;
            }
            else if (best.length() == 1)
            {
                /* exactly one match - it's easy to pick the winner */
                best = best[1];
            }
            else
            {
                /*
                 *   We have multiple topics tied for first place.  Run through
                 *   the topic list and ask each topic to propose the winner.
                 */
                local toks = topic.topicProd.getOrigTokenList().mapAll(
                    {x: getTokVal(x)});
                local winner = nil;
                foreach (local t in best)
                {
                    /* ask this topic what it thinks the winner should be */
                    winner = t.breakTopicTie(best, topic, fromActor, toks);

                    /* if the topic had an opinion, we can stop searching */
                    if (winner != nil)
                        break;
                }

                /*
                 *   If no one had an opinion, run through the list again and
                 *   try to pick by vocabulary match strength.  This is only
                 *   possible when all of the topics are associated with
                 *   simulation objects; if any topics have pattern matches, we
                 *   can't use this method.
                 */
                if (winner == nil)
                {
                    local rWinner = nil;
                    foreach (local t in best)
                    {
                        /* get this topic's match object(s) */
                        local m = t.matchObj;
                        if (m == nil)
                        {
                            /*
                             *   there's no match object - it's not comparable
                             *   to others in terms of match strength, so we
                             *   can't use this method to break the tie
                             */
                            winner = nil;
                            break;
                        }

                        /*
                         *   If it's a list, search for an element with a
                         *   ResolveInfo entry in the topic match, using the
                         *   strongest match if we find more than one.
                         *   Otherwise, just use the strength of this match.
                         */
                        local ri;
                        if (m.ofKind(Collection))
                        {
                            /* search for a ResolveInfo object */
                            foreach (local mm in m)
                            {
                                /* get this topic */
                                local riCur = topic.getResolveInfo(mm);

                                /* if this is the best match so far, keep it */
                                if (compareVocabMatch(riCur, ri) > 0)
                                    ri = riCur;
                            }
                        }
                        else
                        {
                            /* get the ResolveInfo object */
                            ri = topic.getResolveInfo(m);
                        }

                        /*
                         *   if we didn't find a match, we can't use this
                         *   method to break the tie
                         */
                        if (ri == nil)
                        {
                            winner = nil;
                            break;
                        }

                        /*
                         *   if this is the best match so far, elect it as the
                         *   tentative winner
                         */
                        if (compareVocabMatch(ri, rWinner) > 0)
                        {
                            rWinner = ri;
                            winner = t;
                        }
                    }
                }

                /*
                 *   if there's a tie-breaking winner, use it; otherwise just
                 *   arbitrarily pick the first item in the list of ties
                 */
                best = (winner != nil ? winner : best[1]);
            }

            /*
             *   If there's a hierarchical search path, AND this topic entry
             *   defines a deferToEntry() method, look for matches in the
             *   inferior databases on the path and check to see if we want to
             *   defer to one of them.
             */
            if (best != nil && path != nil && best.propDefined(&deferToEntry))
            {
                /* look for a match in each inferior database */
                for (local i = 1, local len = path.length() ; i <= len ; ++i)
                {
                    local inf;

                    /*
                     *   Look up an entry in this inferior database.  Pass in
                     *   the remainder of the path, so that the inferior
                     *   database can consider further deferral to its own
                     *   inferior databases.
                     */
                    inf = path[i].findTopicResponse(fromActor, topic, convType,
                                                    path.sublist(i + 1));

                    /*
                     *   if we found an entry in this inferior database, and
                     *   our entry defers to the inferior entry, then ignore
                     *   the match in our own database
                     */
                    if (inf != nil && best.deferToEntry(inf))
                        return nil;
                }
            }

            /* return the best matching response object, if any */
            return best;
        }

        /*
         *   Compare the vocabulary match strengths of two ResolveInfo objects,
         *   for the purposes of breaking ties in topic matching.  Uses the
         *   usual comparison/sorting return value conventions: -1 means that a
         *   is weaker than b, 0 means they're equivalent, 1 means a is
         *   stronger than b.
         */
        compareVocabMatch(a, b)
        {
            /*
             *   If both are nil, they're equivalent.  If one or the other is
             *   nil, the non-nil item is stronger.
             */
            if (a == nil && b == nil)
                return 0;
            if (a == nil)
                return -1;
            if (b == nil)
                return 1;

            /*
             *   Both are valid objects, so compare based on the vocabulary
             *   match flags.
             */
            local fa = a.flags_, fb = b.flags_;

            /* check plural truncations - no plural truncation is better */
            if ((fa & PluralTruncated) && !(fb & PluralTruncated))
                return -1;
            if (!(fa & PluralTruncated) && (fb & PluralTruncated))
                return 1;

            /* check any truncation - no truncation is better */
            if ((fa & VocabTruncated) && !(fb & VocabTruncated))
                return -1;
            if (!(fa & VocabTruncated) && (fb & VocabTruncated))
                return 1;

            /* we can't find any reason to prefer one over the other */
            return 0;
        }

        /* show our suggested topic list */
        showSuggestedTopicList(lst, asker, askee, explicit)
        {
            /* get the asking actor's scope list for use later */
            scopeList = asker.scopeList();

            /* remove items that have redundant list groups and full names */
            for (local i = 1, local len = lst.length() ; i <= len ; ++i)
            {
                local a = lst[i];

                /* check for redundant elements */
                for (local j = i + 1 ; j <= len ; ++j)
                {
                    local b = lst[j];

                    /*
                     *   If item 'a' matches item 'b', and both are active,
                     *   remove item 'b'.  We only need to remove redundant
                     items if they're both active, since inactive items
                     */
                    if (a.suggestionGroup == b.suggestionGroup
                        && a.fullName == b.fullName
                        && a.isSuggestionActive(asker, scopeList)
                        && b.isSuggestionActive(asker, scopeList))
                    {
                        /* delete item 'b' from the list */
                        lst.removeElementAt(j);

                        /* adjust our indices for the deletion */
                        --j;
                        --len;
                    }
                }
            }

            /* show our list */
            new SuggestedTopicLister(asker, askee, explicit)
                .showList(asker, nil, lst, 0, 0, nil, nil);
        }

        /*
         *   Flag: this database level should limit topic suggestions (for the
         *   TOPICS and TALK TO commands) to its own topics, excluding any
         *   topics inherited from the "broader" context.  If this property is
         *   set to true, then we won't include suggestions from any lower
         *   level of the database hierarchy.  If this property is nil, we'll
         *   also include any topic suggestions from the broader context.
         *
         *   Topic databases are arranged into a fixed hierarchy for an actor.
         *   At the top level is the current ConvNode object; at the next level
         *   is the ActorState; and at the bottom level is the Actor itself.
         *   So, if the ConvNode's limitSuggestions property is set to true,
         *   then the suggestions for the actor will include ONLY the ConvNode.
         *   If the ConvNode has the property set to nil, but the ActorState
         *   has it set to true, then we'll include the ConvNode and the
         *   ActorState suggestions.
         *
         *   By default, we set this to nil.  This should usually be set to
         *   true for any ConvNode or ActorState where the NPC won't allow the
         *   player to stray from the subject.  For example, if a ConvNode only
         *   accepts a YES or NO response to a question, then this property
         *   should probably be set to true in the ConvNode, since other
         *   suggested topics won't be accepted as conversation topics as long
         *   as the ConvNode is active.
         */
        limitSuggestions = nil

        /*
         *   Add a topic to our topic database.  We'll add it to the
         *   appropriate list or lists as indicated in the topic itself.
         *   'topic' is a TopicEntry object.
         */
        addTopic(topic)
        {
            /* add the topic to each list indicated in the topic */
            foreach (local cur in topic.includeInList)
                addTopicToList(topic, cur);
        }

        /* remove a topic from our topic database */
        removeTopic(topic)
        {
            /* remove the topic from each of its lists */
            foreach (local cur in topic.includeInList)
                removeTopicFromList(topic, cur);
        }

        /* add a suggested topic */
        addSuggestedTopic(topic)
        {
            /* add the topic to our suggestion list */
            addTopicToList(topic, &suggestedTopics);
        }

        /* remove a suggested topic */
        removeSuggestedTopic(topic)
        {
            /* add the topic to our suggestion list */
            removeTopicFromList(topic, &suggestedTopics);
        }

        /*
         *   Add a topic to the given topic list.  The topic list is given as a
         *   property point; for example, we'd specify &askTopics to add the
         *   topic to our ASK list.
         */
        addTopicToList(topic, listProp)
        {
            /* if we haven't created this topic list vector yet, create it now */
            if (self.(listProp) == nil)
                self.(listProp) = new Vector(8);

            /* add the topic */
            self.(listProp).append(topic);
        }

        /* remove a topic from the given topic list */
        removeTopicFromList(topic, listProp)
        {
            /* if the list exists, remove the topic from it */
            if (self.(listProp) != nil)
                self.(listProp).removeElement(topic);
        }

        /*
         *   Our list of suggested topics.  These are SuggestedTopic objects
         *   that describe things that another actor wants to ask or tell this
         *   actor about.
         */
        suggestedTopics = nil

        /*
         *   Get the "owner" of the topics in this database.  The meaning of
         *   "owner" varies according to the topic database type; for actor
         *   topic databases, for example, this is the actor.  Generally, the
         *   owner is the object being queried about the topic, from the
         *   player's perspective.  Each type of database should define this
         *   method to return the appropriate object.
         */
        getTopicOwner() { return nil; }
    ;

    /*
     *   A TopicDatabase for an Actor.  This is used not only directly for an
     *   Actor but also for an actor's sub-databases, in ActorState and
     *   ConvNode.
     *
     *   Actor topic databases field queries for the various types of
     *   topic-based interactions an actor can participate in: ASK, TELL, SHOW,
     *   GIVE, and so on.
     *
     *   Each actor has its own topic database, which means each actor can have
     *   its own set of responses.  Actor states can also have their own
     *   separate topic databases; this makes it easy to make an actor's
     *   response to a particular question vary according to the actor's state.
     *   Conversation nodes can also have their own separate databases, which
     *   allows for things like threaded conversations.
     */
    class ActorTopicDatabase: TopicDatabase
        /*
         *   Initiate conversation on the given simulation object.  If we can
         *   find an InitiateTopic matching the given object, we'll show its
         *   topic response and return true; if we can't find a topic to
         *   initiate, we'll simply return nil.
         */
        initiateTopic(obj)
        {
            /* find an initiate topic for the given object */
            if (handleTopic(gPlayerChar, obj, initiateConvType, nil))
            {
                /*
                 *   we handled the topic, so note that we're in conversation
                 *   with the player character now
                 */
                getTopicOwner().noteConversation(gPlayerChar);

                /* indicate that we found a topic to initiate */
                return true;
            }

            /* we didn't find a topic to initiate */
            return nil;
        }

        /* show a topic response */
        showTopicResponse(fromActor, topic, resp)
        {
            local actor = getTopicOwner();
            local newNode;

            /*
             *   note whether the response is conversational - we need to do
             *   this ahead of time, since invoking the response can sometimes
             *   have the side effect of changing the response's status
             */
            local isConv = resp.isConversational;

            /* tell the conversation manager we're starting a response */
            conversationManager.beginResponse(actor);

            /* let the response object handle it */
            resp.handleTopic(fromActor, topic);

            /*
             *   By default, after showing a response, we want to leave the
             *   conversation node tree entirely if we didn't explicitly set
             *   the next node in the course of the response.  So, set the
             *   default new node to 'nil'.  However, if the topic is
             *   non-conversational, it shouldn't affect the conversation
             *   thread at all, so leave the current node unchanged.
             */
            if (isConv)
                newNode = nil;
            else
                newNode = actor.curConvNode;

            /* tell the conversation manager we're done with the response */
            conversationManager.finishResponse(actor, newNode);
        }

        /*
         *   Our 'ask about', 'ask for', 'tell about', 'give', 'show',
         *   miscellaneous, command, and self-initiated topic databases - these
         *   are vectors we initialize as needed.  Since every actor and every
         *   actor state has its own separate topic database, it's likely that
         *   the bulk of these databases will be empty, so we don't bother even
         *   creating a vector for a topic list until the first topic is added.
         *   This means we have to be able to cope with these being nil
         *   anywhere we use them.
         */
        askTopics = nil
        askForTopics = nil
        tellTopics = nil
        showTopics = nil
        giveTopics = nil
        miscTopics = nil
        commandTopics = nil
        initiateTopics = nil

        /* our special command database */
        specialTopics = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A "suggested" topic.  These provide suggestions for things the player
     *   might want to ASK or TELL another actor about.  At certain times
     *   (specifically, when starting a conversation with HELLO or TALK TO, or
     *   when the player enters a TOPICS command to explicitly ask for a list
     *   of topic suggestions), we'll look for these objects in the actor or
     *   actor state for the actor to whom we're talking.  We'll show a list
     *   of each currently active suggestion we find.  This gives the player
     *   some guidance of what to talk about.  For example:
     *
     *   >talk to bob
     *.  "Excuse me," you say.
     *
     *   Bob looks up from his newspaper.  "Yes?  Oh, you again."
     *
     *   (You'd like to ask him about the black book, the candle, and the
     *   bell, and tell him about the crypt.)
     *
     *   Topic suggestions are entirely optional.  Some authors don't like the
     *   idea, since they think it's too much like a menu system, and just
     *   gives away the solution to the game.  If you don't want to have
     *   anything to do with topic suggestions, we won't force you - simply
     *   don't define any SuggestedTopic objects, and the library will never
     *   offer suggestions and will even disable the TOPICS command.
     *
     *   If you do want to use topic suggestions, the easiest way to use this
     *   class is to combine it using multiple inheritance with a TopicEntry
     *   object.  You just have to add SuggestedTopic to the superclass list
     *   for your topic entry object, and give the suggested topic a name
     *   string (using a property and format defined by the language-specific
     *   library) to display in suggestions lists.  Doing this, the suggestion
     *   will automatically be enabled whenever the topic entry is available,
     *   and will automatically be removed from the suggestions when the topic
     *   is invoked in conversation (in other words, we'll only suggest asking
     *   about the topic until it's been asked about once).
     *
     *   Topic suggestions can be associated with an actor or an actor state;
     *   these are topics that a given character would like to talk to the
     *   associated actor about.  The association is a bit tricky: suggested
     *   topic objects are stored with the actor being *talked to*.  For
     *   example, if we want to suggest topics that the player character might
     *   want to ASK BILL ABOUT, we store these suggestions with *Bill*.  We
     *   do NOT store the suggestions with the player character.  This might
     *   seem backwards at first glance, since fundamentally the suggestions
     *   belong in the player character's "brain" - they are, after all,
     *   things the player character wants to talk about.  In practice,
     *   though, there are two things that make it easier to keep the
     *   information with the character being asked.  First, in most games,
     *   there's just one player character, so one of the two actors in each
     *   association will always be the player character; by storing the
     *   objects with the NPC, we can just let the PC be assumed as the other
     *   actor as a default, saving us some typing that would be necessary if
     *   we had to specify each object in the other direction.  Second, we
     *   keep the *response* objects associated with the character being asked
     *   - that association is intuitive, at least.  The thing is, we can
     *   usually combine the suggestion and response into a single object,
     *   saving another bunch of typing; if we didn't keep the suggestion with
     *   the character being asked, we couldn't combine the suggestions and
     *   responses this way, since they'd have to be associated with different
     *   actors.
     */
    class SuggestedTopic: object
        /*
         *   The name of the suggestion.  The rules for setting this vary by
         *   language; in the English version, we'll display the fullName when
         *   we show a stand-alone item, and the groupName when we appear in a
         *   list group (such as a group of ASK ABOUT or TELL ABOUT
         *   suggestions).
         *
         *   In English, the fullName should be suitable for use after
         *   'could': "You could <fullName>, <fullName>, or <fullName>".
         *
         *   In English, the phrasing where the 'name' property is used
         *   depends on the specific subclass, but it should usually be a
         *   qualified noun phrase (that is, it should include a qualifier
         *   such as "a" or "the" or a possessive).  For ASK and TELL, for
         *   example, the 'name' should be suitable for use after ABOUT: "You
         *   could ask him about <the lighthouse>, <Bob's black book>, or <the
         *   weather>."
         *
         *   By default, we'll walk up our 'location' tree looking for another
         *   suggested topic; if we find one, we'll use its corresponding name
         *   values.
         */
        fullName = (fromEnclosingSuggestedTopic(&fullName, ''))
        name = (fromEnclosingSuggestedTopic(&name, ''))

        /*
         *   Our associated topic.  In most cases, this will be initialized
         *   automatically: if this suggested topic object is also a
         *   TopicEntry object (using multiple inheritance), we'll set this
         *   during start-up to 'self', or if our location is a TopicEntry,
         *   we'll set this to our location.  This only needs to be
         *   initialized manually if neither of those conditions is true.
         */
        associatedTopic = nil

        /*
         *   Set the location to the actor to ask or tell about this topic.
         *   This is the target of the ASK ABOUT or TELL ABOUT command, NOT
         *   the actor who's doing the asking.  This can also be set to a
         *   TopicEntry object, in which case we'll be associated with the
         *   actor with which the topic entry is associated, and we'll also
         *   automatically tie the topic entry to this suggestion.
         *
         *   Because we're using the location property, you can use the '+'
         *   notation to add a suggested topic to the target actor, state
         *   objects, or topic entry.
         */
        location = nil

        /*
         *   The actor who *wants* to ask or tell about this topic.  Our
         *   location property gives the actor to be asked or told, because
         *   we're associated with the target actor - the same actor who has
         *   the TopicEntry information for the topic.  This property, in
         *   contrast, gives the actor who's doing the asking.
         *
         *   By default, we return the player character; in most cases, you
         *   won't have to override this.  In most games, only the player
         *   character uses the suggested topic mechanism, because there's no
         *   reason to suggest topics for NPC's - they're just automata, after
         *   all, so if we want them to ask something, we can just program
         *   them to ask it directly.  Also, most games have only one player
         *   character.  Games that meet these criteria won't ever have to
         *   override this.  If you do have multiple player characters, you'll
         *   probably want to override this for each suggested topic to
         *   indicate which character wants to ask about the topic, as the
         *   different player characters might have different things they'd
         *   want to talk about.
         */
        suggestTo = (gPlayerChar)

        /* the ListGroup with which we're to list this suggestion */
        suggestionGroup = []

        /* find the nearest enclosing SuggestedTopic parent */
        findEnclosingSuggestedTopic()
        {
            /* walk up our location list */
            for (local loc = location ; loc != nil ; loc = loc.location)
            {
                /* if this is a suggested topic, it's what we're looking for */
                if (loc.ofKind(SuggestedTopic))
                    return loc;
            }

            /* didn't find anything */
            return nil;
        }

        /* find the outermost enclosing SuggestedTopic parent */
        findOuterSuggestedTopic()
        {
            local outer;

            /* walk up our location list */
            for (local loc = self, outer = nil ; loc != nil ; loc = loc.location)
            {
                /* if this is a suggested topic, it's the outermost so far */
                if (loc.ofKind(SuggestedTopic))
                    outer = loc;
            }

            /* return the outermost suggested topic we found */
            return outer;
        }

        /*
         *   get a property from the nearest enclosing SuggestedTopic, or
         *   return the given default value if there is no enclosing
         *   SuggestedTopic
         */
        fromEnclosingSuggestedTopic(prop, defaultVal)
        {
            /* look for the nearest enclosing suggested topic */
            local enc = findEnclosingSuggestedTopic();

            /*
             *   return the desired property from the enclosing suggested
             *   topic object if we found one, or the default if there is no
             *   enclosing object
             */
            return (enc != nil ? enc.(prop) : defaultVal);
        }

        /*
         *   Should we suggest this topic to the given actor?  We'll return
         *   true if the actor is the same actor for which this suggestion is
         *   intended, and the associated topic entry is currently active, and
         *   we haven't already satisfied our curiosity about the topic.
         */
        isSuggestionActive(actor, scopeList)
        {
            /*
             *   Check to see if this is our target actor; that the associated
             *   topic itself is active; that our curiosity hasn't already been
             *   satisfied; and that it's at least possible to match the
             *   associated topic right now.  If all of these conditions are
             *   met, we can make this suggestion.
             */
            return (actor == suggestTo
                    && associatedTopicIsActive()
                    && associatedTopicCanMatch(actor, scopeList)
                    && !curiositySatisfied);
        }

        /*
         *   The number of times to suggest asking about our topic.  When
         *   we've asked about our associated topic this many times, we'll
         *   have satisfied our curiosity.  In most cases, we'll only want to
         *   suggest a topic until it's asked about once, since most topics
         *   only have a single meaningful response, so we'll use 1 as the
         *   default.  This should be overridden in cases where a topic will
         *   reveal more information when asked several times.  If this is
         *   nil, it means that there's no limit to the number of times to
         *   suggest asking about this.
         */
        timesToSuggest = 1

        /*
         *   Have we satisfied our curiosity about this topic?  Returns true
         *   if so, nil if not.  We'll never suggest a topic when this returns
         *   true, because this means that the player no longer feels the need
         *   to ask about the topic.
         */
        curiositySatisfied = (timesToSuggest != nil
                              && associatedTopicTalkCount() >= timesToSuggest)

        /* initialize - this is called automatically during pre-initialization */
        initializeSuggestedTopic()
        {
            /* if we have a location, link up with our location */
            if (location != nil)
                location.addSuggestedTopic(self);

            /*
             *   if we're also a TopicEntry (using multiple inheritance), then
             *   we are our own associated topic object
             */
            if (ofKind(TopicEntry))
                associatedTopic = self;
        }

        /*
         *   Methods that rely on the associated topic.  We isolate these in a
         *   few methods here so that the rest of class doesn't depend on the
         *   exact nature of our topic association.  In particular, this allows
         *   for subclasses that don't have an associated topic at all, or that
         *   have multiple associated topics.  Subclasses with specialized
         *   topic relationships can simply override these methods to define
         *   these methods appropriately.
         */

        /* is the associated topic active? */
        associatedTopicIsActive() { return associatedTopic.checkIsActive(); }

        /* get the number of previous invocations of the associated topic */
        associatedTopicTalkCount() { return associatedTopic.talkCount; }

        /* is it possible to match the associated topic? */
        associatedTopicCanMatch(actor, scopeList)
            { return associatedTopic.isMatchPossible(actor, scopeList); }

        /*
         *   Note that we're being shown in a topic inventory listing.  By
         *   default, we don't do anything here, but subclasses can use this to
         *   do any extra work they want to do on being listed.
         */
        noteSuggestion() { }
    ;

    /*
     *   A suggested topic that applies to an entire AltTopic group.
     *
     *   Normally, a suggestion is tied to an individual TopicEntry.  This
     *   means that when a topic has several AltTopic alternatives, each
     *   AltTopic can be its own separate, independent suggestion.  A
     *   particular alternative can be a suggestion or not, independently of
     *   the other alternatives for the same TopicEntry.  Since each AltTopic
     *   is a separate suggestion, asking about one of the alternatives won't
     *   have any effect on the "curiosity" about the other alternatives - in
     *   other words, the other alternatives will be separately suggested when
     *   they become active.
     *
     *   In many cases, it's better for an entire set of alternatives to be
     *   treated as a single suggested topic.  That is, we want to suggest the
     *   topic when ANY of the alternatives is active, and asking about any one
     *   of the alternatives will satisfy the PC's curiosity for ALL of the
     *   alternatives.  This sort of arrangement is usually better for cases
     *   where the conditions that trigger the different alternatives aren't
     *   things that ought to make the PC think to ask the same question again.
     *
     *   Use this class by associating it with the *root* TopicEntry of the
     *   group of alternatives.  You can do this most simply by mixing this
     *   class into the superclass list of the root TopicEntry:
     *
     *.  + AskTellTopic, SuggestedTopicTree, SuggestedAskTopic
     *.     // ...
     *.  ;
     *   ++ AltTopic ... ;
     *   ++ AltTopic ... ;
     *
     *   This makes the entire group of AltTopics part of the same suggestion.
     *   Note that you must *also* include SuggestedAsk, SuggestedTellTopic, or
     *   one of the other specialized types among the superclass, to indicate
     *   which kind of suggestion this is.
     */
    class SuggestedTopicTree: SuggestedTopic
        /* is the associated topic active? */
        associatedTopicIsActive()
        {
            /* the topic is active if anything in the AltTopic group is active */
            return associatedTopic.anyAltIsActive;
        }

        /* get the number of previous invocations of the associated topic */
        associatedTopicTalkCount()
        {
            /* return the number of invocations of any alternative */
            return associatedTopic.altTalkCount;
        }
    ;

    /*
     *   A suggested ASK ABOUT topic.  We'll list ASK ABOUT topics together in
     *   a subgroup ("you'd like to ask him about the book, the candle, and
     *   the bell...").
     */
    class SuggestedAskTopic: SuggestedTopic
        suggestionGroup = [suggestionAskGroup]
    ;

    /*
     *   A suggested TELL ABOUT topic.  We'll list TELL ABOUT topics together
     *   in a subgroup.
     */
    class SuggestedTellTopic: SuggestedTopic
        suggestionGroup = [suggestionTellGroup]
    ;

    /*
     *   A suggested ASK FOR topic.  We'll list ASK FOR topics together as a
     *   group.
     */
    class SuggestedAskForTopic: SuggestedTopic
        suggestionGroup = [suggestionAskForGroup]
    ;

    /*
     *   A suggested GIVE TO topic.
     */
    class SuggestedGiveTopic: SuggestedTopic
        suggestionGroup = [suggestionGiveGroup]
    ;

    /*
     *   A suggested SHOW TO topic.
     */
    class SuggestedShowTopic: SuggestedTopic
        suggestionGroup = [suggestionShowGroup]
    ;

    /*
     *   A suggested YES/NO topic
     */
    class SuggestedYesTopic: SuggestedTopic
        suggestionGroup = [suggestionYesNoGroup]
    ;
    class SuggestedNoTopic: SuggestedTopic
        suggestionGroup = [suggestionYesNoGroup]
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A conversation node.  Conversation nodes are supplemental topic
     *   databases that represent a point in time in a conversation - a
     *   particular context that arises from what came immediately before in
     *   the conversation.  A conversation node is used to set up a group of
     *   special responses that make sense only in a momentary context within a
     *   conversation.
     *
     *   A ConvNode object must be nested (via the 'location' property) within
     *   an actor or an ActorState.  This is how we associate the ConvNode with
     *   its actor.  Note that putting a ConvNode inside an ActorState doesn't
     *   do anything different from putting the node directly inside the
     *   ActorState's actor - we allow it only for convenience, to allow
     *   greater flexibility arranging source code.
     */
    class ConvNode: ActorTopicDatabase
        /*
         *   Every ConvNode must have a name property.  This is a string
         *   identifying the object.  Use this name string instead of a regular
         *   object name (so ConvNode instances can essentially always be
         *   anonymous, as far as the compiler is concerned).  This string is
         *   used to find the ConvNode in the master ConvNode database
         *   maintained in the conversationManager object.
         *
         *   A ConvNode name should be unique with respect to all other
         *   ConvNode objects - no two ConvNode objects should have the same
         *   name string.  Other than this, the name strings are arbitrary.
         *   (However, they shouldn't contain any '>' characters, because this
         *   would prevent them from being used in <.convnode> tags, which is
         *   the main place ConvNode's are usually used.)
         */
        name = ''

        /*
         *   Is this node "sticky"?  If so, we'll stick to this node if we
         *   show a response that doesn't set a new node.  By default, we're
         *   not sticky, so if we show a response that doesn't set a new node
         *   and doesn't use a <.convstay> tag, we'll simply forget the node
         *   and set the actor to no current ConvNode.
         *
         *   Sticky nodes are useful when you want the actor to stay
         *   on-subject even when the player digresses to talk about other
         *   things.  This is useful when the actor has a particular thread
         *   they want to drive the conversation along.
         */
        isSticky = nil

        /*
         *   Show our NPC-initiated greeting.  This is invoked when our actor's
         *   initiateConversation() method is called to cause our actor to
         *   initiate a conversation with the player character.  This method
         *   should show what our actor says to initiate the conversation.  By
         *   default, we'll invoke our npcGreetingList's script, if the
         *   property is non-nil.
         *
         *   A greeting should always be defined for any ConvNode that's used
         *   in an initiateConversation() call.
         *
         *   To define a greeting when defining a ConvNode, you can override
         *   this method with a simple double-quoted string message, or you can
         *   define an npcGreetingList property as an EventList of some kind.
         */
        npcGreetingMsg()
        {
            /* if we have an npcGreetingList property, invoke the script */
            if (npcGreetingList != nil)
                npcGreetingList.doScript();
        }

        /* an optional EventList containing our NPC-initiated greetings */
        npcGreetingList = nil

        /*
         *   Our NPC-initiated conversation continuation message.  This is
         *   invoked on each turn (during the NPC's takeTurn() daemon
         *   processing) that we're in this conversation node and the player
         *   character doesn't do anything conversational.  This allows the NPC
         *   to carry on the conversation of its own volition.  Define this as
         *   a double-quoted string if you want the NPC to say something to
         *   continue the conversation.
         */
        npcContinueMsg = nil

        /*
         *   An optional EventList containing NPC-initiated continuation
         *   messages.  You can define an EventList here instead of defining
         *   npcContinueMsg, if you want more than one continuation message.
         */
        npcContinueList = nil

        /*
         *   Flag: automatically show a topic inventory on activating this
         *   conversation node.  Some conversation nodes have sufficiently
         *   obscure entries that it's desirable to show a topic inventory
         *   automatically when the node becomes active.
         *
         *   By default, we automatically show a topic inventory if the node
         *   contains an active SpecialTopic entry.  Since special topics are
         *   inherently obscure, in that they use non-standard commands, we
         *   always want to show topics when one of these becomes active.
         */
        autoShowTopics()
        {
            /* if we have an active special topic, show the topic inventory */
            return (specialTopics != nil
                    && specialTopics.indexWhich({x: x.checkIsActive()}) != nil);
        }

        /* our NPC is initiating a conversation starting with this node */
        npcInitiateConversation()
        {
            local actor = getActor();

            /* tell the conversation manager we're the actor who's talking */
            conversationManager.beginResponse(actor);

            /* note that we're in conversation with the player character now */
            getActor().noteConversation(gPlayerChar);

            /* show our NPC greeting */
            npcGreetingMsg();

            /* look for an ActorHelloTopic within the node */
            handleTopic(gPlayerChar, actorHelloTopicObj, helloConvType, nil);

            /* end the response, staying in the current ConvNode by default */
            conversationManager.finishResponse(actor, self);
        }

        /*
         *   Continue the conversation of the NPC's own volition.  Returns
         *   true if we displayed anything, nil if not.
         */
        npcContinueConversation()
        {
            local actor = getActor();
            local disp;

            /* tell the conversation manager we're starting a response */
            conversationManager.beginResponse(actor);

            /* show our text, watching to see if we generate any output */
            disp = outputManager.curOutputStream.watchForOutput(function()
            {
                /*
                 *   if we have a continuation list, invoke it; otherwise if we
                 *   have a continuation message, show it; otherwise, just
                 *   return nil to let the caller know we have nothing to add
                 */
                if (npcContinueList != nil)
                    npcContinueList.doScript();
                else
                    npcContinueMsg;
            });

            /* end the response, staying in the current ConvNode by default */
            conversationManager.finishResponse(actor, self);

            /*
             *   if we actually said anything, note that we're in conversation
             *   with the player character
             */
            if (disp)
                getActor().noteConversation(gPlayerChar);

            /* return the display indication */
            return disp;
        }

        /* our actor is our location, or our location's actor */
        getActor()
        {
            /* if our location is an actor state, return the state's actor */
            if (location.ofKind(ActorState))
                return location.getActor();

            /* otherwise, our location must be our actor */
            return location;
        }

        /* our actor is the "owner" of our topics */
        getTopicOwner() { return getActor(); }

        /*
         *   Handle a conversation topic.  The actor state object will call
         *   this to give the ConvNode the first crack at handling a
         *   conversation command.  We'll return true if we handle the command,
         *   nil if not.  Our default handling is to look up the topic in the
         *   given database list property, and handle it through the TopicEntry
         *   we find there, if any.
         */
        handleConversation(otherActor, topic, convType, path)
        {
            /* try handling it, returning the handled/not-handled result */
            return handleTopic(otherActor, topic, convType, path);
        }

        /*
         *   Can we end the conversation?  If so, return true; our caller will
         *   invoke our endConversation() to let us know that the conversation
         *   is over.
         *
         *   To prevent the conversation from ending, simply return nil.
         *
         *   In most cases, you won't want to force the conversation to keep
         *   going without any comment.  Instead, you'll want to display some
         *   message to let the player know what's going on - something like
         *   "Hey! We're not through here!"  If you do display a message, then
         *   rather than returning nil, return the special value blockEndConv -
         *   this tells the caller that the actor said something, so the caller
         *   will call noteConvAction() to prevent further generated
         *   conversation output on this same turn.
         *
         *   'reason' gives the reason the conversation is ending, as an
         *   endConvXxx enum code.
         */
        canEndConversation(actor, reason) { return true; }

        /*
         *   Receive notification that our actor is ending a stateful
         *   conversation.  This is called before the normal
         *   InConversationState disengagement operations.  'reason' is one of
         *   the endConvXxx enums, indicating why the conversation is ending.
         *
         *   Instances can override this for special behavior on terminating a
         *   conversation.  For example, an actor who just asked a question
         *   could say something to indicate that the other actor is being
         *   rude.  By default, we do nothing.
         *
         *   Note that there's no way to block the ending of the conversation
         *   here.  If you want to prevent the conversation from ending, use
         *   canEndConversation() instead.
         */
        endConversation(actor, reason) { }

        /*
         *   Process a special command.  Check the given command line string
         *   against all of our topics, and see if we have a match to any topic
         *   that takes a special command syntax.  If we find a matching
         *   special topic, we'll note the match, and turn the command into our
         *   secret internal pseudo-command "XSPCLTOPIC".  That command will
         *   then go through the parser, which will recognize it and process it
         *   using the normal conversational mechanisms, which will find the
         *   SpecialTopic we noted earlier (in this method) and display its
         *   response.
         *
         *   'str' is the original input string, exactly as entered by the
         *   player, and 'procStr' is the "processed" version of the input
         *   string.  The nature of the processing varies by language, but
         *   generally this involves things like removing punctuation marks and
         *   any "noise words" that don't usually change the meaning of the
         *   input, at least for the purposes of matching a special topic.
         */
        processSpecialCmd(str, procStr)
        {
            local match;
            local cnt;

            /* we don't have an active special topic yet */
            activeSpecialTopic = nil;

            /*
             *   if we have no special topics, there's definitely no special
             *   processing we need to do
             */
            if (specialTopics == nil)
                return str;

            /* scan our special topics for a match */
            cnt = 0;
            foreach (local cur in specialTopics)
            {
                /* if this one is active, and it matches the string, note it */
                if (cur.checkIsActive() && cur.matchPreParse(str, procStr))
                {
                    /* remember it as the last match */
                    match = cur;

                    /* count the match */
                    ++cnt;
                }
            }

            /*
             *   If we found exactly one match, then activate it.  If we found
             *   zero or more than one, ignore any special topics and proceed
             *   on the assumption that this is a normal command.  (We ignore
             *   ambiguous matches because this probably means that the entire
             *   command is some very common word that happens to be acceptable
             *   as a keyword in one or more of our matches.  In these cases,
             *   the common word was probably meant as an ordinary command,
             *   since the player would likely have been more specific if a
             *   special topic were really desired.)
             */
            if (cnt == 1)
            {
                /*
                 *   remember the active SpecialTopic - we'll use this memory
                 *   to find it again when we get through the full command
                 *   processing
                 */
                activeSpecialTopic = match;

                /*
                 *   Change the command to our special internal pseudo-command
                 *   that triggers the active special topic.  Include the
                 *   original string as a literal phrase, enclosed in double
                 *   quotes and specially coded to ensure that the tokenizer
                 *   doesn't become confused by any embedded quotes.
                 */
                return 'xspcltopic "' + SpecialTopicAction.encodeOrig(str) + '"';
            }
            else
            {
                /* proceed, treating the original input as an ordinary command */
                return str;
            }
        }

        patWhitespace = static new RexPattern('<space>+')
        patDelim = static new RexPattern('<punct|space>')

        /*
         *   Handle an XSPCLTOPIC command from the given actor.  This is part
         *   two of the two-phase processing of SpecialTopic matches.  Our
         *   pre-parser checks each SpecialTopic's custom syntax for a match
         *   to the player's text input, and if it finds a match, it sets our
         *   activeSpecialTopic property to the matching SpecialTopic, and
         *   changes the user's command to XSPCLTOPIC for processing by the
         *   regular parser.  The regular parser sees the XSPCLTOPIC command,
         *   which is a valid verb that calls the issuing actor's
         *   saySpecialTopic() routine, which in turn forwards the request to
         *   the issuing actor's interlocutor's current conversation node -
         *   which is to say, 'self'.  We complete the two-step procedure by
         *   going back to the active special topic object that we previously
         *   noted and showing its response.
         */
        saySpecialTopic(fromActor)
        {
            /* make sure we have an active special topic object */
            if (activeSpecialTopic != nil)
            {
                local actor = getTopicOwner();

                /* tell the conversation manager we're starting a response */
                conversationManager.beginResponse(actor);

                /* let the SpecialTopic handle the response */
                activeSpecialTopic.handleTopic(fromActor, nil);

                /*
                 *   Tell the conversation manager we're done.  By default, we
                 *   want to leave the conversation tree entirely, so set the
                 *   new default node to 'nil'.
                 */
                conversationManager.finishResponse(actor, nil);

                /* that wraps things up for the active special topic */
                activeSpecialTopic = nil;
            }
            else
            {
                /*
                 *   There is no active special topic, so the player must have
                 *   typed in the XSPCLTOPIC command explicitly - if we got
                 *   here through the normal two-step procedure then this
                 *   property would not be nil.  Politely decline the command,
                 *   since it's not for the player's direct use.
                 */
                gLibMessages.commandNotPresent;
            }
        }

        /*
         *   The active special topic.  This is the SpecialTopic object that
         *   we matched during pre-parsing, so it's the one whose response we
         *   wish to show while processing the command we pre-parsed.
         */
        activeSpecialTopic = nil

        /*
         *   Note that we're becoming active, with a reason code.  Our actor
         *   will call this method when we're becoming active, as long as we
         *   weren't already active.
         *
         *   'reason' is a string giving a reason code for why we're being
         *   called.  For calls from the library, this will be one of these
         *   codes:
         *
         *    'convnode' - processing a <.convnode> tag
         *
         *    'convend' - processing a <.convend> tag
         *
         *    'initiateConversation' - a call to Actor.initiateConversation()
         *
         *    'endConversation' - a call to Actor.endConversation()
         *
         *   The reason code is provided so that the node can adapt its action
         *   for different trigger conditions, if desired.  By default, we
         *   ignore the reason code and just call the basic noteActive()
         *   method.
         */
        noteActiveReason(reason)
        {
            noteActive();
        }

        /*
         *   Note that we're becoming active, with a reason code.  Our actor
         *   will call this method when we're becoming active, as long as we
         *   weren't already active.
         *
         *   Note that if you want to adapt the method's behavior according to
         *   why the node was activated, you can override noteActiveReason()
         *   instead of this method.
         */
        noteActive()
        {
            /* if desired, schedule a topic inventory whenever we're activated */
            if (autoShowTopics())
                conversationManager.scheduleTopicInventory();
        }

        /*
         *   Note that we're leaving this conversation node.  This doesn't do
         *   anything by default, but individual instances might find the
         *   notification useful for triggering side effects.
         */
        noteLeaving() { }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Pre-parser for special ConvNode-specific commands.  When the player
     *   character is talking to another character, and the NPC's current
     *   ConvNode includes topics with their own commands, we'll check the
     *   player's input to see if it matches any of these topics.
     */
    specialTopicPreParser: StringPreParser
        doParsing(str, which)
        {
            local actor;
            local node;

            /*
             *   don't handle this on requests for missing literals - these
             *   responses are always interpreted as literal text, so there's
             *   no way this could be a special ConvNode command
             */
            if (which == rmcAskLiteral)
                return str;

            /*
             *   if the player character isn't currently in conversation, or
             *   the actor with whom the player character is conversing doesn't
             *   have a current conversation node, there's nothing to do
             */
            if ((actor = gPlayerChar.getCurrentInterlocutor()) == nil
                || (node = actor.curConvNode) == nil)
                return str;

            /* ask the conversation node to process the string */
            return node.processSpecialCmd(str, processInputStr(str));
        }

        /*
         *   Process the input string, as desired, for special-topic parsing.
         *   This method is for the language module's use; by default, we do
         *   nothing.
         *
         *   Language modules should override this to remove punctuation marks
         *   and to do any other language-dependent processing to make the
         *   string parsable.
         */
        processInputStr(str) { return str; }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A conversational action type descriptor.  This descriptor is used in
     *   handleConversation() in Actor and ActorState to describe the type of
     *   conversational action we're performing.  The type descriptor object
     *   encapsulates a set of information that tells us how to handle the
     *   action.
     */
    class ConvType: object
        /*
         *   The unknown interlocutor message property.  This is used when we
         *   try this conversational action without knowing whom we're talking
         *   to.  For example, if we just say HELLO, and there's no one around
         *   to talk to, we'll use this as the default response.  This can be a
         *   library message property, or simply a single-quoted string to
         *   display.
         */
        unknownMsg = nil

        /*
         *   The TopicDatabase topic-list property.  This is the property of
         *   the TopicDatabase object that we evaluate to get this list of
         *   topic entries to search for a match to the topic.
         */
        topicListProp = nil

        /* the default response property for this action */
        defaultResponseProp = nil

        /*
         *   Call the default response property on the given topic database.
         *   This invokes the property given by defaultResponseProp().  We have
         *   both the property and the method to call the property because this
         *   allows us to test for the existence of the property and to call it
         *   with the appropriate argument list.
         */
        defaultResponse(db, otherActor, topic) { }

        /*
         *   Perform any special follow-up action for this type of
         *   conversational action.
         */
        afterResponse(actor, otherActor) { }
    ;

    helloConvType: ConvType
        unknownMsg = &sayHelloMsg
        topicListProp = &miscTopics
        defaultResponseProp = &defaultGreetingResponse
        defaultResponse(db, other, topic)
            { db.defaultGreetingResponse(other); }

        /* after an explicit HELLO, show any suggested topics */
        afterResponse(actor, otherActor)
        {
            /* show or schedule a topic inventory, as appropriate */
            conversationManager.showOrScheduleTopicInventory(actor, otherActor);
        }
    ;

    byeConvType: ConvType
        unknownMsg = &sayGoodbyeMsg
        topicListProp = &miscTopics
        defaultResponseProp = &defaultGoodbyeResponse
        defaultResponse(db, other, topic)
            { db.defaultGoodbyeResponse(other); }
    ;

    yesConvType: ConvType
        unknownMsg = &sayYesMsg
        topicListProp = &miscTopics
        defaultResponseProp = &defaultYesResponse
        defaultResponse(db, other, topic)
            { db.defaultYesResponse(other); }
    ;

    noConvType: ConvType
        unknownMsg = &sayNoMsg
        topicListProp = &miscTopics
        defaultResponseProp = &defaultNoResponse
        defaultResponse(db, other, topic)
            { db.defaultNoResponse(other); }
    ;

    askAboutConvType: ConvType
        topicListProp = &askTopics
        defaultResponseProp = &defaultAskResponse
        defaultResponse(db, other, topic)
            { db.defaultAskResponse(other, topic); }
    ;

    askForConvType: ConvType
        topicListProp = &askForTopics
        defaultResponseProp = &defaultAskForResponse
        defaultResponse(db, other, topic)
            { db.defaultAskForResponse(other, topic); }
    ;

    tellAboutConvType: ConvType
        topicListProp = &tellTopics
        defaultResponseProp = &defaultTellResponse
        defaultResponse(db, other, topic)
            { db.defaultTellResponse(other, topic); }
    ;

    giveConvType: ConvType
        topicListProp = &giveTopics
        defaultResponseProp = &defaultGiveResponse
        defaultResponse(db, other, topic)
            { db.defaultGiveResponse(other, topic); }
    ;

    showConvType: ConvType
        topicListProp = &showTopics
        defaultResponseProp = &defaultShowResponse
        defaultResponse(db, other, topic)
            { db.defaultShowResponse(other, topic); }
    ;

    commandConvType: ConvType
        topicListProp = &commandTopics
        defaultResponseProp = &defaultCommandResponse
        defaultResponse(db, other, topic)
            { db.defaultCommandResponse(other, topic); }
    ;

    /*
     *   This type is for NPC-initiated conversations.  It's not a normal
     *   conversational action, since it doesn't involve handling a player
     *   command, but is usually instead triggered by an agenda item,
     *   takeTurn(), or other background activity.
     */
    initiateConvType: ConvType
        topicListProp = &initiateTopics
    ;

    /*
     *   CONSULT ABOUT isn't a true conversational action, since it's applied
     *   to inanimate objects (such as books); but it's handled through the
     *   conversation system, so it needs a conversation type object
     */
    consultConvType: ConvType
        topicListProp = &consultTopics
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A topic database entry.  Actors and actor state objects store topic
     *   databases; a topic database is essentially a set of these entries.
     *
     *   A TopicEntry can go directly inside an Actor, in which case it's part
     *   of the actor's global set of topics; or, it can go inside an
     *   ActorState, in which case it's part of the state's database and is
     *   only active when the state is active; or, it can go inside a
     *   TopicGroup, which is a set of topics with a common controlling
     *   condition; or, it can go inside a ConvNode, in which case it's in
     *   effect only when the conversation node is active.
     *
     *   Each entry is a relationship between a topic, which is something that
     *   can come up in an ASK or TELL action, and a handling for the topic.
     *   In addition, each entry determines what kind or kinds of actions it
     *   responds to.
     *
     *   Note that TopicEntry objects are *not* simulation objects.  Rather,
     *   these are abstract objects; they can be associated with simulation
     *   objects via the matching mechanism, but these are separate from the
     *   actual simulation objects.  The reason for this separation is that a
     *   given simulation object might have many different response - the
     *   response could vary according to who's being asked the question, who's
     *   asking, and what else is happening in the game.
     *
     *   An entry decides for itself if it matches a topic.  By default, an
     *   entry can match based on either a simulation object, which we'll match
     *   to anything in the topic's "in scope" or "likely" match lists, or
     *   based on a regular expression string, which we'll match to the actual
     *   topic text entered in the player's command.
     *
     *   An entry can decide how strongly it matches a topic.  The database
     *   will choose the strongest match when multiple entries match the same
     *   topic.  The strength of the match is given by a numeric score; the
     *   higher the score, the stronger the match.  The match strength makes it
     *   easy to specify a hierarchy of topics from specific to general, so
     *   that we provide general responses to general topic areas, but can
     *   still respond to particular topics areas more specifically.  For
     *   example, we might want to provide a specific match to the FROBNOZ
     *   SPELL object, talking about that particular magic spell, but provide a
     *   generic '.* spell' pattern to response to questions about any old
     *   spell.  We'd give the generic pattern a lower score, so that the
     *   specific FROBNOZ SPELL response would win when it matches, but we'd
     *   fall back on the generic pattern in other cases.
     */
    class TopicEntry: object
        /*
         *   My matching simulation object or objects.  This can be either a
         *   single object or a list of objects.
         */
        matchObj = nil

        /*
         *   Is this topic active?  This can be used to control how an actor
         *   can respond without have to worry about adding and removing topics
         *   manually at key events, or storing the topics in state objects.
         *   Sometimes, it's easier to just put a topic entry in the actor's
         *   database from the start, and test some condition dynamically when
         *   the topic is actually queried.  To do this, override this method
         *   to test the condition that determines when the topic entry should
         *   become active.  We'll never show the topic's response when
         *   isActive returns nil.  By default, we simply return true to
         *   indicate that the topic entry is active.
         */
        isActive = true

        /*
         *   Flag: we are a "conversational" topic.  This is true by default.
         *   When this is set to nil, a ConversationReadyState will NOT show
         *   its greeting and will not enter its InConversationState to show
         *   this topic entry's response.
         *
         *   This should be set to nil when the topic entry's response is
         *   non-conversational, in which case a greeting would be
         *   undesirable.  This is appropriate for responses like "You don't
         *   think he'd want to talk about that", where the response indicates
         *   that the player character didn't even ask a question (or
         *   whatever).
         */
        isConversational = true

        /*
         *   Do we imply a greeting?  By default, all conversational topics
         *   imply a greeting.  We separate this out so that the implied
         *   greeting can be controlled independently of whether or not we're
         *   actually conversational, if desired.
         */
        impliesGreeting = (isConversational)

        /*
         *   Get the actor associated with the topic, if any.  By default,
         *   we'll return our enclosing database's topic owner, if it's an
         *   actor - in almost all cases, if there's any actor associated with
         *   a topic, it's simply the owner of the database containing the
         *   topic.
         */
        getActor()
        {
            local owner;

            /*
             *   if we have an owner, and it's an actor, then it's our
             *   associated actor; otherwise, we don't have any associated
             *   actor
             */
            if ((owner = location.getTopicOwner()) != nil && owner.ofKind(Actor))
                return owner;
            else
                return nil;
        }

        /*
         *   Determine if this topic is active.  This checks the isActive
         *   property, and also takes into account our relationship to
         *   alternative entries for the topic.  Generally, you should *define*
         *   (override) isActive, and *call* this method.
         */
        checkIsActive()
        {
            /*
             *   if our isActive property indicates we're not active, we're
             *   definitely not active, so there's no need to check for an
             *   overriding alternative
             */
            if (!isActive)
                return nil;

            /* if we have an active nested alternative, it overrides us */
            if (altTopicList.indexWhich({x: x.isActive}) != nil)
                return nil;

            /* ask our container if its topics are active */
            return location.topicGroupActive();
        }

        /*
         *   Check to see if any alternative in the alternative group is
         *   active.  This returns true if we're active or if any of our nested
         *   AltTopics is active.
         */
        anyAltIsActive()
        {
            /*
             *   if all topics within our container are inactive, then there's
             *   definitely no active alternative
             */
            if (!location.topicGroupActive())
                return nil;

            /*
             *   if we're active, or any of our nested AltTopics is active, our
             *   alternative group is active
             */
            if (isActive || altTopicList.indexWhich({x: x.isActive}) != nil)
                return true;

            /* we didn't find any active alternatives in the entire group */
            return nil;
        }

        /*
         *   Adjust my score value for any hierarchical adjustments.  We'll add
         *   the score adjustment for each enclosing object.
         */
        adjustScore(score)
        {
            /* the score is nil, it means there's no match, so don't adjust it */
            if (score == nil)
                return score;

            /* add in the cumulative adjustment from my containers */
            return score + location.topicGroupScoreAdjustment;
        }

        /*
         *   Check to see if we want to defer to the given topic from an
         *   inferior topic database.  By default, we never defer to a topic
         *   from an inferior database: we choose a matching topic from the top
         *   database in the hierarchy where we find a match.
         *
         *   The database hierarchy, for most purposes, starts with the
         *   ConvNode at the highest level, then the ActorState, then the
         *   Actor.  We search those databases, in that order, and we take the
         *   first match we find.  By default, if there's another match in a
         *   lower-level database, it doesn't matter what its matchScore is: we
         *   always pick the one from the highest-level database where we find
         *   a match.  You can override this method to change this behavior.
         *
         *   We don't actually define this method here, because the presence of
         *   the method is significant.  If the method isn't defined at all, we
         *   won't bother looking for a possible deferral, saving the trouble
         *   of searching the other databases in the hierarchy.
         */
        // deferToEntry(other) { return nil; }

        /*
         *   Our match strength score.  By default, we'll use a score of 100,
         *   which is just an arbitrary base score.
         */
        matchScore = 100

        /*
         *   The set of database lists we're part of.  This is a list of
         *   property pointers, giving the TopicDatabase properties of the
         *   lists we participate in.
         */
        includeInList = []

        /*
         *   Our response.  This is displayed when we're the topic entry
         *   selected to handle an ASK or TELL.  Each topic entry must override
         *   this to show our response text (or, alternatively, an entry can
         *   override handleTopic so that it doesn't call this property).
         */
        topicResponse = ""

        /*
         *   The number of times this topic has invoked by the player.  Each
         *   time the player asks/tells/etc about this topic, we'll increment
         *   this count.
         */
        talkCount = 0

        /*
         *   The number of times this topic or any nested AltTopic has been
         *   invoked by the player.  Each time the player asks/tells/etc about
         *   this topic OR any of its AltTopic children, we'll increment this
         *   count.
         */
        altTalkCount = 0

        /*
         *   the owner of any AltTopic nested within me is the same as my own
         *   topic owner, which we take from our location
         */
        getTopicOwner()
        {
            if (location != nil)
                return location.getTopicOwner();
            else
                return nil;
        }

        /*
         *   Initialize.  If we have a location property, we'll assume that the
         *   location is a topic database object, and we'll add ourselves to
         *   that database.
         */
        initializeTopicEntry()
        {
            /* if we have a location, add ourselves to its topic database */
            if (location != nil)
                location.addTopic(self);

            /* sort our list of AltTopic children */
            altTopicList = altTopicList.sort(
                SortAsc, {a, b: a.altTopicOrder - b.altTopicOrder});
        }

        /* add a topic nested within us */
        addTopic(entry)
        {
            /* if we have a location, add the entry to its topic database */
            if (location != nil)
                location.addTopic(entry);
        }

        /*
         *   Add an AltTopic entry.  This is called by our AltTopic children
         *   during initialization; we'll simply add the entry to our list of
         *   AltTopic children.
         */
        addAltTopic(entry)
        {
            /* add the entry to our list of alternatives */
            altTopicList += entry;
        }

        /* get the topic group score adjustment (for AltTopics nested within) */
        topicGroupScoreAdjustment = (location.topicGroupScoreAdjustment)

        /* check the group isActive status (for AltTopics nested within) */
        topicGroupActive = (location.topicGroupActive)

        /* our list of AltTopic children */
        altTopicList = []

        /*
         *   Match a topic.  This is abstract in this base class; it must be
         *   defined by each concrete subclass.  This returns nil if there's no
         *   match, or an integer value if there's a match.  The higher the
         *   number's value, the stronger the match.
         *
         *   This is abstract in the base class because the meaning of 'topic'
         *   varies by subclass, according to which type of command it's used
         *   with.  For example, in ASK and TELL commands, 'topic' is a
         *   ResolvedTopic describing the topic in the player's command; for
         *   GIVE and SHOW commands, it's the resolved simulation object.
         */
        // matchTopic(fromActor, topic) { return nil; }

        /*
         *   Check to see if a match to this topic entry is *possible* right
         *   now for the given actor.  For most subclasses, this is inherently
         *   imprecise, because the 'match' function simply isn't reversible in
         *   general: to know if we can be matched, we'd have to determine if
         *   there's a non-empty set of possible inputs that can match us.
         *   This method is complementary to matchTopic(), so subclasses must
         *   override with a corresponding implementation.
         *
         *   'actor' is the actor to whom we're making the suggestion.
         *   'scopeList' is the list of objects that are in scope for the
         *   actor.
         *
         *   The library only uses this to determine if a suggestion should be
         *   offered.  So, specialized topic instances with non-standard match
         *   rules don't have to worry about this unless they're used as
         *   suggestions, or unless the game itself needs this information for
         *   some other reason.
         */
        // isMatchPossible(actor, scopeList) { return true; }

        /*
         *   Break a tie among matching topics entries.  The topic database
         *   searcher calls this on each matching topic entry when it finds
         *   multiple entries tied for first place, based on their match
         *   scores.  This gives the entries a chance to figure out which one
         *   is actually the best match for the input, given the other entries
         *   that also matched.
         *
         *   This method returns a TopicEntry object - one of the objects from
         *   the match list - if it has an opinion as to which one should take
         *   precedence.  It returns nil if it doesn't know or doesn't care.
         *   Returning nil gives the other topics in the match list a chance to
         *   make the selection.  If all of the objects in the list return nil,
         *   the topic database searcher simply picks one of the topic matches
         *   arbitrarily.
         *
         *   'matchList' is the list of tied TopicEntry objects.  'topic' is
         *   the ResolvedTopic object from the parser, representing the
         *   player's input phrase that we're matching.  'fromActor' is the
         *   actor performing the command.  'toks' is a list of strings giving
         *   the word tokens of the noun phrase.
         *
         *   The topic database searcher calls this method for each matching
         *   topic entry in the case of a tie, and simply accepts the opinion
         *   of the first one that expresses an opinion by returning a non-nil
         *   value.  There's no voting; whoever happens to get *and use* the
         *   first say also gets the last word.  We expect that this won't be a
         *   problem in practice: when this comes up at all, it's because there
         *   are a couple of closely related topic entries that are active in a
         *   particular context, and you need a special bit of tweaking to pick
         *   the right one for a given input phrase.  Simply pick one of the
         *   involved entries and define this method there.
         */
        breakTopicTie(matchList, topic, fromActor, toks)
        {
            /*
             *   we don't have an opinion - defer to the next object in the
             *   list, or allow an arbitrary selection
             */
            return nil;
        }

        /*
         *   Set pronouns for the topic, if possible.  If the topic corresponds
         *   to a game-world object, then we should set the pronoun antecedent
         *   to the game object.  This must be handled per subclass because of
         *   the range of possible meanings of 'topic'.
         */
        setTopicPronouns(fromActor, topic) { }

        /*
         *   Handle the topic.  This is called when we find that this is the
         *   best topic entry for the current topic.
         *
         *   By default, we'll do one of two things:
         *
         *   - If 'self' inherits from Script, then we'll simply invoke our
         *   doScript() method.  This makes it especially easy to set up a
         *   topic entry that shows a series of responses: just add EventList
         *   or one of its subclasses to the base class list when defining the
         *   topic, and define the eventList property as a list of string
         *   responses.  For example:
         *
         *.     + TopicEntry, StopEventList @blackBook
         *.        ['<q>What makes you think I know anything about it?</q>
         *.         he says, his voice shaking. ',
         *.         '<q>No! You can\'t make me tell you!</q> he wails. ',
         *.         '<q>All right, fine! I\'ll tell you, but I warn you,
         *.         this is knowledge mortal men were never meant to know.</q> ',
         *.         // and so on
         *.        ]
         *.     ;
         *
         *   - Otherwise, we'll call our topicResponse property, which should
         *   simply be a double-quoted string to display.  This is the simplest
         *   way to define a topic with just one response.
         *
         *   Note that 'topic' will vary by subclass, depending on the type of
         *   command used with the topic type.  For example, for ASK and TELL
         *   commands, 'topic' is a ResolvedTopic object; for GIVE and SHOW,
         *   it's a simulation object (i.e., generally a Thing subclass).
         */
        handleTopic(fromActor, topic)
        {
            /* note the invocation */
            noteInvocation(fromActor);

            /* set pronoun antecedents if possible */
            setTopicPronouns(fromActor, topic);

            /* check to see if we're a Script */
            if (ofKind(Script))
            {
                /* we're a Script - invoke our script */
                doScript();
            }
            else
            {
                /* show our simple response string */
                topicResponse;
            }
        }

        /* note that we've been invoked */
        noteInvocation(fromActor)
        {
            /*
             *   we count as one of the alternatives in our alternative group,
             *   so note the invocation of the group
             */
            noteAltInvocation(fromActor, self);

            /* count the invocation */
            ++talkCount;
        }

        /*
         *   Note that something in our entire alternative group has been
         *   invoked.  We count as a member of our own group, so this is
         *   invoked when we're invoked; this is also invoked when any AltTopic
         *   child of ours is invoked.
         */
        noteAltInvocation(fromActor, alt)
        {
            local owner;

            /* notify our owner of the topic invocation */
            if ((owner = location.getTopicOwner()) != nil)
                owner.notifyTopicResponse(fromActor, alt);

            /* count the alternative invocation */
            ++altTalkCount;
        }

        /*
         *   Add a suggested topic.  A suggested topic can be nested within a
         *   topic entry; doing this associates the suggested topic with the
         *   topic entry, and automatically associates the suggested topic
         *   with the entry's actor or actor state.
         */
        addSuggestedTopic(t)
        {
            /*
             *   If the SuggestedTopic is *directly* within us, we're the
             *   SuggestedTopic object's associated TopicEntry.  The nesting
             *   could be deeper, if we have alternative topics nested within
             *   us; in these cases, we're not directly associated with the
             *   suggested topic.
             */
            if (t.location == self)
                t.associatedTopic = self;

            /* add the suggestion to our location's topic database */
            if (location != nil)
                location.addSuggestedTopic(t);
        }
    ;

    /*
     *   A TopicGroup is an abstract container for a set of TopicEntry objects.
     *   The purpose of the group object is to apply a common "is active"
     *   condition to all of the topics within the group.
     *
     *   The isActive condition of the TopicGroup is effectively AND'ed with
     *   any other conditions on the nested TopicEntry's.  In other words, a
     *   TopicEntry within the TopicGroup is active if the TopicEntry would
     *   otherwise be acive AND the TopicGroup is active.
     *
     *   TopicEntry objects are associated with the group via the 'location'
     *   property - set the location of the TopicEntry to point to the
     *   containing TopicGroup.
     *
     *   You can put a TopicGroup anywhere a TopicEntry could go - directly
     *   inside an Actor, inside an ActorState, or within another TopicGroup.
     *   The topic entries within a topic group act as though they were
     *   directly in the topic group's container.
     */
    class TopicGroup: object
        /*
         *   The group "active" condition - each instance should override this
         *   to specify the condition that applies to all of the TopicEntry
         *   objects within the group.
         */
        isActive = true

        /*
         *   The *adjustment* to the match score for topic entries contained
         *   within this group.  This is usually a positive number, so that it
         *   boosts the match strength of the child topics.
         */
        matchScoreAdjustment = 0

        /*
         *   the topic owner for any topic entries within the group is the
         *   topic owner taken from the group's own location
         */
        getTopicOwner() { return location.getTopicOwner(); }

        /* are TopicEntry objects within the group active? */
        topicGroupActive()
        {
            /*
             *   our TopicEntry objects are active if the group condition is
             *   true and our container's contents are active
             */
            return isActive && location.topicGroupActive();
        }

        /*
         *   Get my score adjustment.  We'll return our own basic score
         *   adjustment plus the cumulative adjustment for our containers.
         */
        topicGroupScoreAdjustment = (matchScoreAdjustment
                                     + location.topicGroupScoreAdjustment)

        /* add a topic - we'll simply add the topic directly to our container */
        addTopic(topic) { location.addTopic(topic); }

        /* add a suggested topic - we'll pass this up to our container */
        addSuggestedTopic(topic) { location.addSuggestedTopic(topic); }
    ;

    /*
     *   An alternative topic entry.  This makes it easy to define different
     *   responses to a topic according to the game state; for example, we
     *   might want to provide a different response for a topic after some
     *   event has occurred, so that we can reflect knowledge of the event in
     *   the response.
     *
     *   A set of alternative topics is sort of like an inverted if-then-else.
     *   You start by defining a normal TopicEntry (an AskTopic, or an
     *   AskTellTopic, or whatever) for the basic response.  Then, you add a
     *   nested AltTopic located within the base topic; you can add another
     *   AltTopic nested within the base topic, and another after that, and so
     *   on.  When we need to choose one of the topics, we'll choose the last
     *   one that indicates it's active.  So, the order of appearance is
     *   essentially an override order: the first AltTopic overrides its parent
     *   TopicEntry, and each subsequent AltTopic overrides its previous
     *   AltTopic.
     *
     *   + AskTellTopic @lighthouse "It's very tall.";
     *.  ++ AltTopic "Not really..." isActive=(...);
     *.  ++ AltTopic "Well, maybe..." isActive=(...);
     *.  ++ AltTopic "One more thing..." isActive=(...);
     *
     *   In this example, the response we'll show for ASK ABOUT LIGHTHOUSE will
     *   always be the LAST entry of the group that's active.  For example, if
     *   all of the responses are active except for the very last one, then
     *   we'll show the "Well, maybe" response, because it's the last active
     *   response.  If the main AskTellTopic is active, but none of the
     *   AltTopics are active, we'll show the "It's very tall" main response,
     *   because it's the last element of the group that's active.
     *
     *   Note that an AltTopic takes its matching information from its parent,
     *   so you don't need to specify a matchObj or any other matching
     *   information in an AltTopic.  You merely need to provide the response
     *   text and the isActive test.
     */
    class AltTopic: TopicEntry
        /* we match if our parent matches, and with the same score */
        matchTopic(fromActor, topic)
            { return location.matchTopic(fromActor, topic); }

        /* we can match if our parent can match */
        isMatchPossible(actor, scopeList)
            { return location.isMatchPossible(actor, scopeList); }

        /* we can match a pre-parse string if our parent can */
        matchPreParse(str, pstr) { return location.matchPreParse(str, pstr); }

        /* set pronouns for the topic */
        setTopicPronouns(fromActor, topic)
            { location.setTopicPronouns(fromActor, topic); }

        /* include in the same lists as our parent */
        includeInList = (location.includeInList)

        /* AltTopic initialization */
        initializeAltTopic()
        {
            /* add myself to our parent's child list */
            if (location != nil)
                location.addAltTopic(self);
        }

        /*
         *   Determine if this topic is active.  An AltTopic is active if its
         *   own isActive indicates true, AND none of its subsequent siblings
         *   are active.
         */
        checkIsActive()
        {
            /* we can't be active if our own isActive says we're not */
            if (!isActive)
                return nil;

            /*
             *   Check for any active element after us in the parent's list.
             *   To do this, scan from the end of the parent list backwards,
             *   and look for an element that's active.  If we reach our own
             *   entry, then we'll know that there are no active entries
             *   following us in the list.  Note that we already know we're
             *   active, or we wouldn't have gotten this far, so we can simply
             *   look for the rightmost active element in the list.
             */
            if (location != nil
                && location.altTopicList.lastValWhich({x: x.isActive}) != self)
            {
                /*
                 *   we found an active element after ourself, so it overrides
                 *   us - we're therefore not active
                 */
                return nil;
            }

            /* ask our container if its topics are active */
            return location.topicGroupActive();
        }

        /* take our implied-greeting status from our parent */
        impliesGreeting = (location.impliesGreeting)

        /* take our conversational status from our parent */
        isConversational = (location.isConversational)

        /*
         *   Our relative order within our parent's list of alternatives.  By
         *   default, we simply return the source file ordering, which ensures
         *   that static AltTopic objects (i.e., those defined directly in
         *   source files, not dynamically created with 'new') will be ordered
         *   just as they're laid out in the source file.
         */
        altTopicOrder = (sourceTextOrder)

        /* note invocation */
        noteInvocation(fromActor)
        {
            /* count our own invocation */
            ++talkCount;

            /* let our container know its AltTopic child is being invoked */
            if (location != nil)
                location.noteAltInvocation(fromActor, self);
        }

        /* our AltTopic counter is the AltTopic counter for the enclosing topic */
        altTalkCount = (location != nil ? location.altTalkCount : talkCount)
    ;

    /*
     *   A "topic match" topic entry.  This is a topic entry that matches topic
     *   phrases in the grammar.
     *
     *   Handling topic phrases is a bit tricky, because they can't be resolved
     *   to definitive game-world objects the way ordinary noun phrases can.
     *   Topic phrases can refer to things that aren't physically present, but
     *   which are known to the actor performing the command; they can refer to
     *   abstract Topic objects, that have no physical existence in the game
     *   world at all; and they can ever be arbitrary text that doesn't match
     *   any vocabulary defined by the game.
     *
     *   Our strategy in matching topics is to first narrow the list down to
     *   the physical and abstract game objects that both match the vocabulary
     *   used in the command and are part of the memory of the actor performing
     *   the command.  That much is handled by the normal topic phrase
     *   resolution rules, and gives us a list of possible matches.  Then,
     *   given this narrowed list of possibilities, we look through the list of
     *   objects that we're associated with; we effectively intersect the two
     *   lists, and if the result is non-empty, we consider it a match.
     *   Finally, we also consider any regular expression that we're associated
     *   with; if we have one, and the topic phrase text in the command matches
     *   the input, we'll consider it a match.
     */
    class TopicMatchTopic: TopicEntry
        /*
         *   A regular expression pattern that we'll match to the actual topic
         *   text as entered in the command.  If 'matchExactCase' is true,
         *   we'll match the exact text in its original upper/lower case
         *   rendering; otherwise, we'll convert the player input to lower-case
         *   before matching it against the pattern.  In most cases, we'll want
         *   to match the input no matter what combination of upper and lower
         *   case the player entered, so matchExactCase is nil by default.
         *
         *   Note that both the object (or object list) and the regular
         *   expression pattern can be included for a single topic entry
         *   object.  This allows a topic entry to match several different ways
         *   of entering the topic name, or to match several different topics
         *   with the same response.
         */
        matchPattern = nil
        matchExactCase = nil

        /*
         *   Match the topic.  By default, we'll match to either the simulation
         *   object or objects in matchObj, or the pattern in matchPattern.
         *   Note that we always try both ways of matching, so a single
         *   AskTellTopic can define both a pattern and an object list.
         *
         *   'topic' is a ResolvedTopic object describing the player's text
         *   input and the list of objects that the parser matched to the text.
         *
         *   Subclasses can override this as desired to use other ways of
         *   matching.
         */
        matchTopic(fromActor, topic)
        {
            /*
             *   if we have one or more match objects, try matching to the
             *   topic's best simulation object match
             */
            if (matchObj != nil)
            {
                /*
                 *   we have a match object or match object list - if it's a
                 *   collection, check each element, otherwise just match the
                 *   single object
                 */
                if (matchObj.ofKind(Collection))
                {
                    /* try matching each object in the list */
                    if (matchObj.indexWhich({x: findMatchObj(x, topic)}) != nil)
                        return matchScore;
                }
                else
                {
                    /* match the single object */
                    if (findMatchObj(matchObj, topic))
                        return matchScore;
                }
            }

            /*
             *   check for a match to the regular expression pattern, if we
             *   have a pattern AND the resolved topic allows literal matches
             */
            if (matchPattern != nil && topic.canMatchLiterally())
            {
                local txt;

                /*
                 *   There's no match object; try matching our regular
                 *   expression to the actual topic text.  Get the actual text.
                 */
                txt = topic.getTopicText();

                /*
                 *   if they don't want an exact case match, convert the
                 *   original topic text to lower case
                 */
                if (!matchExactCase)
                    txt = txt.toLower();

                /* if the regular expression matches, we match */
                if (rexMatch(matchPattern, txt) != nil)
                    return matchScore;
            }

            /* we didn't find a match - indicate this with a nil score */
            return nil;
        }

        /*
         *   Match an individual item from our match list to the given
         *   ResolvedTopic object.  We'll check each object in the resolved
         *   topic's "in scope" and "likely" lists.
         */
        findMatchObj(obj, rt)
        {
            /* check the "in scope" list */
            if (rt.inScopeList.indexOf(obj) != nil)
                return true;

            /* check the "likely" list */
            return (rt.likelyList.indexOf(obj) != nil);
        }

        /*
         *   It's possible for us to match if any of our matchObj objects are
         *   known to the actor.  If we have no matchObj objects, we must be
         *   matching on a regular expression or on a custom condition, so we
         *   can't speculate on matchability; we'll simply return true in those
         *   cases.
         */
        isMatchPossible(actor, scopeList)
        {
            /* check what we have in our matchObj */
            if (matchObj == nil)
            {
                /*
                 *   we have no match object, so we must match on a regular
                 *   expression or a custom condition; we can't speculate on
                 *   our matchability, so just return true as a default
                 */
                return true;
            }
            else if (matchObj.ofKind(Collection))
            {
                /*
                 *   we have a list of match objects - return true if any of
                 *   them are known or are currently in scope
                 */
                return (matchObj.indexWhich(
                    {x: actor.knowsAbout(x) || scopeList.indexOf(x)}) != nil);
            }
            else
            {
                /*
                 *   we have a single match object - return true if it's known
                 *   or it's in scope
                 */
                return (actor.knowsAbout(matchObj)
                        || scopeList.indexOf(matchObj) != nil);
            }
        }

        /* set the topic pronouns */
        setTopicPronouns(fromActor, topic)
        {
            /* check to see what kind of match object we have */
            if (matchObj == nil)
            {
                /*
                 *   no match object, so we must match a regular expression
                 *   pattern; this gives us no clue what game object we might
                 *   match, so there's nothing we can do here
                 */
            }
            else if (matchObj.ofKind(Collection))
            {
                local lst;

                /*
                 *   We match a list of objects.  Get the subset of the
                 *   in-scope list from the topic that we match.  Consider only
                 *   the in-scope items for now, and consider only game-world
                 *   objects (Things).
                 */
                lst = matchObj.subset(
                    {x: x.ofKind(Thing) && topic.inScopeList.indexOf(x) != nil});

                /* if that didn't turn up anything, consider the likelies, too */
                if (lst.length() == 0)
                    lst = matchObj.subset(
                        {x: (x.ofKind(Thing)
                             && topic.likelyList.indexOf(x) != nil)});

                /*
                 *   if that narrows it down to one match, make it the pronoun
                 *   antecedent
                 */
                if (lst.length() == 1)
                    fromActor.setPronounObj(lst[1]);
            }
            else
            {
                /*
                 *   we match a single object; if it's a game-world object (a
                 *   Thing), use it as the pronoun antecedent
                 */
                if (matchObj.ofKind(Thing))
                    fromActor.setPronounObj(matchObj);
            }
        }
    ;

    /*
     *   A dual ASK/TELL topic database entry.  This type of topic is included
     *   in both the ASK ABOUT and TELL ABOUT lists.
     *
     *   Many authors have chosen to treat ASK and TELL as equivalent, or at
     *   least, equivalent for most topics.  Since these verbs only very weakly
     *   suggest what the player character is actually saying, it's frequently
     *   the case that a given topic response makes just as much sense coming
     *   from TELL as from ASK, or vice versa.  In these cases, it's best to
     *   enter the topic under both ASK and TELL; which one the player tries
     *   might simply depend on the player's frame of mind, and they might feel
     *   cheated if one works and the other doesn't in cases where both are
     *   equally valid.
     */
    class AskTellTopic: TopicMatchTopic
        /* include me in both the ASK and TELL lists */
        includeInList = [&askTopics, &tellTopics]
    ;

    /*
     *   An ASK ABOUT topic database entry.  This type of topic is included in
     *   the ASK ABOUT list only.
     */
    class AskTopic: AskTellTopic
        includeInList = [&askTopics]
    ;

    /*
     *   A TELL ABOUT topic database entry.  This type of topic entry is
     *   included in the TELL ABOUT list only.
     */
    class TellTopic: AskTellTopic
        includeInList = [&tellTopics]
    ;

    /*
     *   An ASK FOR topic database entry.  This type of topic entry is
     *   included in the ASK FOR list only.
     */
    class AskForTopic: AskTellTopic
        includeInList = [&askForTopics]
    ;

    /*
     *   A combination ASK ABOUT and ASK FOR topic.
     */
    class AskAboutForTopic: AskTellTopic
        includeInList = [&askTopics, &askForTopics]
    ;

    /*
     *   A combination ASK ABOUT, TELL ABOUT, and ASK FOR topic.
     */
    class AskTellAboutForTopic: AskTellTopic
        includeInList = [&askTopics, &tellTopics, &askForTopics]
    ;

    /*
     *   A base class for topic entries that match simple simulation objects.
     */
    class ThingMatchTopic: TopicEntry
        /*
         *   Match the topic.  We'll match the simulation object in 'obj' to
         *   our matchObj object or list.
         */
        matchTopic(fromActor, obj)
        {
            /*
             *   if matchObj is a collection, check each element, otherwise
             *   just match the single object
             */
            if (matchObj.ofKind(Collection))
            {
                /* try matching each object in the list */
                if (matchObj.indexOf(obj) != nil)
                    return matchScore;
            }
            else
            {
                /* match the single object */
                if (matchObj == obj)
                    return matchScore;
            }

            /* didn't find a match - indicate this by returning a nil score */
            return nil;
        }

        /*
         *   It's possible for us to match if any of our matchObj objects are
         *   in scope.
         */
        isMatchPossible(actor, scopeList)
        {
            /* check to see what kind of match object we have */
            if (matchObj.ofKind(Collection))
            {
                /* we can match if any of our match objects are in scope */
                return (matchObj.indexWhich({x: scopeList.indexOf(x)}) != nil);
            }
            else
            {
                /* we can match if our single match object is in scope */
                return scopeList.indexOf(matchObj);
            }
        }

        /* set the topic pronouns */
        setTopicPronouns(fromActor, topic)
        {
            /*
             *   the 'topic' is just an ordinary game object; as long as it's a
             *   Thing, set it as the antecedent
             */
            if (topic.ofKind(Thing))
                fromActor.setPronounObj(topic);
        }
    ;

    /*
     *   A GIVE/SHOW topic database entry.
     *
     *   Note that this base class is usable for any command that refers to a
     *   simulation object.  It's NOT suitable for ASK/TELL lists, or for other
     *   commands that refer to topics, since we expect our 'topic' to be a
     *   resolved simulation object.
     */
    class GiveShowTopic: ThingMatchTopic
        /* include me in both the GIVE and SHOW lists */
        includeInList = [&giveTopics, &showTopics]
    ;

    /*
     *   A GIVE TO topic database entry.  This type of topic entry is included
     *   in the GIVE TO list only.
     */
    class GiveTopic: GiveShowTopic
        includeInList = [&giveTopics]
    ;

    /*
     *   A SHOW TO topic database entry.  This type of topic entry is included
     *   in the SHOW TO list only.
     */
    class ShowTopic: GiveShowTopic
        includeInList = [&showTopics]
    ;

    /*
     *   A TopicEntry that can match a Thing or a Topic.  This can be used to
     *   combine ASK/TELL-type responses and GIVE/SHOW-type responses in a
     *   single topic entry.
     *
     *   When this kind of topic is used as a suggested topic, note that you
     *   should name the suggestion according to the least restrictive verb.
     *   This is important because the suggestion will be active if any of the
     *   verbs would allow it; to ensure that we suggest a verb that will
     *   actually work, we should thus use the least restrictive verb.  In
     *   practice, this means you should use ASK or TELL as the suggestion
     *   name, because an object merely has to be known to be used as a topic;
     *   it might be possible to ASK/TELL about an object but not GIVE/SHOW the
     *   object, because the object is known but not currently in scope.
     */
    class TopicOrThingMatchTopic: ThingMatchTopic, TopicMatchTopic
        matchTopic(fromActor, obj)
        {
            /*
             *   if we're being asked to match a ResolvedTopic, use the
             *   inherited TopicMatchTopic handling; otherwise, use the
             *   inherited ThingMatchTopic handling
             */
            if (obj.ofKind(ResolvedTopic))
                return inherited TopicMatchTopic(fromActor, obj);
            else
                return inherited ThingMatchTopic(fromActor, obj);
        }

        isMatchPossible(actor, scopeList)
        {
            /* if a match is possible from either subclass, allow it */
            return (inherited TopicMatchTopic(actor, scopeList)
                    || inherited ThingMatchTopic(actor, scopeList));
        }

        setTopicPronouns(fromActor, obj)
        {
            /*
             *   if the object is a ResolvedTopic, use the inherited
             *   TopicMatchTopic handling, otherwise use the ThingMatchTopic
             *   handling
             */
            if (obj.ofKind(ResolvedTopic))
                return inherited TopicMatchTopic(fromActor, obj);
            else
                return inherited ThingMatchTopic(fromActor, obj);
        }
    ;

    /*
     *   A combined ASK/TELL/SHOW topic.  Players will sometimes want to point
     *   something out when it's visible, rather than asking about it; this
     *   allows SHOW TO to be used as a synonym for ASK ABOUT for these cases.
     */
    class AskTellShowTopic: TopicOrThingMatchTopic
        includeInList = [&askTopics, &tellTopics, &showTopics]
    ;

    /*
     *   A combined ASK/TELL/GIVE/SHOW topic.
     */
    class AskTellGiveShowTopic: TopicOrThingMatchTopic
        includeInList = [&askTopics, &tellTopics, &giveTopics, &showTopics]
    ;

    /*
     *   A command topic.  This is used to respond to orders given to an NPC,
     *   as in "BOB, GO EAST."  The match object for this kind of topic entry
     *   is an Action class; for example, to create a response to "BOB, LOOK",
     *   we'd create a CommandTopic that matches LookAction.
     *
     *   If you're designing a CommandTopic for a command can be accepted from
     *   a remote location, such as by telephone, you should be aware that the
     *   command will be running in the NPC's visual sense context.  This means
     *   that if the player character can't see the NPC, the topic result
     *   message will be hidden - the NPC's visual sense context hides all
     *   messages generated while it's in effect if the PC can't see the NPC.
     *   This is usually desirable, since most messages relay visual
     *   information that wouldn't be visible to the player character if the PC
     *   can't see the subject of the message.  However, if you've specifically
     *   designed your CommandTopic to work remotely, this isn't at all what
     *   you want, since you've already taken the remoteness into account in
     *   the message and thus want the message to be displayed after all.  The
     *   way to handle this is to wrap the message in a callWithSenseContext()
     *   with a nil sense context.  For example:
     *
     *   topicResponse()
     *.     { callWithSenseContext(nil, nil, {: "Here's my message!" }); }
     */
    class CommandTopic: TopicEntry
        /* we go in the command topics list */
        includeInList = [&commandTopics]

        /* match the topic */
        matchTopic(fromActor, obj)
        {
            /*
             *   Check the collection or the single object, as needed.  Note
             *   that our match object is an Action base class, so we must
             *   match if 'obj' is of the match object class.
             */
            if (matchObj.ofKind(Collection))
            {
                /* check each entry for a match */
                if (matchObj.indexWhich({x: obj.ofKind(x)}) != nil)
                    return matchScore;
            }
            else
            {
                /* check our single object */
                if (obj.ofKind(matchObj))
                    return matchScore;
            }

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

        /*
         *   we can always match, since the player can always type in any
         *   possible action
         */
        isMatchPossible(actor, scopeList) { return true; }

        /* we have no pronouns to set */
        setTopicPronouns(fromActor, topic) { }
    ;

    /*
     *   A base class for simple miscellaneous topics.  These handle things
     *   like YES, NO, HELLO, and GOODBYE, where the topic is entirely
     *   contained in the verb, and there's no separate noun phrase needed to
     *   indicate the topic.
     */
    class MiscTopic: TopicEntry
        matchTopic(fromActor, obj)
        {
            /*
             *   if it's one of our matching topics, return our match score,
             *   otherwise return a nil score to indicate failure
             */
            return (matchList.indexOf(obj) != nil) ? matchScore : nil;
        }

        /*
         *   a match is always possible for simple verb topics (since the
         *   player could always type the verb)
         */
        isMatchPossible(actor, scopeList) { return true; }
    ;

    /*
     *   A greeting topic - this handles a HELLO or TALK TO command, as well
     *   as implied greetings (the kind of greeting generated when we jump
     *   directly into a conversation with an actor that uses stateful
     *   conversations, by typing a command like ASK ABOUT or TELL ABOUT
     *   without first saying HELLO explicitly).
     */
    class HelloTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [helloTopicObj, impHelloTopicObj]

        /*
         *   this is an explicit greeting, so it obviously shouldn't trigger
         *   an implied greeting, regardless of how conversational we are
         */
        impliesGreeting = nil

        /*
         *   if we use this as a greeting upon entering a ConvNode, we'll want
         *   to stay in the node afterward
         */
        noteInvocation(fromActor)
        {
            inherited(fromActor);
            "<.convstay>";
        }
    ;

    /*
     *   An implied greeting topic.  This handles ONLY implied greetings.
     *
     *   Note that we have a higher-than-normal score by default.  This makes
     *   it easy to program two common cases for conversational states.
     *   First, the more common case, where you want a single message for both
     *   implied and explicit greetings: just create a HelloTopic, since that
     *   responds to both kinds.  Second, the less common case, where we want
     *   to differentiate, writing separate responses for implied and explicit
     *   greetings: create a HelloTopic for the explicit kind, and ALSO create
     *   an ImpHelloTopic for the implied kind.  Since the ImpHelloTopic has a
     *   higher score, it'll overshadow the HelloTopic object when it matches
     *   an implied greeting; but since ImpHelloTopic doesn't match an
     *   explicit greeting, we'll fall back on the HelloTopic for that.
     */
    class ImpHelloTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [impHelloTopicObj]
        matchScore = 200

        /*
         *   this is itself a greeting, so we obviously don't want to trigger
         *   another greeting to greet the greeting
         */
        impliesGreeting = nil

        /*
         *   if we use this as a greeting upon entering a ConvNode, we'll want
         *   to stay in the node afterward
         */
        noteInvocation(fromActor)
        {
            inherited(fromActor);
            "<.convstay>";
        }
    ;

    /*
     *   Actor Hello topic - this handles greetings when an NPC initiates the
     *   conversation.
     */
    class ActorHelloTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [actorHelloTopicObj]
        matchScore = 200

        /* this is a greeting, so we don't want to trigger another greeting */
        impliesGreeting = nil

        /*
         *   if we use this as a greeting upon entering a ConvNode, we'll want
         *   to stay in the node afterward
         */
        noteInvocation(fromActor)
        {
            inherited(fromActor);
            "<.convstay>";
        }
    ;

    /*
     *   A goodbye topic - this handles both explicit GOODBYE commands and
     *   implied goodbyes.  Implied goodbyes happen when a conversation ends
     *   without an explicit GOODBYE command, such as when the player character
     *   walks away from the NPC, or the NPC gets bored and wanders off, or the
     *   NPC terminates the conversation of its own volition.
     */
    class ByeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [byeTopicObj,
                     leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj]

        /*
         *   If we're not already in a conversation when we say GOODBYE, don't
         *   bother saying HELLO implicitly - if the player is saying GOODBYE
         *   explicitly, she probably has the impression that there's some kind
         *   of interaction already going on with the NPC.  If we didn't
         *   override this, you'd get an automatic HELLO followed by the
         *   explicit GOODBYE when not already in conversation, which is a
         *   little weird.
         */
        impliesGreeting = nil
    ;

    /*
     *   An implied goodbye topic.  This handles ONLY automatic (implied)
     *   conversation endings, which happen when we walk away from an actor
     *   we're talking to, or the other actor ends the conversation after being
     *   ignored for too long, or the other actor ends the conversation of its
     *   own volition via npc.endConversation().
     *
     *   We use a higher-than-default matchScore so that any time we have both
     *   a ByeTopic and an ImpByeTopic that are both active, we'll choose the
     *   more specific ImpByeTopic.
     */
    class ImpByeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [leaveByeTopicObj, boredByeTopicObj, actorByeTopicObj]
        matchScore = 200
    ;

    /*
     *   A "bored" goodbye topic.  This handles ONLY goodbyes that happen when
     *   the actor we're talking terminates the conversation out of boredom
     *   (i.e., after a period of inactivity in the conversation).
     *
     *   Note that this is a subset of ImpByeTopic - ImpByeTopic handles
     *   "bored" and "leaving" goodbyes, while this one handles only the
     *   "bored" goodbyes.  You can use this kind of topic if you want to
     *   differentiate the responses to "bored" and "leaving" conversation
     *   endings.
     */
    class BoredByeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [boredByeTopicObj]
        matchScore = 300
    ;

    /*
     *   A "leaving" goodbye topic.  This handles ONLY goodbyes that happen
     *   when the PC walks away from the actor they're talking to.
     *
     *   Note that this is a subset of ImpByeTopic - ImpByeTopic handles
     *   "bored" and "leaving" goodbyes, while this one handles only the
     *   "leaving" goodbyes.  You can use this kind of topic if you want to
     *   differentiate the responses to "bored" and "leaving" conversation
     *   endings.
     */
    class LeaveByeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [leaveByeTopicObj]
        matchScore = 300
    ;

    /*
     *   An "actor" goodbye topic.  This handles ONLY goodbyes that happen when
     *   the NPC terminates the conversation of its own volition via
     *   npc.endConversation().
     */
    class ActorByeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [actorByeTopicObj]
        matchScore = 300
    ;

    /* a topic for both HELLO and GOODBYE */
    class HelloGoodbyeTopic: MiscTopic
        includeInList = [&miscTopics]
        matchList = [helloTopicObj, impHelloTopicObj,
                     byeTopicObj, boredByeTopicObj, leaveByeTopicObj,
                     actorByeTopicObj]

        /*
         *   since we handle greetings, we don't want to trigger a separate
         *   implied greeting
         */
        impliesGreeting = nil
    ;

    /*
     *   Topic singletons representing HELLO and GOODBYE topics.  These are
     *   used as the parameter to matchTopic() when we're looking for the
     *   response to the corresponding verbs.
     */
    helloTopicObj: object;
    byeTopicObj: object;

    /*
     *   a topic singleton for implied greetings (the kind of greeting that
     *   happens when we jump right into a conversation with a command like
     *   ASK ABOUT or TELL ABOUT, rather than explicitly saying HELLO first)
     */
    impHelloTopicObj: object;

    /*
     *   a topic singleton for an NPC-initiated hello (this is the kind of
     *   greeting that happens when the NPC is the one who initiates the
     *   conversation, via actor.initiateConversation())
     */
    actorHelloTopicObj: object;

    /*
     *   topic singletons for the two kinds of automatic goodbyes (the kind of
     *   conversation ending that happens when we simply walk away from an
     *   actor we're in conversation with, or when we ignore the other actor
     *   for enough turns that the actor gets bored and ends the conversation
     *   of its own volition)
     */
    boredByeTopicObj: object;
    leaveByeTopicObj: object;

    /*
     *   a topic singleton for an NPC-initiated goodbye (this is the kind of
     *   goodbye that happens when the NPC is the one who breaks off the
     *   conversation, via npc.endConversation())
     */
    actorByeTopicObj: object;

    /*
     *   A YES/NO topic.  These handle YES and/or NO, which are normally used
     *   as responses to questions posed by the NPC.  YesNoTopic is the base
     *   class, and can be used to create a single response for both YES and
     *   NO; YesTopic provides a response just for YES; and NoTopic provides a
     *   response just for NO.  The only thing an instance of these classes
     *   should normally need to specify is the response text (or a list of
     *   response strings, by multiply inheriting from an EventList subclass as
     *   usual).
     */
    class YesNoTopic: MiscTopic
        includeInList = [&miscTopics]

        /*
         *   our list of matching topic objects - we'll only ever be asked to
         *   match 'yesTopicObj' (for YES inputs) or 'noTopicObj' (for NO
         *   inputs)
         */
        matchList = [yesTopicObj, noTopicObj]
    ;

    class YesTopic: YesNoTopic
        matchList = [yesTopicObj]
    ;

    class NoTopic: YesNoTopic
        matchList = [noTopicObj]
    ;

    /*
     *   Topic singletons representing the "topic" of YES and NO commands.  We
     *   use these as the parameter to matchTopic() in the TopicEntry objects
     *   when we're looking for a response to a YES or NO command.
     */
    yesTopicObj: object;
    noTopicObj: object;

    /*
     *   A default topic entry.  This is an easy way to create an entry that
     *   will be used as a last resort, if no other entry is found.  This kind
     *   of entry will match *any* topic, but with the lowest possible score,
     *   so it will only be used if there's no other match for the topic.
     *
     *   It's a good idea to provide some variety in a character's default
     *   responses, because it seems that in every real game session, the
     *   player will at some point spend a while peppering an NPC with
     *   questions on every topic that comes to mind.  Usually, the player will
     *   think of many things that the author didn't anticipate.  The more
     *   things the author covers, the better, but it's unrealistic to think
     *   that an author can reasonably anticipate every topic, or even most
     *   topics, that players will think of.  So, we'll have a whole bunch of
     *   ASK, ASK, ASK commands all at once, and much of the time we'll get a
     *   bunch of default responses in a row.  It gets tedious in these cases
     *   when the NPC repeats the same default response over and over.
     *
     *   A simple but effective trick is to provide three or four random
     *   variations on "I don't know that," customized for the character.  This
     *   makes the NPC seem less like a totally predictable robot, and it can
     *   also be a convenient place to flesh out the character a bit.  An easy
     *   way to do this is to add ShuffledEventList to the superclass list of
     *   the default topic entry, and provide a eventList list with the various
     *   random responses.  For example:
     *
     *   + DefaultAskTellTopic, ShuffledEventList
     *.    ['Bob mutters something unintelligible and keeps fiddling with
     *.     the radio. ',
     *.     'Bob looks up from the radio for a second, but then goes back
     *.     to adjusting the knobs. ',
     *.     'Bob just keeps adjusting the radio, completely ignoring you. ']
     *.  ;
     *
     *   It's important to be rather generic in default responses; in
     *   particular, it's a bad idea to suggest that the NPC doesn't know about
     *   the topic.  From the author's perspective, it's easy to make the
     *   mistake of thinking "this is a default response, so it'll only be used
     *   for topics that are completely off in left field."  Wrong!  Sometimes
     *   the player will indeed ask about completely random stuff, but in
     *   *most* cases, the player is only asking because they think it's a
     *   reasonable thing to ask about.  Defaults that say things like "I don't
     *   know anything about that" or "What a crazy thing to ask about" or "You
     *   must be stupid if you think I know about that!" can make a game look
     *   poorly implemented, because these will inevitably be shown in response
     *   to questions that the NPC really ought to know about:
     *
     *.  >ask bob about his mother
     *.  "I don't know anything about that!"
     *.
     *.  >ask bob about his father
     *.  "You'd have to be a moron to think I'd know about that!"
     *
     *   It's better to use responses that suggest that the NPC is
     *   uninterested, or is hostile, or is preoccupied with something else, or
     *   doesn't understand the question, or something else appropriate to the
     *   character.  If you can manage to make the response about the
     *   *character*, rather than the topic, it'll reduce the chances that the
     *   response is jarringly illogical.
     */
    class DefaultTopic: TopicEntry
        /*
         *   A list of objects to exclude from the default match.  This can be
         *   used to create a default topic that matches everything EXCEPT a
         *   few specific topics that are handled in enclosing topic databases.
         *   For example, if you want to create a catch-all in a ConvNode's
         *   list of topics, but you want a particular topic to escape the
         *   catch-all and be sent instead to the Actor's topic database, you
         *   can put that topic in the exclude list for the catch-all, making
         *   it a catch-almost-all.
         */
        excludeMatch = []

        /* match anything except topics in our exclude list */
        matchTopic(fromActor, topic)
        {
            /*
             *   If the topic matches anything in the exclusion list, do NOT
             *   match the topic.  If 'topic' is a ResolvedTopic, search its
             *   in-scope and 'likely' lists; otherwise search for 'topic'
             *   directly in the exclusion list.
             */
            if (topic.ofKind(ResolvedTopic))
            {
                /* it's a resolved topic, so search the in-scope/likely lists */
                if (topic.inScopeList.intersect(excludeMatch).length() != 0
                    || topic.likelyList.intersect(excludeMatch).length() != 0)
                    return nil;
            }
            else if (excludeMatch.indexOf(topic) != nil)
                return nil;

            /* match anything else with our score */
            return matchScore;
        }

        /* use a low default matching score */
        matchScore = 1

        /* a match is always possible for a default topic */
        isMatchPossible(actor, scopeList) { return true; }

        /* set the topic pronoun */
        setTopicPronouns(fromActor, topic)
        {
            /*
             *   We're not matching anything, so we can get no guidance from
             *   the match object.  Instead, look at the topic itself.  If it's
             *   a Thing, set the Thing as the antecedent.  If it's a
             *   ResolvedTopic, and there's only one Thing match in scope, or
             *   only one Thing match in the likely list, set that.  Otherwise,
             *   we have no grounds for guessing.
             */
            if (topic != nil)
            {
                if (topic.ofKind(Thing))
                {
                    /* we have a Thing - use it as the antecedent */
                    fromActor.setPronounObj(topic);
                }
                else if (topic.ofKind(ResolvedTopic))
                {
                    local lst;

                    /*
                     *   if there's only one Thing in scope, or only one Thing
                     *   in the 'likely' list, use it
                     */
                    lst = topic.inScopeList.subset({x: x.ofKind(Thing)});
                    if (lst.length() == 0)
                        lst = topic.likelyList.subset({x: x.ofKind(Thing)});

                    /* if we got exactly one object, it's the antecedent */
                    if (lst.length() == 1)
                        fromActor.setPronounObj(lst[1]);
                }
            }
        }
    ;

    /*
     *   Default topic entries for different uses.  We'll use a hierarchy of
     *   low match scores, in descending order of specificity: 3 for
     *   single-type defaults (ASK only, for example), 2 for multi-type
     *   defaults (ASK/TELL), and 1 for the ANY default.
     */
    class DefaultCommandTopic: DefaultTopic
        includeInList = [&commandTopics]
        matchScore = 3
    ;
    class DefaultAskTopic: DefaultTopic
        includeInList = [&askTopics]
        matchScore = 3
    ;
    class DefaultTellTopic: DefaultTopic
        includeInList = [&tellTopics]
        matchScore = 3
    ;
    class DefaultAskTellTopic: DefaultTopic
        includeInList = [&askTopics, &tellTopics]
        matchScore = 2
    ;
    class DefaultGiveTopic: DefaultTopic
        includeInList = [&giveTopics]
        matchScore = 3
    ;
    class DefaultShowTopic: DefaultTopic
        includeInList = [&showTopics]
        matchScore = 3
    ;
    class DefaultGiveShowTopic: DefaultTopic
        includeInList = [&giveTopics, &showTopics]
        matchScore = 2
    ;
    class DefaultAskForTopic: DefaultTopic
        includeInList = [&askForTopics]
        matchScore = 3
    ;
    class DefaultAnyTopic: DefaultTopic
        includeInList = [&askTopics, &tellTopics, &showTopics, &giveTopics,
                         &askForTopics, &miscTopics, &commandTopics]

        /*
         *   exclude these from actor-initiated hellos & goodbyes - those
         *   should only match topics explicitly
         */
        excludeMatch = [actorHelloTopicObj, actorByeTopicObj]
        matchScore = 1
    ;

    /*
     *   A "special" topic.  This is a topic that responds to its own unique,
     *   custom command input.  In other words, rather than responding to a
     *   normal command like ASK ABOUT or SHOW TO, we'll respond to a command
     *   for which we define our own syntax.  Our special syntax doesn't have
     *   to follow any of the ordinary parsing conventions, because whenever
     *   our ConvNode is active, we get a shot at parsing player input before
     *   the regular parser gets to see it.
     *
     *   A special topic MUST be part of a ConvNode, because these are
     *   inherently meaningful only in context.  A special topic is active
     *   only when its conversation node is active.
     *
     *   Special topics are automatically Suggested Topics as well as Topic
     *   Entries.  Because special topics use their own custom grammar, it's
     *   unreasonable to expect a player to guess at the custom grammar, so we
     *   should always provide a topic inventory suggestion for every special
     *   topic.
     */
    class SpecialTopic: TopicEntry, SuggestedTopicTree
        /*
         *   Our keyword list.  Each special topic instance must define a list
         *   of strings giving the keywords we match.  The special topic will
         *   match user input if the user input consists exclusively of words
         *   from this keyword list.  The user input doesn't have to include
         *   all of the words defined here, but all of the words in the user's
         *   input have to appear here to match.
         *
         *   Alternatively, an instance can specifically define its own custom
         *   regular expression pattern instead of using the keyword list; the
         *   regular expression allows the instance to include punctuation in
         *   the syntax, or apply more restrictive criteria than simply
         *   matching the keywords.
         */
        keywordList = []

        /*
         *   Initialize the special topic.  This runs during
         *   pre-initialization, to give us a chance to do pre-game set-up.
         *
         *   This routine adds the topic's keywords to the global dictionary,
         *   under the 'special' token type.  Since a special topic's keywords
         *   are accepted when the special topic is active, it would be wrong
         *   for the parser to claim that the words are unknown when the
         *   special topic isn't active.  By adding the keywords to the
         *   dictionary, we let the parser know that they're valid words, so
         *   that it won't claim that they're unknown.
         */
        initializeSpecialTopic()
        {
            /* add each keyword */
            foreach (local cur in keywordList)
            {
                /*
                 *   Add the keyword.  Since we don't actually need the
                 *   word-to-object association that the dictionary stores,
                 *   simply associate the word with the SpecialTopic class
                 *   rather than with this particular special topic instance.
                 *   The dictionary only stores a given word-obj-prop
                 *   association once, even if it's entered repeatedly, so
                 *   tying all of the special topic keywords to the
                 *   SpecialTopic class ensures that we won't store redundant
                 *   entries if the same keyword is used in multiple special
                 *   topics.
                 */
                cmdDict.addWord(SpecialTopic, cur, &specialTopicWord);
            }
        }

        /*
         *   our regular expression pattern - we'll build this automatically
         *   from the keyword list if this isn't otherwise defined
         */
        matchPat = nil

        /* our suggestion (topic inventory) base name */
        name = ''

        /*
         *   our suggestion (topic inventory) full name is usually the same as
         *   the base name; special topics usually aren't grouped in topic
         *   suggestion listings, since each topic usually has its own unique,
         *   custom syntax
         */
        fullName = (name)

        /* on being suggested, update the special topic history */
        noteSuggestion() { specialTopicHistory.noteListing(self); }

        /* include in the specialTopics list of our parent topic database */
        includeInList = [&specialTopics]

        /*
         *   By default, don't limit the number of times we'll suggest this
         *   topic.  Since a special topic is valid only in a particular
         *   ConvNode context, we normally want all of the topics in that
         *   context to be available, even if they've been used before.
         */
        timesToSuggest = nil

        /* check for a match */
        matchTopic(fromActor, topic)
        {
            /*
             *   We match if and only if we're the current active topic for
             *   our conversation node, as designated during our pre-parsing.
             *   Because we're activated exclusively by our special syntax,
             *   the only way we can ever match is by matching our special
             *   syntax in pre-parsing; when that happens, the pre-parser
             *   notes the matching SpecialTopic and sends a pseudo-command to
             *   the parser to let it know to invoke the special topic's
             *   response.  We take this circuitous route to showing the
             *   response because we do our actual matching in the pre-parse
             *   step, but we want to do the actual command processing
             *   normally; we can only accomplish both needs using this
             *   two-step process, with the two steps tied together via our
             *   memory of the topic selected in pre-parse.
             */
            if (getConvNode().activeSpecialTopic == self)
                return matchScore;
            else
                return nil;
        }

        /*
         *   a special topic is always matchable, since we match on literal
         *   text
         */
        isMatchPossible(actor, scopeList) { return true; }

        /*
         *   Match a string during pre-parsing.  By default, we'll match the
         *   string if all of its words (as defined by the regular expression
         *   parser) match our keywords.
         */
        matchPreParse(str, procStr)
        {
            /* build the regular expression pattern if there isn't one */
            if (matchPat == nil)
            {
                local pat;

                /* start with the base pattern string */
                pat = '<nocase><space>*(%<';

                /* add the keywords */
                for (local i = 1, local len = keywordList.length() ;
                     i <= len ; ++i)
                {
                    /* add this keyword to the pattern */
                    pat += keywordList[i];

                    /* add the separator or terminator, as appropriate */
                    if (i == len)
                        pat += '%><space>*)+';
                    else
                        pat += '%><space>*|%<';
                }

                /* create the pattern object */
                matchPat = new RexPattern(pat);
            }

            /* we have a match if the pattern matches the processed input */
            return rexMatch(matchPat, procStr) == procStr.length();
        }

        /* find our enclosing ConvNode object */
        getConvNode()
        {
            /* scan up the containment tree for a ConvNode */
            for (local loc = location ; loc != nil ; loc = loc.location)
            {
                /* if this is a ConvNode, it's what we're looking for */
                if (loc.ofKind(ConvNode))
                    return loc;
            }

            /* not found */
            return nil;
        }
    ;

    /*
     *   A history of special topics listed in topic inventories.  This keeps
     *   track of special topics that we've recently offered, so that we can
     *   provide better feedback if the player tries to use a recently-listed
     *   special topic after it's gone out of context.
     *
     *   When the player types a command that the parser doesn't recognize, the
     *   parser will check the special topic history to see if the command
     *   matches a special topic that was suggested recently.  If so, we'll
     *   explain that the command isn't usable right now, rather than claiming
     *   that the command is completely invalid.  A player might justifiably
     *   find it confusing to have the game suggest a command one minute, and
     *   then claim that the very same command is invalid a minute later.
     *
     *   Ideally, we'd search *every* special topic for a match each time the
     *   player enters an invalid command, but that could take a long time in a
     *   conversation-heavy game with a large number of special topics.  As a
     *   compromise, we keep track of the last few special commands that were
     *   actually suggested, so that we can scan those.  The reasoning is that
     *   a player is more likely to try a recently-offered special command; the
     *   player will probably eventually forget older suggestions, and in any
     *   case it's much more jarring to see a "command not understood" response
     *   to a suggestion that's still fresh in the player's memory.
     *
     *   This is a transient object because we're interested in the special
     *   topics that have been offered in the current session, irrespective of
     *   things like 'undo' and 'restore'.  From the player's perspective, the
     *   recency of a special topic suggestion is a function of the transcript,
     *   not of the internal story timeline.  For example, if the game suggests
     *   a special topic, then the player types UNDO, the player might still
     *   think to try the special topic on the next turn simply because it's
     *   right there on the screen a few lines up.
     */
    transient specialTopicHistory: object
        /*
         *   Maximum number of topics to keep in our inventory.  When the
         *   history exceeds this number, we'll throw away the oldest entry
         *   each time we need to add a new entry - thus, we'll always have the
         *   N most recent suggestions.
         *
         *   This can be configured as desired.  The default setting tries to
         *   strike a balance between speed and good feedback - we try to keep
         *   track of enough entries that most players wouldn't think to try
         *   anything that's aged out of the list, but not so many that it
         *   takes a long time to scan them all.
         *
         *   If you set this to nil, we won't keep a history at all, but
         *   instead simply scan every special topic in the entire game when we
         *   need to look for a match to an entered command - in a game with a
         *   small number of special topics (on the order of, say, 30 or 40),
         *   there should be no problem using this approach.  Note that this
         *   changes the behavior in one important way: when there's no history
         *   limit, we can topics that *haven't even been offered yet*.  In
         *   some ways this is more desirable than only scanning past
         *   suggestions, since it avoids weird situations where the game
         *   claims that a command is unrecognized at one point, but later
         *   suggests and then accepts the exact same command.  It's
         *   conceivably less desirable in that it could accidentally give away
         *   information to the player, by letting them know that a randomly
         *   typed command will be meaningful at some point in the game - but
         *   the odds of this even happening seem minuscule, and the
         *   possibility that it would give away meaningful information even if
         *   it did happen seems very remote.
         */
        maxEntries = 20

        /* note that a special topic 't' is being listed in a topic inventory */
        noteListing(t)
        {
            /*
             *   If t's already in the list, delete it from its current
             *   position, so that we can add it back at the end of the list,
             *   reflecting its status as the most recent entry.
             */
            historyList.removeElement(t);

            /*
             *   if the list is already at capacity, remove the oldest entry,
             *   which is the first entry in the list
             */
            if (maxEntries != nil && historyList.length() >= maxEntries)
                historyList.removeElementAt(1);

            /* add the new entry at the end of the list */
            historyList.append(t);
        }

        /*
         *   Scan the history list (or, if there's no limit to the history,
         *   scan all of the special topics in the entire game) for a match to
         *   an unrecognized command.  Returns true if we find a match, nil if
         *   not.
         */
        checkHistory(toks)
        {
            local str, procStr;

            /* get the original and processed version of the input string */
            str = cmdTokenizer.buildOrigText(toks);
            procStr = specialTopicPreParser.processInputStr(str);

            /*
             *   scan each special topic in the history - or, if the history is
             *   unlimited, scan every special topic
             */
            if (maxEntries != nil)
            {
                /* scan each entry in our history list */
                for (local l = historyList, local i = 1, local len = l.length() ;
                     i <= len ; ++i)
                {
                    /* check this entry */
                    if (l[i].matchPreParse(str, procStr))
                        return true;
                }
            }
            else
            {
                /* no history limit - scan every special topic in the game */
                for (local o = firstObj(SpecialTopic) ; o != nil ;
                     o = nextObj(o, SpecialTopic))
                {
                    /* check this entry */
                    if (o.matchPreParse(str, procStr))
                        return true;
                }
            }

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

        /*
         *   The list of entries.  Create it when we first need it, which
         *   perInstance does for us.
         */
        historyList = perInstance(new transient Vector(maxEntries))
    ;

    /*
     *   An "initiate" topic entry.  This is a rather different kind of topic
     *   entry from the ones we've defined so far; an initiate topic is for
     *   cases where the NPC itself wants to initiate a conversation in
     *   response to something in the environment.
     *
     *   One way to use initiate topics is to use the current location as the
     *   topic key.  This lets the NPC say something appropriate to the current
     *   room, and can be coded simply as
     *
     *.     actor.initiateTopic(location);
     */
    class InitiateTopic: ThingMatchTopic
        /* include in the initiateTopics list */
        includeInList = [&initiateTopics]

        /*
         *   since this kind of topic is triggered by internal calculations in
         *   the game, and not on anything the player is doing, there's no
         *   reason that our match object should be a pronoun antecedent
         */
        setTopicPronouns(fromActor, topic) { }
    ;

    /* a catch-all default initiate topic */
    class DefaultInitiateTopic: DefaultTopic
        includeInList = [&initiateTopics]
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An ActorState represents the current state of an Actor.
     *
     *   The main thing that makes actors special is that they're supposed to
     *   be living, breathing people or creatures.  That substantially
     *   complicates the programming of one of these objects, because in order
     *   to create the appearance of animation, many things about an actor have
     *   to change over time.
     *
     *   The ActorState is designed to make it easier to program this
     *   variability that's needed to make an actor seem life-like.  The idea
     *   is to separate the parts of an actor that tend to change according to
     *   what the actor is doing, moving all of those out of the Actor object
     *   and into an ActorState object instead.  Each ActorState object
     *   represents one state of an actor (i.e., one thing the actor can be
     *   doing).  The Actor object becomes easier to program, because we've
     *   reduced the Actor object to the character's constant, unchanging
     *   features.  The stateful part is also easier to program, because we
     *   don't have to make it conditional on anything; we simply define all of
     *   the stateful parts in an ActorState, and we define separate ActorState
     *   objects for the different states.
     *
     *   For example, suppose we want a shopkeeper actor, whose activities
     *   include waiting behind the counter, sweeping the floor, and stacking
     *   cans.  We'd define one ActorState object for each of these activities.
     *   When the shopkeeper switches from standing behind the counter to
     *   sweeping, for example, we simply set the "curState" property in the
     *   shopkeeper object so that it points to the "sweeping" state object.
     *   When it's time to stack cans, we change "curState" to it points to the
     *   "stacking cans" state object.
     */
    class ActorState: TravelMessageHandler, ActorTopicDatabase
        construct(actor) { location = actor; }

        /*
         *   Activate the state - this is called when we're about to become
         *   the active state for an actor.  We do nothing by default.
         */
        activateState(actor, oldState) { }

        /*
         *   Deactivate the state - this is called when we're the active state
         *   for an actor, and the actor is about to switch to a new state.
         *   We do nothing by default.
         */
        deactivateState(actor, newState) { }

        /*
         *   Is this the actor's initial state?  If so, we'll automatically
         *   set the actor's curState to point to 'self' during
         *   pre-initialization.  For obvious reasons, this should be set to
         *   true for only one state for each actor; if multiple states are
         *   all flagged as initial for the same actor, we'll pick on
         *   arbitrarily as the actual initial state.
         */
        isInitState = nil

        /*
         *   Should we automatically suggest topics when the player greets our
         *   actor?  By default, we show our "topic inventory" (the list of
         *   currently active topics marked as "suggested").  This can be set
         *   to nil to suppress this automatic suggestion list.
         *
         *   Some authors might not like the idea of automatically suggesting
         *   topics every time we greet a character, but nonetheless wish to
         *   keep the TOPICS command as a sort of hint mechanism.  This flag
         *   can be used for this purpose.  Authors who don't like suggested
         *   topics at all can simply skip defining any SuggestedTopic entries,
         *   in which case there will never be anything to suggest, rendering
         *   this flag moot.
         */
        autoSuggest = true

        /*
         *   The 'location' is the actor that we're associated with.
         *
         *   ActorState objects aren't actual simulation objects, so the
         *   'location' property isn't used for containment.  For convenience,
         *   though, use it to indicate which actor we're associated with; this
         *   lets us use the '+' notation to define the state objects
         *   associated with an actor.
         */
        location = nil

        /*
         *   Get the actor associated with the state - this is simply the
         *   'location' property.  If we're nested inside another ActorState,
         *   then our actor is our enclosing ActorState's actor.
         */
        getActor()
        {
            if (location.ofKind(ActorState))
                return location.getActor();
            else
                return location;
        }

        /* the owner of any topic entries within the state is just my actor */
        getTopicOwner() { return getActor(); }

        /* initialize the actor state */
        initializeActorState()
        {
            /*
             *   if we're the initial state for our actor, set the actor's
             *   current state property to point to me
             */
            if (isInitState)
                getActor().setCurState(self);
        }

        /*
         *   Show the special description for the actor when the actor is
         *   associated with this state.  By default, we use the actor's
         *   actorHereDesc message, which usually shows a generic message
         *   (something like "Bob is here" or "Bob is sitting on the chair") to
         *   indicate that the actor is present.
         *
         *   States representing scripted activities should override these to
         *   indicate what the actor is doing: "Bob is sweeping the floor," for
         *   example.
         */
        specialDesc() { getActor().actorHereDesc; }

        /* show the special description for the actor at a distance */
        distantSpecialDesc() { getActor().actorThereDesc; }

        /* show the special description for the actor in a remote location */
        remoteSpecialDesc(actor) { getActor().actorThereDesc; }

        /*
         *   The list group(s) for the special description.  By default, if
         *   our specialDesc isn't overridden, we'll keep this in sync with
         *   the specialDesc by returning our actor's actorListWith.  And if
         *   specialDesc *is* overridden, we'll just return an empty list to
         *   indicate that we're not part of any list group.  If you want to
         *   provide your own listing group special to the state, simply
         *   override this and speicfy the custom list group.
         */
        specialDescListWith()
        {
            /*
             *   if specialDesc is inherited from ActorState, then use the
             *   default handling from the actor; otherwise, use no grouping at
             *   all by default
             */
            if (!overrides(self, ActorState, &specialDesc))
                return getActor().actorListWith;
            else
                return [];
        }

        /* show the special description when we appear in a contents listing */
        showSpecialDescInContents(actor, cont)
        {
            /* by default, just show our posture in our container */
            getActor().listActorPosture(actor);
        }

        /*
         *   Our "state" description.  This shows information on what the actor
         *   is *currently* doing; we display this after the static part of the
         *   actor's description on EXAMINE <ACTOR>.  By default, we add
         *   nothing here, but state objects that represent scripted activies
         *   should override this to describe their scripted activities.
         */
        stateDesc = ""

        /*
         *   Should we obey an action?  If so, returns true; if not, displays
         *   an appropriate response and returns nil.  This will only be
         *   called when the issuing actor is different from our actor, since
         *   a command to oneself is implicitly always obeyed.
         */
        obeyCommand(issuingActor, action)
        {
            /*
             *   By default, we ignore all orders.  We do need to generate a
             *   response, though, so for this purpose, treat the order as a
             *   conversational action, with the 'action' object as the topic.
             */
            handleConversation(issuingActor, action, commandConvType);

            /* indicate that the order is refused */
            return nil;
        }

        /*
         *   Suggest topics for the given actor to talk to us about.  This is
         *   called when the given actor enters a TOPICS command (in which
         *   case 'explicit' will be true) or enters a conversation with us
         *   via TALK TO or the like (in which case 'explicit' will be nil).
         */
        suggestTopicsFor(actor, explicit)
        {
            /*
             *   if this is not an explicit TOPICS request, and we're not in
             *   "auto suggest" mode, don't show anything - we don't want any
             *   automatic suggestions in this mode
             */
            if (!explicit && !autoSuggest)
                return;

            /*
             *   show a paragraph break, in case we're being tacked on to
             *   another report; but make it cosmetic, so that this by itself
             *   doesn't suppress a default report, in case we don't end up
             *   displaying any topics
             */
            cosmeticSpacingReport('<.p>');

            /* show our suggestion list */
            showSuggestedTopicList(getSuggestedTopicList(),
                                   actor, getActor(), explicit);
        }

        /*
         *   Get our suggested topic list.  The suggested topic list consists
         *   of the union of the current ConvNode's suggestion list, the
         *   ActorState list, and the Actor's suggestion list.  In each case,
         *   the suggestion list is the list of all SuggestedTopic objects at
         *   each database level.
         *
         *   The suggestions are arranged in a hierarchy, and each hierarchy
         *   level can prevent suggestions from a lower level from being
         *   included.  The top level of the hierarchy is the ConvNode; the
         *   next level is the ActorState; and the last level is the Actor.
         *   Suggestions are limited at each level with the 'limitSuggestions'
         *   property: if true, suggestions from lower levels are not included.
         */
        getSuggestedTopicList()
        {
            local v = new Vector(16);
            local node;
            local lst;

            /* add the actor's current conversation node topics */
            if ((node = getActor().curConvNode) != nil)
            {
                /* if there are any suggested topics in the node, include them */
                if ((lst = node.suggestedTopics) != nil)
                    v.appendAll(lst);

                /*
                 *   if this ConvNode is marked as limiting suggestions to
                 *   those defined within the node, return what we have
                 *   without adding anything from the broader context
                 */
                if (node.limitSuggestions)
                    return v;
            }

            /* add our own topics */
            if ((lst = stateSuggestedTopics) != nil)
                v.appendAll(lst);

            /*
             *   if the ActorState is limiting suggestions, don't include any
             *   suggestions from the broader context (i.e., from the Actor
             *   itself)
             */
            if (limitSuggestions)
                return v;

            /* if our actor has its own list, add those as well */
            if ((lst = getActor().suggestedTopics) != nil)
                v.appendAll(lst);

            /* return the combined list */
            return v;
        }

        /*
         *   get the topic suggestions for this state - by default, we just
         *   return our own suggestedTopics list
         */
        stateSuggestedTopics = (suggestedTopics)

        /*
         *   Get my implied in-conversation state.  This is used when our actor
         *   initiates a conversation without specifying a particular
         *   conversation state to enter (i.e., actor.initiateConversation() is
         *   called with 'state' set to nil).  By default, we don't have an
         *   implied conversation state, so we just return 'self' to indicate
         *   that we want to stay in the current state.  States that are
         *   coupled with separate in-conversation states, such as
         *   ConversationReadyState, should return their associated
         *   conversation states here.
         */
        getImpliedConvState = (self)

        /*
         *   General conversation handler.  This can be used to process most
         *   conversational commands - ASK, TELL, GIVE, SHOW, etc.  The
         *   standard sequence of processing is as follows:
         *
         *   - If our actor has a non-nil current conversation node (ConvNode)
         *   object, and the ConvNode wants to handle the event, let the
         *   ConvNode handle it.
         *
         *   - Otherwise, check our own topic database to see if we can find a
         *   TopicEntry that matches the topic; if we can find one, let the
         *   TopicEntry handle it.
         *
         *   - Otherwise, let the actor handle it.
         *
         *   'otherActor' is the actor who originated the conversation command
         *   (usually the player character). 'topic' is the subject being
         *   discussed (the indirect object of ASK ABOUT, for example).
         *   convType' is a ConvType describing the type of conversational
         *   action we're performing.
         */
        handleConversation(otherActor, topic, convType)
        {
            local actor = getActor();
            local hasDefault;
            local node;
            local path;

            /* determine if I have a default response handler */
            hasDefault = propDefined(convType.defaultResponseProp);

            /*
             *   Figure the database search path for looking up the topics.
             *   We'll start in the ConvNode database, then continue to the
             *   ActorState database, then finally to the Actor database.
             *   However, we won't reach the Actor database if there's a
             *   default response handler in the state, because if we fail to
             *   find it at the state, we'll take the default.
             *
             *   Since the path we need to provide at each point is the
             *   *remaining* path, don't bother including the ConvNode, since
             *   we'd just have to take it right back out to get the remaining
             *   path after the ConvNode.
             */
            path = [self];
            if (!hasDefault)
                path += actor;

            /*
             *   If our actor has a current conversation node, check to see if
             *   the conversation node wants to handle it.  If not, check our
             *   own topic database, then the actor's.
             */
            if ((node = actor.curConvNode) == nil
                || !node.handleConversation(otherActor, topic, convType, path))
            {
                /* get the remaining database search path */
                path = path.sublist(2);

                /*
                 *   Either we don't have a ConvNode, or the ConvNode isn't
                 *   interested in handling the operation.  Check to see if we
                 *   can handle it through our own topic database.
                 */
                if (!handleTopic(otherActor, topic, convType, path))
                {
                    /*
                     *   We couldn't find anything in our topic database that's
                     *   interested in handling it.  Check to see if the state
                     *   object defines the default response handler method,
                     *   and use that as the response if so.
                     */
                    if (hasDefault)
                    {
                        /*
                         *   the state object (i.e., self) does define the
                         *   default response method, so invoke that
                         */
                        convType.defaultResponse(self, otherActor, topic);
                    }
                    else
                    {
                        /*
                         *   We don't have a topic database entry and we don't
                         *   have our own definition of the default response
                         *   handler.  All that remains is to let our actor
                         *   handle it.
                         */
                        actor.handleConversation(otherActor, topic, convType);
                    }
                }
            }

            /* whatever happened, run the appropriate after-response handling */
            convType.afterResponse(actor, otherActor);
        }

        /*
         *   Receive notification that a TopicEntry is being used (via its
         *   handleTopic method) to respond to a command.  The TopicEntry will
         *   call this before it shows its message or takes any other action.
         *   By default, we do nothing.
         */
        notifyTopicResponse(fromActor, entry) { }

        /*
         *   Handle a before-action notification for our actor.  By default,
         *   we do nothing.
         */
        beforeAction()
        {
            /* do nothing by default */
        }

        /* handle an after-action notification for our actor */
        afterAction()
        {
        }

        /* handle a before-travel notification */
        beforeTravel(traveler, connector)
        {
            local other = getActor().getCurrentInterlocutor();

            /*
             *   if our conversational partner is departing, break off the
             *   conversation
             */
            if (connector != nil
                && other != nil
                && traveler.isActorTraveling(other))
            {
                /* end the conversation */
                if (!endConversation(gActor, endConvTravel))
                {
                    /*
                     *   they don't want to allow the conversation to end, so
                     *   abort the travel action
                     */
                    exit;
                }
            }
        }

        /* handle an after-travel notification */
        afterTravel(traveler, connector)
        {
        }

        /*
         *   End the current conversation.  'reason' indicates why we're
         *   leaving the conversation - this is one of the endConvXxx enums
         *   defined in adv3.h.  beforeTravel() calls this automatically when
         *   the other party is trying to depart, and they're talking to us.
         *
         *   This returns true if we wish to allow the conversation to end,
         *   nil if not.
         */
        endConversation(actor, reason)
        {
            local ourActor = getActor();
            local node;

            /* tell the current ConvNode about it */
            if ((node = ourActor.curConvNode) != nil)
            {
                local ret;

                /* the can-end call might show a response, so set our actor */
                conversationManager.beginResponse(ourActor);

                /* ask the node if it's okay to end the conversation */
                ret = node.canEndConversation(actor, reason);

                /*
                 *   If the result is blockEndConv, it means that the actor
                 *   said something to force the conversation to keep going.
                 *   Make a note that the other actor already said something on
                 *   this turn so that we don't generate another scripted
                 *   message later, and flag this as preventing the
                 *   conversation ending.
                 */
                if (ret == blockEndConv)
                {
                    /* flag that the other actor said something this turn */
                    ourActor.noteConvAction(actor);

                    /* we're unable to end the conversation now */
                    ret = nil;
                }

                /* end the response, leaving the node unchanged by default */
                conversationManager.finishResponse(
                    ourActor, ourActor.curConvNode);

                /*
                 *   if the node said no, tell the caller we can't end the
                 *   conversation right now
                 */
                if (!ret)
                    return nil;

                /* tell the node we are indeed ending the conversation */
                node.endConversation(actor, reason);
            }

            /* forget any conversation tree position */
            ourActor.setConvNodeReason(nil, 'endConversation');

            /* indicate that we are allowing the conversation to end */
            return true;
        }

        /*
         *   Take a turn.  This is called when it's the actor's turn and
         *   there's not something else the actor needs to be doing (such as
         *   following another actor, or carrying out a command in the actor's
         *   pending command queue).
         *
         *   By default, we perform several steps automatically.
         *
         *   First, we check to see if the actor is in a ConvNode.  If so, the
         *   ConvNode takes precedence.  If we haven't been addressed already
         *   in conversation on this turn, we'll let the ConvNode perform its
         *   "continuation," which lets the NPC advance the conversation of its
         *   own volition.  In any case, if we have a current ConvNode, we're
         *   done with the turn, since we assume the actor will want to proceed
         *   with the conversation before pursuing its agenda or performing a
         *   background action.
         *
         *   Second, assuming there's no active ConvNode, we check for an
         *   "agenda" item that's ready to execute.  If we find one, we execute
         *   it, and we're done.  The agenda item takes precedence over any
         *   other scripting we might have.
         *
         *   Finally, if we also inherit from Script, and we didn't find an
         *   active ConvNode or an agenda item that was ready to execute, we
         *   invoke our doScript() method.  This makes it especially easy to
         *   define random background messages for the actor - just add an
         *   EventList class (ShuffledEventList is usually the right one) to
         *   the state's superclass list, and define a list of background
         *   message strings.
         */
        takeTurn()
        {
            local actor = getActor();

            /*
             *   Check to see if we want to continue a conversation.  If so,
             *   and we haven't already conversed this turn, try the
             *   continuing conversation.  If that displays anything, consider
             *   the turn done.
             *
             *   Otherwise, try executing an agenda item.  If we do, consider
             *   the turn done.
             *
             *   Otherwise, if we're of class Script, execute our scripted
             *   action.
             */
            if (actor.curConvNode != nil
                && !actor.conversedThisTurn()
                && actor.curConvNode.npcContinueConversation())
            {
                /*
                 *   we displayed an NPC-motivated conversation continuation,
                 *   so we're done with this turn
                 */
            }
            else if (actor.executeAgenda())
            {
                /* we executed an agenda item, so we need do nothing more */
            }
            else if (ofKind(Script))
            {
                /* we're a Script, so invoke our scripted action */
                doScript();
            }
        }

        /*
         *   Receive notification that we just followed another actor as part
         *   of our programmed following behavior (in other words, due to our
         *   'followingActor' property, not due to an explicit FOLLOW command
         *   directed to us).  'success' is true if we ended up in the actor's
         *   location, nil if not.
         *
         *   This can be used to update the actor's state after a 'follow'
         *   operation occurs; for example, if the actor's state depends on
         *   the actor's location, this can update the state accordingly.  We
         *   don't do anything by default.
         */
        justFollowed(success)
        {
            /* do nothing by default */
        }

        /*
         *   Our group-travel arrival description.  By default, when we perform
         *   an accompanying travel with another actor as the lead actor, the
         *   accompanying travel state will display this message instead of our
         *   specialDesc when the lead actor first arrives in the new location.
         *   We'll just display our own specialDesc by default, but this should
         *   usually be overridden to say something specific to the group
         *   travel arrival.  The actual message is entirely dependent on the
         *   nature of the group travel, which is why we don't provide a
         *   special message by default.
         *
         *   For scripted behavior, it's sometimes better to use arrivingTurn()
         *   rather than this method to describe the behavior.
         *   arrivingWithDesc() is called as part of the room description, so
         *   it's best for any message shown here to fit well into the usual
         *   room description format.  For more complex transitions into the
         *   new room state, arrivingTurn() is sometimes more appropriate,
         *   since it runs like a daemon, after the arrival (and thus the new
         *   room description) is completed.
         */
        arrivingWithDesc() { specialDesc(); }

        /*
         *   Perform any special action on a group-travel arrival.  When group
         *   travel is performed using the AccompanyingInTravelState class,
         *   this is essentially called in lieu of the regular takeTurn()
         *   method on the state that is coming into effect after the group
         *   travel.  (Not really, but effectively: the accompanying travel
         *   state will still be in effect, so its takeTurn() method is what's
         *   really called, but that method will call this method explicitly.)
         *   By default, we do nothing.  Since this runs on our turn, it's a
         *   good place to put any scripted behavior we perform on arriving at
         *   our new destination after the group travel.
         */
        arrivingTurn() { }

        /*
         *   For our TravelMessageHandler implementation, the nominal traveler
         *   is our actor.  Note that this is all we need to implement for
         *   travel message handling, since we simply inherit the default
         *   handling for all of the arrival/departure messages.
         */
        getNominalTraveler() { return getActor(); }
    ;

    /*
     *   A "ready for conversation" state.  This can be used as the base class
     *   for actor states when the actor is receptive to conversation, and we
     *   want to have the sense of a conversational context.  The key feature
     *   that this class provides is the ability to provide messages when
     *   engaging and disengaging the conversation.
     *
     *   Note that this state is NOT required for conversation, since the basic
     *   ActorState object accepts conversational commands like ASK, TELL,
     *   GIVE, and TAKE.  The special feature of the "conversation ready" state
     *   is that we explicitly move the actor to a separate state when
     *   conversation begins.  This is especially appropriate for states in
     *   which the NPC is actively carrying on some other activity; the
     *   conversation should interrupt those states, so that the actor stops
     *   the other activity and gives us its full attention.
     *
     *   This type of state can be associated with its in-conversation state
     *   object in one of two ways.  First, the inConvState property can be
     *   explicitly set to point to the in-conversation state object.  Second,
     *   this object can be nested inside its in-conversation state object via
     *   the 'location' property (so you can use the '+' syntax to put this
     *   object inside its in-conversation state object).  The 'ready' object
     *   goes inside the 'conversing' object because a single 'conversing'
     *   object can frequently be shared among several 'ready' states.
     */
    class ConversationReadyState: ActorState
        /*
         *   The associated in-conversation state.  This should be set to an
         *   InConversationState object that controls the actor's behavior
         *   while carrying on a conversation.  Note that the library will
         *   automatically set this if the instance is nested (via its
         *   'location' property) inside an InConversationState object.
         */
        inConvState = nil

        /* my implied conversational state is my in-conversation state */
        getImpliedConvState = (inConvState)

        /*
         *   Show our greeting message.  If 'explicit' is true, it means that
         *   the player character is greeting us through an explicit greeting
         *   command, such as HELLO or TALK TO.  Otherwise, the greeting is
         *   implied by some other conversational action, such a ASK ABOUT or
         *   SHOW TO.  We do nothing by default; this should be overridden in
         *   most cases to show some sort of exchange of pleasantries -
         *   something like this:
         *
         *.  >bob, hello
         *.  "Hi, there," you say.
         *
         *   Bob looks up over his newspaper.  "Oh, hello," he says, putting
         *   down the paper.  "What can I do for you?"
         *
         *   Note that games shouldn't usually override this method.  Instead,
         *   you should simply create a HelloTopic entry and put it inside the
         *   state object; we'll find the HelloTopic and show its message as
         *   our greeting.
         *
         *   If you want to distinguish between explicit and implicit
         *   greetings, you can create an ImpHelloTopic entry for implied
         *   greetings (i.e., the kind of greeting that occurs automatically
         *   when the player jumps right into a conversation with our actor
         *   using ASK ABOUT or the like, without explicitly saying HELLO
         *   first).  The regular HelloTopic will handle explicit greetings,
         *   and the ImpHelloTopic will handle the implied kind.
         */
        showGreetingMsg(actor, explicit)
        {
            /* look for a HelloTopic in our topic database */
            if (handleTopic(actor, explicit ? helloTopicObj : impHelloTopicObj,
                            helloConvType, nil))
                "<.p>";
        }

        /*
         *   Enter this state from a conversation.  This should show any
         *   message we want to display when we're ending a conversation and
         *   switching from the conversation to this state.  'reason' is the
         *   endConvXxx enum indicating what triggered the termination of the
         *   conversation.  'oldNode' is the ConvNode we were in just before we
         *   initiated the termination - we need this information because we
         *   want to look in the ConvNode for a Bye topic message to display,
         *   but we can't just look in the actor for the node because it will
         *   already have been cleared out by the time we get here.
         *
         *   Games shouldn't normally override this method.  Instead, simply
         *   create a ByeTopic entry and put it inside the state object; we'll
         *   find the ByeTopic and show its message for the goodbye.
         *
         *   If you want to distinguish between different types of goodbyes,
         *   you can create an ImpByeTopic for any implied goodbye (i.e., the
         *   kind where the other actor just walks away, or where we get bored
         *   of the other actor ignoring us).  You can also further
         *   differentiate by creating BoredByeTopic and/or LeaveByeTopic
         *   objects to handle just those cases.  The regular ByeTopic will
         *   handle explicit GOODBYE commands, and the others (ImpByeTopic,
         *   BoredByeTopic, LeaveByeTopic) will handle the implied kinds.
         */
        enterFromConversation(actor, reason, oldNode)
        {
            local topic;
            local reasonMap = [endConvBye, byeTopicObj,
                               endConvTravel, leaveByeTopicObj,
                               endConvBoredom, boredByeTopicObj,
                               endConvActor, actorByeTopicObj];

            /* figure out which topic object we need, based on the reason code */
            topic = reasonMap[reasonMap.indexOf(reason) + 1];

            /*
             *   Look for a ByeTopic in the ConvNode; failing that, try our own
             *   database.
             */
            if (oldNode == nil
                || !oldNode.handleConversation(actor, topic, byeConvType, nil))
            {
                /* there's no node handler; try our own database */
                handleTopic(actor, topic, byeConvType, nil);
            }
        }

        /* handle a conversational action directed to our actor */
        handleConversation(otherActor, topic, convType)
        {
            /*
             *   If this is a greeting, handle it ourselves.  Otherwise, pass
             *   it along to our associated in-conversation state.
             */
            if (convType == helloConvType)
            {
                /*
                 *   Switch to our associated in-conversation state and show a
                 *   greeting.  Since we're explicitly entering the
                 *   conversation, we have no topic entry.
                 */
                enterConversation(otherActor, nil);

                /* show or schedule a topic inventory, as appropriate */
                conversationManager.showOrScheduleTopicInventory(
                    getActor(), otherActor);
            }
            else
            {
                /*
                 *   it's not a greeting, so pass it to our in-conversation
                 *   state for handling
                 */
                inConvState.handleConversation(otherActor, topic, convType);
            }
        }

        /*
         *   Initiate conversation based on the given simulation object.  This
         *   is an internal method that isn't usually called directly from game
         *   code; game code usually calls the Actor's initiateTopic(), which
         *   calls this routine to check for a topic that's part of the state
         *   object.
         */
        initiateTopic(obj)
        {
            /* defer to our in-conversation state */
            return inConvState.initiateTopic(obj);
        }

        /*
         *   Receive notification that a TopicEntry is being used (via its
         *   handleTopic method) to respond to a command.  If the TopicEntry is
         *   conversational, automatically enter our in-conversation state.
         */
        notifyTopicResponse(fromActor, entry)
        {
            if (entry.isConversational)
                enterConversation(fromActor, entry);
        }

        /*
         *   Enter a conversation with the given actor, either explicitly (via
         *   HELLO or TALK TO) or implicitly (by directly asking a question,
         *   etc).  'entry' gives the TopicEntry that's triggering the implicit
         *   conversation entry; if this is nil, it means that we're being
         *   triggered explicitly.
         */
        enterConversation(actor, entry)
        {
            local myActor = getActor();
            local explicit = (entry == nil);

            /* if the actor can't talk to us, we can't enter the conversation */
            if (!actor.canTalkTo(myActor))
            {
                /* tell them we can't talk now */
                reportFailure(&objCannotHearActorMsg, myActor);

                /* terminate the command */
                exit;
            }

            /*
             *   Show our greeting, if desired.  We show a greeting if we're
             *   being invoked explicitly (that is, there's no TopicEntry), or
             *   if we're being invoked explicitly and the TopicEntry implies a
             *   greeting.
             */
            if (explicit || entry.impliesGreeting)
                showGreetingMsg(actor, explicit);

            /* activate the in-conversation state */
            myActor.setCurState(inConvState);
        }

        /*
         *   Get this state's suggested topic list.  ConversationReady states
         *   shouldn't normally have topic entries of their own, since a
         *   ConvversationReady state usually forwards conversation handling
         *   to its corresponding in-conversation state.  So, simply return
         *   the suggestion list from our in-conversation state object.
         */
        stateSuggestedTopics = (inConvState.suggestedTopics)

        /* initialize the actor state object */
        initializeActorState()
        {
            /* inherit the default handling */
            inherited();

            /*
             *   if we're nested inside an in-conversation state object, the
             *   containing in-conversation state is the one we'll use for
             *   conversations
             */
            if (location.ofKind(InConversationState))
                inConvState = location;
        }
    ;

    /*
     *   The "in-conversation" state.  This works with ConversationReadyState
     *   to handle transitions in and out of conversations.  In this state, we
     *   are actively engaged in a conversation.
     *
     *   Throughout this implementation, we assume that we only care about
     *   conversations with a single character, specifically the player
     *   character.  There's generally no good reason to fully model
     *   conversations between NPC's, since that kind of NPC activity is in
     *   most cases purely pre-scripted and thus requires no special state
     *   tracking.  Since we generally only need to worry about tracking a
     *   conversation with the player character, we don't bother with the
     *   possibility that we're simultaneously in conversation with more than
     *   one other character.
     */
    class InConversationState: ActorState
        /*
         *   Our attention span, in turns.  This is the number of turns that
         *   we'll be willing to stay in the conversation while the other
         *   character is ignoring us.  After the conversation has been idle
         *   this long, we'll assume the other actor is no longer talking to
         *   us, so we'll terminate the conversation ourselves.
         *
         *   If the NPC's doesn't have a limited attention span, set this
         *   property to nil.  This will prevent the NPC from ever disengaging
         *   of its own volition.
         */
        attentionSpan = 4

        /*
         *   The state to switch to when the conversation ends.  Instances can
         *   override this to select the next state.  By default, we'll return
         *   to the state that we were in immediately before the conversation
         *   started.
         */
        nextState = (previousState)

        /*
         *   End the current conversation.  'reason' indicates why we're
         *   leaving the conversation - this is one of the endConvXxx enums
         *   defined in adv3.h.
         *
         *   This method is a convenience only; you aren't required to call
         *   this method to end the conversation, since you can simply switch
         *   to another actor state directly if you prefer.  This method's
         *   main purpose is to display an appropriate message terminating the
         *   conversation while switching to the new state.  If you want to
         *   display your own message directly from the code that's changing
         *   the state, there's no reason to call this.
         *
         *   This returns true if we wish to allow the conversation to end,
         *   nil if not.
         */
        endConversation(actor, reason)
        {
            local nxt;
            local myActor = getActor();

            /*
             *   note the current ConvNode for our actor - when we check with
             *   the ConvNode to see about ending the conversation, this will
             *   automatically exit the ConvNode, so we need to save this first
             *   so that we can refer to it later to check for a Bye topic
             */
            local oldNode = myActor.curConvNode;

            /*
             *   Inherit the base behavior first - if it disallows the action,
             *   return failure.  The inherited version will check with the
             *   current ConvNode to see if has any objection.
             */
            if (!inherited(actor, reason))
                return nil;

            /* get the next state */
            nxt = nextState;

            /* if there isn't one, stay in the actor's current state */
            if (nxt == nil)
                nxt = myActor.curState;

            /*
             *   If the next state is a 'conversation ready' state, tell it
             *   we're entering from a conversation.  We're ending the
             *   conversation explicitly only if 'reason' is endConvBye.  Pass
             *   along the ConvNode we just exited (if any), so that we can
             *   look for a response in the node.
             */
            if (nxt.ofKind(ConversationReadyState))
                nxt.enterFromConversation(actor, reason, oldNode);

            /* switch our actor to the next state */
            myActor.setCurState(nxt);

            /* indicate that we are allowing the conversation to end */
            return true;
        }

        /*  handle a conversational command */
        handleConversation(otherActor, topic, convType)
        {
            /* handle goodbyes specially */
            if (convType == byeConvType)
            {
                /*
                 *   If this is an implicit goodbye, run the normal
                 *   conversation handling in order to display any implied
                 *   ByeTopic message - but capture the output in case we
                 *   decide not to end the conversation after all.  Only do
                 *   this in the case of an implicit goodbye, though - for an
                 *   explicit goodbye, there's no need for this as the explicit
                 *   BYE will do the same thing on its own.
                 */
                local txt = nil;
                if (topic != byeTopicObj)
                {
                    txt = mainOutputStream.captureOutput(
                        {: inherited(otherActor, topic, convType) });
                }

                /*
                 *   try to end the conversation; if we won't allow it,
                 *   terminate the action here
                 */
                if (!endConversation(otherActor, endConvBye))
                    exit;

                /* show the captured ByeTopic output */
                if (txt != nil)
                    say(txt);
            }
            else
            {
                /* use the inherited handling */
                inherited(otherActor, topic, convType);
            }
        }

        /*
         *   provide a default HELLO response, if we don't have a special
         *   TopicEntry for it
         */
        defaultGreetingResponse(actor)
        {
            /*
             *   As our default response, point out that we're already at the
             *   actor's service.  (This isn't an error, because the other
             *   actor might not have been talking to us, even though we
             *   thought we were talking to them.)
             */
            gLibMessages.alreadyTalkingTo(getActor(), actor);
        }

        takeTurn()
        {
            local actor = getActor();

            /* if we didn't interact this turn, increment our boredom counter */
            if (!actor.conversedThisTurn())
                actor.boredomCount++;

            /* run the inherited handling */
            inherited();
        }

        /* activate this state */
        activateState(actor, oldState)
        {
            /*
             *   If the previous state was a ConversationReadyState, or we
             *   have no other state remembered, remember the previous state -
             *   this is the default we'll return to at the end of the
             *   conversation, if the instance doesn't specify another state.
             *
             *   We don't remember prior states that aren't conv-ready states
             *   to make it easier to temporarily interrupt a conversation
             *   with some other state, and later return to the conversation.
             *   If we remembered every prior state, then we'd return to the
             *   interrupting state when the conversation ended, which is
             *   usually not what's wanted.  Usually, we want to return to the
             *   last conv-ready state when a conversation ends, ignoring any
             *   other intermediate states that have been active since the
             *   conv-ready state was last in effect.
             */
            if (previousState == nil || oldState.ofKind(ConversationReadyState))
                previousState = oldState;

            /*
             *   reset the actor's boredom counter, since we're just starting a
             *   new conversation, and add our boredom agenda item to the
             *   active list to monitor our boredom level
             */
            actor.boredomCount = 0;
            actor.addToAgenda(actor.boredomAgendaItem);

            /* remember the time of the last conversation command */
            actor.lastConvTime = Schedulable.gameClockTime;
        }

        /* deactivate this state */
        deactivateState(actor, newState)
        {
            /*
             *   we're leaving the conversation state, so there's no need to
             *   monitor our boredom level any longer
             */
            actor.removeFromAgenda(actor.boredomAgendaItem);

            /* do the normal work */
            inherited(actor, newState);
        }

        /*
         *   The previous state - this is the state we were in before the
         *   conversation began, and the one we'll return to by default when
         *   the conversation ends.  We'll set this automatically on
         *   activation.
         */
        previousState = nil
    ;

    /*
     *   A special kind of agenda item for monitoring "boredom" during a
     *   conversation.  We check to see if our actor is in a conversation, and
     *   the PC has been ignoring the conversation for too long; if so, our
     *   actor initiates the end of the conversation, since the PC apparently
     *   isn't paying any attention to us.
     */
    class BoredomAgendaItem: AgendaItem
        /* we construct these dynamically during actor initialization */
        construct(actor)
        {
            /* remember our actor as our location */
            location = actor;
        }

        /*
         *   we're ready to run if our actor is in an InConversationState and
         *   its boredom count has reached the limit for the state
         */
        isReady()
        {
            local actor = getActor();
            local state = actor.curState;

            return (inherited()
                    && state.ofKind(InConversationState)
                    && state.attentionSpan != nil
                    && actor.boredomCount >= state.attentionSpan);
        }

        /* on invocation, end the conversation */
        invokeItem()
        {
            local actor = getActor();
            local state = actor.curState;

            /* tell the state to end the conversation */
            state.endConversation(actor.getCurrentInterlocutor(), endConvBoredom);
        }

        /*
         *   by default, handle boredom before other agenda items - we do this
         *   because an ongoing conversation will be the first thing on the
         *   NPC's mind
         */
        agendaOrder = 50
    ;

    /*
     *   A "hermit" actor state is a state where the actor is unresponsive to
     *   conversational overtures (ASK ABOUT, TELL ABOUT, HELLO, GOODBYE, YES,
     *   NO, SHOW TO, GIVE TO, and any orders directed to the actor).  Any
     *   attempt at conversation will be met with the 'noResponse' message.
     */
    class HermitActorState: ActorState
        /*
         *   Show our response to any conversational command.  We'll simply
         *   show the standard "there's no response" message by default, but
         *   subclasses can (and usually should) override this to explain
         *   what's really going on.  Note that this routine will be invoked
         *   for any sort of conversation command, so any override needs to be
         *   generic enough that it's equally good for ASK, TELL, and
         *   everything else.
         *
         *   Note that it's fairly easy to create a shuffled list of random
         *   messages, if you want to add some variety to the actor's
         *   responses.  To do this, use an embedded ShuffledEventList:
         *
         *   myState: HermitActorState
         *.    noResponse() { myList.doScript(); }
         *.    myList: ShuffledEventList {
         *.      ['message1', 'message2', 'message3'] }
         *.  ;
         */
        noResponse() { mainReport(&noResponseFromMsg, getActor()); }

        /* all conversation actions get the same default response */
        handleConversation(otherActor, topic, convType)
        {
            /* just show our standard default response */
            noResponse();
        }

        /*
         *   Since the hermit state blocks topics from outside the state, don't
         *   offer suggestions for other topics while in this state.
         *
         *   Note that you might sometimes want to override this to allow the
         *   usual topic suggestions (by setting this to nil).  In particular:
         *
         *   - If it's not outwardly obvious that the actor is unresponsive,
         *   you'll probably want to allow suggestions.  Remember, TOPICS
         *   suggests topics that the *PC* wants to talk about, not things the
         *   NPC is interested in.  If the PC doesn't necessarily know that the
         *   NPC won't respond, the PC would still want to ask about those
         *   topics.
         *
         *   - If the hermit state is to be short-lived, you might want to show
         *   the topic suggestions even in the hermit state, so that the player
         *   is aware that there are still useful topics to explore with the
         *   NPC.  The player might otherwise assume that the NPC is out of
         *   useful topics, and not bother trying again later when the NPC
         *   becomes more responsive.
         */
        limitSuggestions = true
    ;

    /*
     *   The basic "accompanying" state.  In this state, whenever the actor
     *   we're accompanying travels to a location we want to follow, we'll
     *   travel at the same time with the other actor.
     */
    class AccompanyingState: ActorState
        /*
         *   Check to see if we are to accompany the given traveler on the
         *   given travel.  'traveler' is the Traveler performing the travel,
         *   and 'conn' is the connector that the traveler is about to take.
         *
         *   Note that 'traveler' is a Traveler object.  This will simply be an
         *   Actor (which is a kind of Traveler) when the actor is performing
         *   the travel directly, but it could also be another kind of
         *   Traveler, such as a Vehicle.  This routine must determine whether
         *   to accompany other kinds of actors.
         *
         *   By default, we'll return true to indicate that we want to
         *   accompany any traveler anywhere they go.  This should almost
         *   always be overridden in practice to be more specific.
         */
        accompanyTravel(traveler, conn) { return true; }

        /*
         *   Get our accompanying state object.  We'll create a basic
         *   accompanying in-travel state object, returning to the current
         *   state when we're done.  'traveler' is the Traveler object that's
         *   performing the travel; this might be an Actor, but could also be a
         *   Vehicle or other Traveler subclass.
         */
        getAccompanyingTravelState(traveler, connector)
        {
            /*
             *   Create the default intermediate state for the travel.  Note
             *   that the lead actor is the actor performing the command - this
             *   won't necessarily be the traveler, since the actor could be
             *   steering a vehicle.
             */
            return new AccompanyingInTravelState(
                getActor(), gActor, getActor().curState);
        }

        /*
         *   handle a before-travel notification for my actor
         */
        beforeTravel(traveler, connector)
        {
            /*
             *   If we want to accompany the given traveler on this travel, add
             *   ourselves to the initiating actor's list of accompanying
             *   actors.  Never set an actor to accompany itself, since doing
             *   so would lead to infinite recursion.
             */
            if (accompanyTravel(traveler, connector) && getActor() != gActor)
            {
                /*
                 *   Add me to the list of actors accompanying the actor
                 *   initiating the travel - that actor will run a nested
                 *   travel action on us before doing its own travel.  Note
                 *   that the initiating actor is gActor, since that's the
                 *   actor performing the action that led to the travel.
                 */
                gActor.addAccompanyingActor(getActor());

                /* put my actor into the appropriate new group travel state */
                getActor().setCurState(
                    getAccompanyingTravelState(traveler, connector));
            }

            /* inherit the default handling */
            inherited(traveler, connector);
        }
    ;

    /*
     *   "Accompanying in-travel" state - this is an actor state used when an
     *   actor is taking part in a group travel operation.  This state lasts
     *   only as long as the single turn - which belongs to the lead actor -
     *   that it takes to carry out the group travel.  Once our turn comes
     *   around, we'll restore the actor to the previous state - or, we can set
     *   the actor to a different state, if desired.  Setting the actor to a
     *   different state is useful when the group travel triggers a new
     *   scripted activity in the new room.
     */
    class AccompanyingInTravelState: ActorState
        construct(actor, lead, next)
        {
            /* do the normal initialization */
            inherited(actor);

            /* remember the lead actor and the next state */
            leadActor = lead;
            nextState = next;
        }

        /* the lead actor of the group travel */
        leadActor = nil

        /*
         *   the next state - we'll switch our actor to this state after the
         *   travel has been completed
         */
        nextState = nil

        /*
         *   Show our "I am here" description.  By default, we'll use the
         *   arrivingWithDesc of the *next* state object.
         */
        specialDesc() { nextState.arrivingWithDesc; }

        /* take our turn */
        takeTurn()
        {
            /*
             *   The group travel only takes the single turn in which the
             *   travel is initiated, so by the time our turn comes around, the
             *   group travel is done.  Clear out the lead actor's linkage to
             *   us as an accompanying actor.
             */
            leadActor.accompanyingActors.removeElement(getActor());

            /* switch our actor to the next state */
            getActor().setCurState(nextState);

            /*
             *   call our next state's on-arrival turn-taking method, so that
             *   it can carry out any desired scripted behavior for our arrival
             */
            nextState.arrivingTurn();
        }

        /* initiate a topic - defer to the next state */
        initiateTopic(obj) { return nextState.initiateTopic(obj); }

        /*
         *   Override our departure messages.  When we're accompanying another
         *   actor on a group travel, the lead actor will, as part of its turn,
         *   send each accompanying actor (including us) on ahead.  This means
         *   that the lead actor will see us departing from the starting
         *   location, because we'll leave before the lead actor has itself
         *   departed.  Rather than using the normal "Bob leaves to the west"
         *   departure report, customize the departure reports to indicate
         *   specifically that we're going with the lead actor.  (Note that we
         *   only have to handle the departing messages, since group travel
         *   always sends accompanying actors on ahead of the main actor, hence
         *   the accompanying actors will always be seen departing, not
         *   arriving.)
         *
         *   Note that all of these call our generic sayDeparting() method by
         *   default, so a subclass can catch all of the departure types at
         *   once just by overriding sayDeparting().  Overriding the individual
         *   methods is still desirable, of course, if you want separate
         *   messages for the different departure types.
         */
        sayDeparting(conn)
            { gLibMessages.sayDepartingWith(getActor(), leadActor); }
        sayDepartingDir(dir, conn) { sayDeparting(conn); }
        sayDepartingThroughPassage(conn) { sayDeparting(conn); }
        sayDepartingViaPath(conn) { sayDeparting(conn); }
        sayDepartingUpStairs(conn) { sayDeparting(conn); }
        sayDepartingDownStairs(conn) { sayDeparting(conn); }

        /*
         *   Describe local travel using our standard departure message as
         *   well.  This is used to describe our travel when our origin and
         *   destination locations are both visible to the PC; in these cases,
         *   we don't describe the departure separately because the whole
         *   process of travel from departure to arrival is visible to the PC
         *   and thus is best handled with a single message, which we generate
         *   here.  In our case, since the "accompanying" state describes even
         *   normal travel as though it were visible all along, we can use our
         *   standard "departing" message to describe local travel as well.
         */
        sayArrivingLocally(dest, conn) { sayDeparting(conn); }
        sayDepartingLocally(dest, conn) { sayDeparting(conn); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A pending conversation information object.  An Actor keeps a list of
     *   these for pending conversations.
     */
    class PendingConvInfo: object
        construct(state, node, turns)
        {
            /* remember how to start the conversation */
            state_ = state;
            node_ = node;

            /* compute the game clock time when we can start the conversation */
            time_ = Schedulable.gameClockTime + turns;
        }

        /*
         *   our ActorState and ConvNode (or ConvNode name string), describing
         *   how we're to start the conversation
         */
        state_ = nil
        node_ = nil

        /* the minimum game clock time at which we can start the conversation */
        time_ = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An "agenda item."  Each actor can have its own "agenda," which is a
     *   list of these items.  Each item represents an action that the actor
     *   wants to perform - this is usually a goal the actor wants to achieve,
     *   or a conversational topic the actor wants to pursue.
     *
     *   On any given turn, an actor can carry out only one agenda item.
     *
     *   Agenda items are a convenient way of controlling complex behavior.
     *   Each agenda item defines its own condition for when the actor can
     *   pursue the item, and each item defines what the actor does when
     *   pursuing the item.  Agenda items can improve the code structure for an
     *   NPC's behavior, since they nicely isolate a single background action
     *   and group it with the conditions that trigger it.  But the main
     *   benefit of agenda items is the one-per-turn pacing - by executing at
     *   most one agenda item per turn, we ensure that the NPC will carry out
     *   its self-initiated actions at a measured pace, rather than as a jumble
     *   of random actions on a single turn.
     *
     *   Note that NPC-initiated conversation messages override agendas.  If an
     *   actor has an active ConvNode, AND the ConvNode displays a
     *   "continuation message" on a given turn, then the actor will not pursue
     *   its agenda on that turn.  In this way, ConvNode continuation messages
     *   act rather like high-priority agenda items.
     */
    class AgendaItem: object
        /*
         *   My actor - agenda items should be nested within the actor using
         *   '+' so that we can find our actor.  Note that this doesn't add the
         *   item to the actor's agenda - that has to be done explicitly with
         *   actor.addToAgenda().
         */
        getActor() { return location; }

        /*
         *   Is this item active at the start of the game?  Override this to
         *   true to make the item initially active; we'll add it to the
         *   actor's agenda during the game's initialization.
         */
        initiallyActive = nil

        /*
         *   Is this item ready to execute?  The actor will only execute an
         *   agenda item when this condition is met.  By default, we're ready
         *   to execute.  Items can override this to provide a declarative
         *   condition of readiness if desired.
         */
        isReady = true

        /*
         *   Is this item done?  On each turn, we'll remove any items marked as
         *   done from the actor's agenda list.  We remove items marked as done
         *   before executing any items, so done-ness overrides readiness; in
         *   other words, if an item is both 'done' and 'ready', it'll simply
         *   be removed from the list and will not be executed.
         *
         *   By default, we simply return nil.  Items can override this to
         *   provide a declarative condition of done-ness, or they can simply
         *   set the property to true when they finish their work.  For
         *   example, an item that only needs to execute once can simply set
         *   isDone to true in its invokeItem() method; an item that's to be
         *   repeated until some success condition obtains can override isDone
         *   to return the success condition.
         */
        isDone = nil

        /*
         *   The ordering of the item relative to other agenda items.  When we
         *   choose an agenda item to execute, we always choose the lowest
         *   numbered item that's ready to run.  You can leave this with the
         *   default value if you don't care about the order.
         */
        agendaOrder = 100

        /*
         *   Execute this item.  This is invoked during the actor's turn when
         *   the item is the first item that's ready to execute in the actor's
         *   agenda list.  We do nothing by default.
         */
        invokeItem() { }

        /*
         *   Reset the item.  This is invoked whenever the item is added to an
         *   actor's agenda.  By default, we'll set isDone to nil as long as
         *   isDone isn't a method; this makes it easier to reuse agenda
         *   items, since we don't have to worry about clearing out the isDone
         *   flag when reusing an item.
         */
        resetItem()
        {
            /* if isDone isn't a method, reset it to nil */
            if (propType(&isDone) != TypeCode)
                isDone = nil;
        }
    ;

    /*
     *   An AgendaItem initializer.  For each agenda item that's initially
     *   active, we'll add the item to its actor's agenda.
     */
    PreinitObject
        execute()
        {
            forEachInstance(AgendaItem, function(item) {
                /*
                 *   If this item is initially active, add the item to its
                 *   actor's agenda.
                 */
                if (item.initiallyActive)
                    item.getActor().addToAgenda(item);
            });
        }
    ;

    /*
     *   A "conversational" agenda item.  This type of item is ready to execute
     *   only when the actor hasn't engaged in conversation during the same
     *   turn.  This type of item is ideal for situations where we want the
     *   actor to pursue a conversational topic, because we won't initiate the
     *   action until we get a turn where the player didn't directly talk to
     *   us.
     */
    class ConvAgendaItem: AgendaItem
        isReady = (!getActor().conversedThisTurn()
                   && getActor().canTalkTo(otherActor)
                   && inherited())

        /*
         *   The actor we're planning to address - by default, this is the PC.
         *   If the conversational overture will be directed to another NPC,
         *   you can specify that other actor here.
         */
        otherActor = (gPlayerChar)
    ;

    /*
     *   A delayed agenda item.  This type of item becomes ready to execute
     *   when the game clock reaches a given turn counter.
     */
    class DelayedAgendaItem: AgendaItem
        /* we're ready if the game clock time has reached our ready time */
        isReady = (Schedulable.gameClockTime >= readyTime && inherited())

        /* the turn counter on the game clock when we become ready */
        readyTime = 0

        /*
         *   Set our ready time based on a delay from the current time.  We'll
         *   become ready after the given number of turns elapses.  For
         *   convenience, we return 'self', so a delayed agenda item can be
         *   initialized and added to an actor's agenda in one simple
         *   operation, like so:
         *
         *   actor.addToAgenda(item.setDelay(1));
         */
        setDelay(turns)
        {
            /*
             *   initialize our ready time as the given number of turns in the
             *   future from the current game clock time
             */
            readyTime = Schedulable.gameClockTime + turns;

            /* return 'self' for the caller's convenience */
            return self;
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An Actor is a living person, animal, or other entity with a will of
     *   its own.  Actors can usually be addressed with targeted commands
     *   ("bob, go north"), and with commands like ASK ABOUT, TELL ABOUT, GIVE
     *   TO, and SHOW TO.
     *
     *   Note that, by default, an Actor can be picked up and moved with
     *   commands like TAKE, PUT IN, and so on.  This is suitable for some
     *   kinds of actors but not for others: it might make sense with a cat or
     *   a small dog, but not with a bank guard or an orc.  For an actor that
     *   can't be taken, use the UntakeableActor or one of its subclasses.
     *
     *   An actor's contents are the things the actor is carrying or wearing.
     */
    class Actor: Thing, Schedulable, Traveler, ActorTopicDatabase
        /* flag: we're an actor */
        isActor = true

        /*
         *   Our current state.  This is an ActorState object representing what
         *   we're currently doing.  Whenever the actor changes to a new state
         *   (for example, because of a scripted activity), this can be changed
         *   to reflect the actor's new state.  The state object groups the
         *   parts of the actor's description and other methods that tend to
         *   vary according to what the actor's doing; it's easier to keep
         *   everything related to scripted activities together in a state
         *   object than it is to handle all of the variability with switch()
         *   statements of the like in methods directly in the actor.
         *
         *   It's not necessary to initialize this if the actor doesn't take
         *   advantage of the ActorState mechanism.  If this isn't initialized
         *   for a particular actor, we'll automatically create a default
         *   ActorState object during pre-initialization.
         */
        curState = nil

        /* set the current state */
        setCurState(state)
        {
            /* if this isn't a change of state, there's nothing to do */
            if (state == curState)
                return;

            /* if we have a previous state, tell it it's becoming inactive */
            if (curState != nil)
                curState.deactivateState(self, state);

            /* notify the new state it's becoming active */
            if (state != nil)
                state.activateState(self, curState);

            /* remember the new state */
            curState = state;
        }

        /*
         *   Our current conversation node.  This is a ConvNode object that
         *   keeps track of the flow of the conversation.
         */
        curConvNode = nil

        /*
         *   Our table of conversation nodes.  At initialization, the
         *   conversation manager scans all ConvNode instances and adds each
         *   one to its actor's table.  This table is keyed by the name of
         *   node, and the value for each entry is the ConvNode object - this
         *   lets us look up the ConvNode object by name.  Because each actor
         *   has its own lookup table, ConvNode names only have to be unique
         *   within the actor's set of ConvNodes.
         */
        convNodeTab = perInstance(new LookupTable(32, 32))

        /* set the current conversation node */
        setConvNode(node) { setConvNodeReason(node, nil); }

        /* set the current conversation node, with a reason code */
        setConvNodeReason(node, reason)
        {
            /* remember the old node */
            local oldNode = curConvNode;

            /* if the node was specified by name, look up the object */
            if (dataType(node) == TypeSString)
                node = convNodeTab[node];

            /* remember the new node */
            curConvNode = node;

            /*
             *   If we're changing to a new node, notify the new and old
             *   nodes.  Note that these notifications occur after the new
             *   node has been set, which ensures that any further node change
             *   triggered by the node change won't redundantly issue the same
             *   notifications: since the old node is no longer active, it
             *   can't receive another departure notification, and since the
             *   new node is already active, it can't receive another
             *   activation.
             */
            if (node != oldNode)
            {
                /* if there's an old node, note that we're leaving it */
                if (oldNode != nil)
                    oldNode.noteLeaving();

                /* let the node know that it's becoming active */
                if (node != nil)
                    node.noteActiveReason(reason);
            }

            /*
             *   note that we've explicitly set a ConvNode (even if it's not
             *   actually changing), in case the conversation manager is
             *   tracking what's happening during a response
             */
            responseSetConvNode = true;
        }

        /*
         *   conversation manager ID - this is assigned by the conversation
         *   manager to map to and from output stream references to the actor;
         *   this is only for internal use by the conversation manager
         */
        convMgrID = nil

        /*
         *   Flag indicating whether or not we've set a ConvNode in the course
         *   of the current response.  This is for use by the converstaion
         *   manager.
         */
        responseSetConvNode = nil

        /*
         *   Initiate a conversation with the player character.  This lets the
         *   NPC initiate a conversation, in response to something the player
         *   character does, or as part of the NPC's scripted activity.  This
         *   is only be used for situations where the NPC initiates the
         *   conversation - if the player character initiates conversation with
         *   TALK TO, ASK, TELL, etc., we handle the conversation through our
         *   normal handlers for those commands.
         *
         *   'state' is the ActorState to switch to for the conversation.  This
         *   will normally be an InConversationState object, but doesn't have
         *   to be.
         *
         *   You can pass nil for 'state' to use the current state's implied
         *   conversational state.  The implied conversational state of a
         *   ConversationReadyState is the associated InConversationState; the
         *   implied conversation state of any other state is simply the same
         *   state.
         *
         *   'node' is a ConvNode object, or a string naming a ConvNode object.
         *   We'll make this our current conversation node.  A valid
         *   conversation node is required because we use this to generate the
         *   initial NPC greeting of the conversation.  In most cases, when the
         *   NPC initiates a conversation, it's because the NPC wants to ask a
         *   question or otherwise say something specific, so there should
         *   always be a conversational context implied, thus the need for a
         *   ConvNode.  If there's no need for a conversational context, the
         *   NPC script code might just as well display the conversational
         *   exchange as a plain old message, and not bother going to all this
         *   trouble.
         */
        initiateConversation(state, node)
        {
            /*
             *   if there's no state provided, use the current state's implied
             *   conversation state
             */
            if (state == nil)
                state = curState.getImpliedConvState;

            /*
             *   if there's an ActorHelloTopic for the old state, invoke it to
             *   show the greeting
             */
            curState.handleTopic(self, actorHelloTopicObj, helloConvType, nil);

            /* switch to the new state, if it's not the current state */
            if (state != nil && state != curState)
                setCurState(state);

            /* we're now talking to the player character */
            noteConversation(gPlayerChar);

            /* switch to the conversation node */
            setConvNodeReason(node, 'initiateConversation');

            /* tell the conversation node that the NPC is initiating it */
            if (node != nil)
                curConvNode.npcInitiateConversation();
        }

        /*
         *   Initiate a conversation based on the given simulation object.
         *   We'll look for an InitiateTopic matching the given object, and if
         *   we can find one, we'll show its topic response.
         */
        initiateTopic(obj)
        {
            /* try our current state first */
            if (curState.initiateTopic(obj))
                return true;

            /* we didn't find a state object; use the default handling */
            return inherited(obj);
        }

        /*
         *   Schedule initiation of conversation.  This allows the caller to
         *   set up a conversation to start on a future turn.  The
         *   conversation will start after (1) the given number of turns has
         *   elapsed, and (2) the player didn't target this actor with a
         *   conversational command on the same turn.  This allows us to set
         *   the NPC so that it *wants* to start a conversation, and will do
         *   so as soon as it has a chance to get a word in.
         *
         *   If 'turns' is zero, the conversation can start the next time the
         *   actor takes a turn; so, if this is called during the PC's action
         *   processing, the conversation can start on the same turn.  Note
         *   that if this is called during the actor's takeTurn() processing,
         *   it won't actually start the conversation until the next turn,
         *   because that's the next time we'll check the queue.  If 'turns'
         *   is 1, then the player will get at least one more command before
         *   the conversation will begin, and so on with higher numbers.
         */
        scheduleInitiateConversation(state, node, turns)
        {
            /* add a new pending conversation to our list */
            pendingConv.append(new PendingConvInfo(state, node, turns));
        }

        /*
         *   Break off our current conversation, of the NPC's own volition.
         *   This is the opposite number of initiateConversation: this causes
         *   the NPC to effectively say BYE on its own, rather than waiting
         *   for the PC to decide to end the conversation.
         *
         *   This call is mostly useful when the actor's current state is an
         *   InConversationState, since the main function of this routine is
         *   to switch to an out-of-conversation state.
         */
        endConversation()
        {
            /*
             *   tell the current state to end the conversation of the NPC's
             *   own volition
             */
            curState.endConversation(self, endConvActor);
        }

        /*
         *   Our list of pending conversation initiators.  In our takeTurn()
         *   processing, we'll check this list for conversations that we can
         *   initiate.
         */
        pendingConv = nil

        /*
         *   Hide actors from 'all' by default.  The kinds of actions that
         *   normally apply to 'all' and the kinds that normally apply to
         *   actors have pretty low overlap.
         *
         *   If a particular actor looks a lot like an inanimate object, it
         *   might want to override this to participate in 'all' for most or
         *   all actions.
         */
        hideFromAll(action) { return true; }

        /*
         *   don't hide actors from defaulting, though - it's frequently
         *   convenient and appropriate to assume an actor by default,
         *   especially for commands like GIVE TO and SHOW TO
         */
        hideFromDefault(action) { return nil; }

        /*
         *   We meet the objHeld precondition for ourself - that is, for any
         *   verb that requires holding an object, we can be considered to be
         *   holding ourself.
         */
        meetsObjHeld(actor) { return actor == self || inherited(actor); }

        /*
         *   Actors are not listed with the ordinary objects in a room's
         *   description.  However, an actor is listed as part of an inventory
         *   description.
         */
        isListed = nil
        isListedInContents = nil
        isListedInInventory = true

        /* the contents of an actor aren't listed in a room's description */
        contentsListed = nil

        /*
         *   Full description.  By default, we'll show either the pcDesc or
         *   npcDesc, depending on whether we're the current player character
         *   or a non-player character.
         *
         *   Generally, individual actors should NOT override this method.
         *   Instead, customize pcDesc and/or npcDesc to describe the permanent
         *   features of the actor.
         */
        desc
        {
            /*
             *   show the appropriate messages, depending on whether we're the
             *   player character or a non-player character
             */
            if (isPlayerChar())
            {
                /* show our as-player-character description */
                pcDesc;
            }
            else
            {
                /* show our as-non-player-character description */
                npcDesc;
            }
        }

        /* show our status */
        examineStatus()
        {
            /*
             *   If I'm an NPC, show where I'm sitting/standing/etc.  (If I'm
             *   the PC, we don't usually want to show this explicitly to avoid
             *   redundancy.  The player is usually sufficiently aware of the
             *   PC's posture by virtue of being in control of the actor, and
             *   the information also tends to show up often enough in other
             *   places, such as on the status line and in the room
             *   description.)
             */
            if (!isPlayerChar())
                postureDesc;

            /* show the status from our state object */
            curState.stateDesc;

            /* inherit the default handling to show our contents */
            inherited();
        }

        /*
         *   Show my posture, as part of the full EXAMINE description of this
         *   actor.  We'll let our nominal actor container handle it.
         */
        postureDesc() { descViaActorContainer(&roomActorPostureDesc, nil); }

        /*
         *   The default description when we examine this actor and the actor
         *   is serving as the player character.  This should generally not
         *   include any temporary status information; just show constant,
         *   fixed features.
         */
        pcDesc { gLibMessages.pcDesc(self); }

        /*
         *   Show the description of this actor when this actor is a non-player
         *   character.
         *
         *   This description should include only the constant, fixed
         *   description of the character.  Do not include information on what
         *   the actor is doing right now, because that belongs in the
         *   ActorState object instead.  When we display the actor's
         *   description, we'll show this text, and then we'll show the
         *   ActorState description as well; this combination approach makes it
         *   easier to keep the description synchronized with any scripted
         *   activities the actor is performing.
         *
         *   By default, we'll show this as a "default descriptive report,"
         *   since it simply says that there's nothing special to say.
         *   However, whenever this is overridden with an actual description,
         *   you shouldn't bother to use defaultDescReport - simply display the
         *   descriptive message directly:
         *
         *   npcDesc = "He's wearing a gorilla costume. "
         */
        npcDesc { defaultDescReport(&npcDescMsg, self); }

        /* examine my contents specially */
        examineListContents()
        {
            /* if I'm not the player character, show my inventory */
            if (!isPlayerChar())
                holdingDesc;
        }

        /*
         *   Always list actors specially, rather than as ordinary items in
         *   contents listings.  We'll send this to our current state object
         *   for processing, since our "I am here" description tends to vary by
         *   state.
         */
        specialDesc() { curState.specialDesc(); }
        distantSpecialDesc() { curState.distantSpecialDesc(); }
        remoteSpecialDesc(actor) { curState.remoteSpecialDesc(actor); }
        specialDescListWith() { return curState.specialDescListWith(); }

        /*
         *   By default, show the special description for an actor in the group
         *   of special descriptions that come *after* the room's portable
         *   contents listing.  An actor's presence is usually a dynamic
         *   feature of a room, and so we don't want to suggest that the actor
         *   is a permanent feature of the room by describing the actor
         *   directly with the room's main description.
         */
        specialDescBeforeContents = nil

        /*
         *   When we're asked to show a special description as part of the
         *   description of a containing object (which will usually be a nested
         *   room of some kind), just show our posture in our container, rather
         *   than showing our full "I am here" description.
         */
        showSpecialDescInContents(actor, cont)
        {
            /* show our posture to indicate our container */
            listActorPosture(actor);
        }

        /*
         *   By default, put all of the actor special descriptions after the
         *   special descriptions of ordinary objects, by giving actors a
         *   higher listing order value.
         */
        specialDescOrder = 200

        /*
         *   Get my listing group for my special description as part of a room
         *   description.  By default, we'll let our immediate location decide
         *   how we're grouped.
         */
        actorListWith()
        {
            local group;

            /*
             *   if our special desc is overridden, don't use any grouping by
             *   default - this make a special description defined in the
             *   actor override any grouping we'd otherwise do
             */
            if (overrides(self, Actor, &specialDesc))
                return [];

            /* get the group for the posture */
            group = location.listWithActorIn(posture);

            /*
             *   if we have a group, return a list containing the group;
             *   otherwise return an empty list
             */
            return (group == nil ? [] : [group]);
        }

        /*
         *   Actor "I am here" description.  This is displayed as part of the
         *   description of a room - it describes the actor as being present in
         *   the room.  By default, we let the "nominal actor container"
         *   provide the description.
         */
        actorHereDesc { descViaActorContainer(&roomActorHereDesc, nil); }

        /*
         *   Actor's "I am over there" description.  This is displayed in the
         *   room description when the actor is visible, but is either in a
         *   separate top-level room or is at a distance.  By default, we let
         *   the "nominal actor container" provide the description.
         */
        actorThereDesc { descViaActorContainer(&roomActorThereDesc, nil); }

        /*
         *   Show our status, as an addendum to the given room's name (this is
         *   the room title, shown at the start of a room description and on
         *   the status line).  By default, we'll let our nominal actor
         *   container provide the status, to indicate when we're
         *   standing/sitting/lying in a nested room.
         *
         *   In concrete terms, this generally adds a message such as "(sitting
         *   on the chair)" to the name of a room if we're in a nested room
         *   within the room.  When we're standing in the main room, this
         *   generally adds nothing.
         *
         *   Note that we pass the room we're describing as the "container to
         *   ignore" parameter, because we don't want to say something like
         *   "Phone Booth (standing in the phone booth)" - that is, we don't
         *   want to mention the nominal container again if the nominal
         *   container is what we're naming in the first place.
         */
        actorRoomNameStatus(room)
            { descViaActorContainer(&roomActorStatus, room); }

        /*
         *   Describe the actor via the "nominal actor container."  The nominal
         *   container is determined by our direct location.
         *
         *   'contToIgnore' is a container to ignore.  If our nominal container
         *   is the same as this object, we'll generate a description without a
         *   mention of a container at all.
         *
         *   The reason we have the 'contToIgnore' parameter is that the caller
         *   might already have reported our general location, and now merely
         *   wants to add that we're standing or standing or whatever.  In
         *   these cases, if we were to say that we're sitting on or standing
         *   on that same object, it would be redundant information: "Bob is in
         *   the garden, sitting in the garden."  The 'contToIgnore' parameter
         *   tells us the object that the caller has already mentioned as our
         *   general location so that we don't re-report the same thing.  We
         *   need to know the actual object, rather than just the fact that the
         *   caller mentioned a general location, because our general location
         *   and the specific place we're standing or sitting or whatever might
         *   not be the same: "Bob is in the garden, sitting in the lawn
         *   chair."
         *
         */
        descViaActorContainer(prop, contToIgnore)
        {
            local pov;
            local cont;

            /* get our nominal container for our current posture */
            cont = location.getNominalActorContainer(posture);

            /* get the point of view, using the player character by default */
            if ((pov = getPOV()) == nil)
                pov = gPlayerChar;

            /*
             *   if we have a nominal container, and it's not the one to
             *   ignore, and the player character can see it, generate the
             *   description via the container; otherwise, use a generic
             *   library message that doesn't mention the container
             */
            if (cont not in (nil, contToIgnore) && pov.canSee(cont))
            {
                /* describe via the container */
                cont.(prop)(self);
            }
            else
            {
                /* use the generic library message */
                gLibMessages.(prop)(self);
            }
        }

        /*
         *   Describe my inventory as part of my description - this is only
         *   called when we examine an NPC.  If an NPC doesn't wish to have
         *   its inventory listed as part of its description, it can simply
         *   override this to do nothing.
         */
        holdingDesc
        {
            /*
             *   show our contents as for a normal "examine", but using the
             *   special contents lister for what an actor is holding
             */
            examineListContentsWith(holdingDescInventoryLister);
        }

        /*
         *   refer to the player character with my player character referral
         *   person, and refer to all other characters in the third person
         */
        referralPerson { return isPlayerChar() ? pcReferralPerson : ThirdPerson; }

        /* by default, refer to the player character in the second person */
        pcReferralPerson = SecondPerson

        /*
         *   The referral person of the current command targeting the actor.
         *   This is meaningful only when a command is being directed to this
         *   actor, and this actor is an NPC.
         *
         *   The referral person depends on the specifics of the language.  In
         *   English, a command like "bob, go north" is a second-person
         *   command, while "tell bob to go north" is a third-person command.
         *   The only reason this is important is in interpreting what "you"
         *   means if it's used as an object in the command.  "tell bob to hit
         *   you" probably means that Bob should hit the player character,
         *   while "bob, hit you" probably means that Bob should hit himself.
         */
        commandReferralPerson = nil

        /* determine if I'm the player character */
        isPlayerChar() { return libGlobal.playerChar == self; }

        /*
         *   Implicit command handling style for this actor.  There are two
         *   styles for handling implied commands: "player" and "NPC",
         *   indicated by the enum codes ModePlayer and ModeNPC, respectively.
         *
         *   In "player" mode, each implied command is announced with a
         *   description of the command to be performed; DEFAULT responses are
         *   suppressed; and failures are shown.  Furthermore, interactive
         *   requests for more information from the parser are allowed.
         *   Transcripts like this result:
         *
         *   >open door
         *.  (first opening the door)
         *.  (first unlocking the door)
         *.  What do you want to unlock it with?
         *
         *   In "NPC" mode, implied commands are treated as complete and
         *   separate commands.  They are not announced; default responses are
         *   shown; failures are NOT shown; and interactive requests for more
         *   information are not allowed.  When an implied command fails in NPC
         *   mode, the parser acts as though the command had never been
         *   attempted.
         *
         *   By default, we return ModePlayer if we're the player character,
         *   ModeNPC if not (thus the respective names of the modes).  Some
         *   authors might prefer to use "player mode" for NPC's as well as for
         *   the player character, which is why the various parts of the parser
         *   that care about this mode consult this method rather than simply
         *   testing the PC/NPC status of the actor.
         */
        impliedCommandMode() { return isPlayerChar() ? ModePlayer : ModeNPC; }

        /*
         *   Try moving the given object into this object.  For an actor, this
         *   will do one of two things.  If 'self' is the actor performing the
         *   action that's triggering this implied command, then we can achieve
         *   the goal simply by taking the object.  Otherwise, the way to get
         *   an object into my possession is to have the actor performing the
         *   command give me the object.
         */
        tryMovingObjInto(obj)
        {
            if (gActor == self)
            {
                /*
                 *   I'm performing the triggering action, so I merely need to
                 *   pick up the object
                 */
                return tryImplicitAction(Take, obj);
            }
            else
            {
                /*
                 *   another actor is performing the action; since that actor
                 *   is the one who must perform the implied action, the way to
                 *   get an object into my inventory is for that actor to give
                 *   it to me
                 */
                return tryImplicitAction(GiveTo, obj, self);
            }
        }

        /* desribe our containment of an object as carrying the object */
        mustMoveObjInto(obj) { reportFailure(&mustBeCarryingMsg, obj, self); }

        /*
         *   You can limit the cumulative amount of bulk an actor can hold, and
         *   the maximum bulk of any one object the actor can hold, using
         *   bulkCapacity and maxSingleBulk.  These properties are analogous to
         *   the same ones in Container.
         *
         *   A word of caution on these is in order.  Many authors worry that
         *   it's unrealistic if the player character can carry too much at one
         *   time, so they'll fiddle with these properties to impose a carrying
         *   limit that seems realistic.  Be advised that authors love this
         *   sort of "realism" a whole lot more than players do.  Players
         *   almost universally don't care about it, and in fact tend to hate
         *   the inventory juggling it inevitably leads to.  Juggling inventory
         *   isn't any fun for the player.  Don't fool yourself about this -
         *   the thoughts in the mind of a player who's tediously carting
         *   objects back and forth three at a time will not include admiration
         *   of your prowess at simulational realism.  In contrast, if you set
         *   the carrying limit to infinity, it's a rare player who will even
         *   notice, and a much rarer player who'll complain about it.
         *
         *   If you really must insist on inventory limits, refer to the
         *   BagOfHolding class for a solution that can salvage most of the
         *   "realism" that the accountancy-inclined author craves, without
         *   creating undue inconvenience for the player.  BagOfHolding makes
         *   inventory limits palatable for the player by essentially
         *   automating the required inventory juggling.  In fact, for most
         *   players, an inventory limit in conjunction with a bag of holding
         *   is actually better than an unlimited inventory, since it improves
         *   readability by keeping the direct inventory list to a manageable
         *   size.
         */
        bulkCapacity = 10000
        maxSingleBulk = 10

        /*
         *   An actor can limit the cumulative amount of weight being held,
         *   using weightCapacity.  By default we make this so large that
         *   there is effectively no limit to how much weight an actor can
         *   carry.
         */
        weightCapacity = 10000

        /*
         *   Can I own the given object?  By default, an actor can own
         *   anything.
         */
        canOwn(obj) { return true; }

        /*
         *   Get the preconditions for travel.  By default, we'll add the
         *   standard preconditions that the connector requires for actors.
         *
         *   Note that these preconditions apply only when the actor is the
         *   traveler.  If the actor is in a vehicle, so that the vehicle is
         *   the traveler in a given travel operation, the vehicle's
         *   travelerPreCond conditions are used instead of ours.
         */
        travelerPreCond(conn) { return conn.actorTravelPreCond(self); }

        /* by default, actors are listed when they arrive aboard a vehicle */
        isListedAboardVehicle = true

        /*
         *   Get the object that's actually going to move when this actor
         *   travels via the given connector.  In most cases this is simply the
         *   actor; but when the actor is in a vehicle, travel commands move
         *   the vehicle, not the actor: the actor stays in the vehicle while
         *   the vehicle moves to a new location.  We determine this by asking
         *   our immediate location what it thinks about the situation.
         *
         *   If we have a special traveler explicitly set, it overrides the
         *   traveler indicated by the location.
         */
        getTraveler(conn)
        {
            /*
             *   Return our special traveler if we have one; otherwise, if we
             *   have a location, return the traveler indicated by our
             *   location; otherwise, we're the traveler.
             */
            if (specialTraveler != nil)
                return specialTraveler;
            else if (location != nil)
                return location.getLocTraveler(self, conn);
            else
                return self;
        }

        /*
         *   Get the "push traveler" for the actor.  This is the nominal
         *   traveler that we want to use when the actor enters a command like
         *   PUSH BOX NORTH.  'obj' is the object we're trying to push.
         */
        getPushTraveler(obj)
        {
            /*
             *   If we already have a special traveler, just use the special
             *   traveler.  Otherwise, if we have a location, ask the location
             *   what it thinks.  Otherwise, we're the traveler.
             */
            if (specialTraveler != nil)
                return specialTraveler;
            else if (location != nil)
                return location.getLocPushTraveler(self, obj);
            else
                return self;
        }

        /* is an actor traveling with us? */
        isActorTraveling(actor)
        {
            /* we're the only actor traveling when we're the traveler */
            return (actor == self);
        }

        /* invoke a callback on each actor traveling with the traveler */
        forEachTravelingActor(func)
        {
            /* we're the only actor, so simply invoke the callback on myself */
            (func)(self);
        }

        /*
         *   Get the actors involved in travel, when we're acting in our role
         *   as a Traveler.  When the Traveler is simply the Actor, the only
         *   actor involved in the travel is 'self'.
         */
        getTravelerActors = [self]

        /* we're the self-motive actor doing the travel */
        getTravelerMotiveActors = [self]

        /*
         *   Set the "special traveler."  When this is set, we explicitly
         *   perform travel through this object rather than through the
         *   traveler indicated by our location.  Returns the old value, so
         *   that the old value can be restored when the caller has finished
         *   its need for the special traveler.
         */
        setSpecialTraveler(traveler)
        {
            local oldVal;

            /* remember the old value so that we can return it */
            oldVal = specialTraveler;

            /* remember the new value */
            specialTraveler = traveler;

            /* return the old value */
            return oldVal;
        }

        /* our special traveler */
        specialTraveler = nil

        /*
         *   Try moving the actor into the given room in preparation for
         *   travel, using pre-condition rules.
         */
        checkMovingTravelerInto(room, allowImplicit)
        {
            /* try moving the actor into the room */
            return room.checkMovingActorInto(allowImplicit);
        }

        /*
         *   Check to ensure the actor is ready to enter the given nested
         *   room, using pre-condition rules.  By default, we'll ask the given
         *   nested room to handle it.
         */
        checkReadyToEnterNestedRoom(dest, allowImplicit)
        {
            /* ask the destination to do the work */
            return dest.checkActorReadyToEnterNestedRoom(allowImplicit);
        }

        /*
         *   Travel within a location, as from a room to a contained nested
         *   room.  This should generally be used in lieu of travelTo when
         *   traveling between locations that are related directly by
         *   containment rather than with TravelConnector objects.
         *
         *   Travel within a location is not restricted by darkness; we assume
         *   that if the nested objects are in scope at all, travel among them
         *   is allowed.
         *
         *   This type of travel does not trigger calls to travelerLeaving()
         *   or travelerArriving().  To mitigate this loss of notification, we
         *   call actorTravelingWithin() on the source and destination
         *   objects.
         */
        travelWithin(dest)
        {
            /* if I'm not going anywhere, ignore the operation */
            if (dest == location)
                return;

            /*
             *   Notify the traveler.  Note that since this is local travel
             *   within a single top-level location, there's no connector.
             */
            getTraveler(nil).travelerTravelWithin(self, dest);
        }

        /*
         *   Traveler interface: perform local travel, between nested rooms
         *   within a single top-level location.
         */
        travelerTravelWithin(actor, dest)
        {
            local origin;

            /* remember my origin */
            origin = location;

            /* notify the source that we're traveling within a room */
            if (origin != nil)
                origin.actorTravelingWithin(origin, dest);

            /*
             *   if our origin and destination have different effective follow
             *   locations, track the follow
             */
            if (origin != nil
                && dest != nil
                && origin.effectiveFollowLocation != dest.effectiveFollowLocation)
            {
                /*
                 *   notify observing objects of the travel; we're not moving
                 *   along a connector, so there is no connector associated
                 *   with the tracking information
                 */
                connectionTable().forEachAssoc(
                    {obj, val: obj.beforeTravel(self, nil)});
            }

            /* move me to the destination */
            moveInto(dest);

            /*
             *   recalculate the global sense context for message generation
             *   purposes, since we've moved to a new location
             */
            if (gAction != nil)
                gAction.recalcSenseContext();

            /* notify the destination of the interior travel */
            if (dest != nil)
                dest.actorTravelingWithin(origin, dest);
        }

        /*
         *   Check for travel in the dark.  If we're in a dark room, and our
         *   destination is a dark room, ask the connector for guidance.
         *
         *   Travel connectors normally call this before invoking our
         *   travelTo() method to carry out the travel.  The darkness check
         *   usually must be made before any barrier checks.
         */
        checkDarkTravel(dest, connector)
        {
            local origin;

            /*
             *   If we're not in the dark in the current location, there's no
             *   need to check for dark-to-dark travel; light-to-dark travel
             *   is always allowed.
             */
            if (isLocationLit())
                return;

            /* get the origin - this is the traveler's location */
            origin = getTraveler(connector).location;

            /*
             *   Check to see if the connector itself is visible in the dark.
             *   If it is, then allow the travel without restriction.
             */
            if (connector.isConnectorVisibleInDark(origin, self))
                return;

            /*
             *   We are attempting dark-to-dark travel.  We allow or disallow
             *   this type of travel on a per-connector basis, so ask the
             *   connector to handle it.  If the connector wishes to disallow
             *   the travel, it will display an appropriate failure report and
             *   terminate the command with 'exit'.
             */
            connector.darkTravel(self, dest);
        }

        /*
         *   Travel to a new location.
         */
        travelTo(dest, connector, backConnector)
        {
            /* send the request to the traveler */
            getTraveler(connector)
                .travelerTravelTo(dest, connector, backConnector);
        }

        /*
         *   Perform scripted travel to the given adjacent location.  This
         *   looks for a directional connector in our current location whose
         *   destination is the given location, and for a corresponding
         *   back-connector in the destination location.  If we can find the
         *   connectors, we'll perform the travel using travelTo().
         *
         *   The purpose of this routine is to simplify scripted travel for
         *   simple cases where directional connectors are available for the
         *   desired travel.  This routine is NOT suitable for intelligent
         *   goal-seeking NPC's who automatically try to find their own routes,
         *   for two reasons.  First, this routine only lets an NPC move to an
         *   *adjacent* location; it won't try to find a path between arbitrary
         *   locations.  Second, this routine is "omniscient": it doesn't take
         *   into account what the NPC knows about the connections between
         *   locations, but simply finds a connector that actually provides the
         *   desired travel.
         *
         *   What this routine *is* suitable for are cases where we have a
         *   pre-scripted series of NPC travel actions, where we have a list of
         *   rooms we want the NPC to visit in order.  This routine simplifies
         *   this type of scripting by automatically finding the connectors;
         *   the script only has to specify the next location for the NPC to
         *   visit.
         */
        scriptedTravelTo(dest)
        {
            local conn;

            /* find a connector from the current location to the new location */
            conn = location.getConnectorTo(self, dest);

            /* if we found the connector, perform the travel */
            if (conn != nil)
                nestedActorAction(self, TravelVia, conn);
        }

        /*
         *   Remember the last door I traveled through.  We use this
         *   information for disambiguation, to boost the likelihood that an
         *   actor that just traveled through a door is referring to the same
         *   door in a subsequent "close" command.
         */
        rememberLastDoor(obj) { lastDoorTraversed = obj; }

        /*
         *   Remember our most recent travel.  If we know the back connector
         *   (i.e., the connector that reverses the travel we're performing),
         *   then we'll be able to accept a GO BACK command to attempt to
         *   return to the previous location.
         */
        rememberTravel(origin, dest, backConnector)
        {
            /* remember the destination of the travel, and the connector back */
            lastTravelDest = dest;
            lastTravelBack = backConnector;
        }

        /*
         *   Reverse the most recent travel.  If we're still within the same
         *   destination we reached in the last travel, and we know the
         *   connector we arrived through (i.e., the "back connector" for the
         *   last travel, which reverses the connector we took to get here),
         *   then try traveling via the connector.
         */
        reverseLastTravel()
        {
            /*
             *   If we don't know the connector back to our previous location,
             *   we obviously can't reverse the travel.  If we're not still in
             *   the same location as the previous travel's destination, then
             *   we can't reverse the travel either, because the back
             *   connector isn't applicable to our current location.  (This
             *   latter condition could only happen if we've been moved
             *   somewhere without ordinary travel occurring, but this is a
             *   possibility.)
             */
            if (lastTravelBack == nil
                || lastTravelDest == nil
                || !isIn(lastTravelDest))
            {
                reportFailure(&cannotGoBackMsg);
                exit;
            }

            /* attempt travel via our back connector */
            nestedAction(TravelVia, lastTravelBack);
        }

        /* the last door I traversed */
        lastDoorTraversed = nil

        /* the destination and back connector for our last travel */
        lastTravelDest = nil
        lastTravelBack = nil

        /*
         *   use a custom message for cases where we're holding a destination
         *   object for BOARD, ENTER, etc
         */
        checkStagingLocation(dest)
        {
            /*
             *   if the destination is within us, explain specifically that
             *   this is the problem
             */
            if (dest.isIn(self))
                reportFailure(&invalidStagingContainerActorMsg, self, dest);
            else
                inherited(dest);

            /* terminate the command */
            exit;
        }

        /*
         *   Travel arrival/departure messages.  Defer to the current state
         *   object on all of these.
         */
        sayArriving(conn)
            { curState.sayArriving(conn); }
        sayDeparting(conn)
            { curState.sayDeparting(conn); }
        sayArrivingLocally(dest, conn)
            { curState.sayArrivingLocally(dest, conn); }
        sayDepartingLocally(dest, conn)
            { curState.sayDepartingLocally(dest, conn); }
        sayTravelingRemotely(dest, conn)
            { curState.sayTravelingRemotely(dest, conn); }
        sayArrivingDir(dir, conn)
            { curState.sayArrivingDir(dir, conn); }
        sayDepartingDir(dir, conn)
            { curState.sayDepartingDir(dir, conn); }
        sayArrivingThroughPassage(conn)
            { curState.sayArrivingThroughPassage(conn); }
        sayDepartingThroughPassage(conn)
            { curState.sayDepartingThroughPassage(conn); }
        sayArrivingViaPath(conn)
            { curState.sayArrivingViaPath(conn); }
        sayDepartingViaPath(conn)
            { curState.sayDepartingViaPath(conn); }
        sayArrivingUpStairs(conn)
            { curState.sayArrivingUpStairs(conn); }
        sayArrivingDownStairs(conn)
            { curState.sayArrivingDownStairs(conn); }
        sayDepartingUpStairs(conn)
            { curState.sayDepartingUpStairs(conn); }
        sayDepartingDownStairs(conn)
            { curState.sayDepartingDownStairs(conn); }

        /*
         *   Get the current interlocutor.  By default, we'll address new
         *   conversational commands (ASK ABOUT, TELL ABOUT, SHOW TO) to the
         *   last conversational partner, if that actor is still within range.
         */
        getCurrentInterlocutor()
        {
            /*
             *   if we've talked to someone before, and we can still talk to
             *   them now, return that actor; otherwise we have no default
             */
            if (lastInterlocutor != nil && canTalkTo(lastInterlocutor))
                return lastInterlocutor;
            else
                return nil;
        }

        /*
         *   Get the default interlocutor.  If there's a current interlocutor,
         *   and we can still talk to that actor, then that's the default
         *   interlocutor.  If not, we'll return whatever actor is the default
         *   for a TALK TO command.  Note that TALK TO won't necessarily have a
         *   default actor; if it doesn't, we'll simply return nil.
         */
        getDefaultInterlocutor()
        {
            local actor;

            /* check for a current interlocutor */
            actor = getCurrentInterlocutor();

            /*
             *   if we're not talking to anyone, or if the person we were
             *   talking to can no longer hear us, look for a default object
             *   for a TALK TO command and use it instead as the default
             */
            if (actor == nil || !canTalkTo(actor))
            {
                /* set up a TALK TO command and a resolver */
                local tt = new TalkToAction();
                local res = new Resolver(tt, gIssuingActor, gActor);

                /* get the default direct object */
                actor = tt.getDefaultDobj(new EmptyNounPhraseProd(), res);

                /* if that worked, get the object from the resolve info */
                if (actor != nil)
                    actor = actor[1].obj_;
            }

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

        /*
         *   The most recent actor that we've interacted with through a
         *   conversational command (ASK, TELL, GIVE, SHOW, etc).
         */
        lastInterlocutor = nil

        /*
         *   Our conversational "boredom" counter.  While we're in a
         *   conversation, this tracks the number of turns since the last
         *   conversational command from the actor we're talking to.
         *
         *   Note that this state is part of the actor, even though it's
         *   usually managed by the InConversationState object.  The state is
         *   stored with the actor rather than with the state object because
         *   it really describes the condition of the actor, not of the state
         *   object.
         */
        boredomCount = 0

        /*
         *   game-clock time (Schedulable.gameClockTime) of the last
         *   conversational command addressed to us by the player character
         */
        lastConvTime = -1

        /*
         *   Did we engage in any conversation on the current turn?  This can
         *   be used as a quick check in background activity scripts when we
         *   want to run a step only in the absence of any conversation on the
         *   same turn.
         */
        conversedThisTurn() { return lastConvTime == Schedulable.gameClockTime; }

        /*
         *   Note that we're performing a conversational command targeting the
         *   given actor.  We'll make the actors point at each other with their
         *   'lastInterlocutor' properties.  This is called on the character
         *   performing the conversation command: if the player types ASK BOB
         *   ABOUT BOOK, this will be called on the player character actor,
         *   with 'other' set to Bob.
         */
        noteConversation(other)
        {
            /* note that we're part of a conversational action */
            noteConvAction(other);

            /* let the other actor know we're conversing with them */
            other.noteConversationFrom(self);
        }

        /*
         *   Note that another actor is issuing a conversational command
         *   targeting us.  For example, if the player types ASK BOB ABOUT
         *   BOOK, then this will be called on Bob, with the player character
         *   actor as 'other'.
         */
        noteConversationFrom(other)
        {
            /* note that we're part of a conversational action */
            noteConvAction(other);
        }

        /*
         *   Note that we're taking part in a conversational action with
         *   another character.  This is symmetrical - it could mean we're the
         *   initiator of the conversation action or the target.  We'll
         *   remember the person we're talking to, and reset our conversation
         *   time counters so we know we've conversed on this turn.
         */
        noteConvAction(other)
        {
            /* note our last conversational partner */
            lastInterlocutor = other;

            /* set the actor to be the pronoun antecedent */
            setPronounObj(other);

            /*
             *   reset our boredom counter, as the other actor has just spoken
             *   to us
             */
            boredomCount = 0;

            /* remember the time of our last conversation from the PC */
            lastConvTime = Schedulable.gameClockTime;
        }

        /* note that we're consulting an item */
        noteConsultation(obj) { lastConsulted = obj; }

        /*
         *   Receive notification that a TopicEntry response in our database is
         *   being invoked.  We'll just pass this along to our current state.
         */
        notifyTopicResponse(fromActor, entry)
        {
            /* let our current state handle it */
            curState.notifyTopicResponse(fromActor, entry);
        }

        /* the object we most recently consulted */
        lastConsulted = nil

        /*
         *   The actor's "agenda."  This is a list of AgendaItem objects that
         *   describe things the actor wants to do of its own volition on its
         *   own turn.
         */
        agendaList = nil

        /*
         *   our special "boredom" agenda item - this makes us initiate an end
         *   to an active conversation when the PC has ignored us for a given
         *   number of consecutive turns
         */
        boredomAgendaItem = perInstance(new BoredomAgendaItem(self))

        /* add an agenda item */
        addToAgenda(item)
        {
            /* if we don't have an agenda list yet, create one */
            if (agendaList == nil)
                agendaList = new Vector(10);

            /* add the item */
            agendaList.append(item);

            /*
             *   keep the list in ascending order of agendaOrder values - this
             *   will ensure that we'll always choose the earliest item that's
             *   ready to run
             */
            agendaList.sort(SortAsc, {a, b: a.agendaOrder - b.agendaOrder});

            /* reset the agenda item */
            item.resetItem();
        }

        /* remove an agenda item */
        removeFromAgenda(item)
        {
            /* if we have an agenda list, remove the item */
            if (agendaList != nil)
                agendaList.removeElement(item);
        }

        /*
         *   Execute the next item in our agenda, if there are any items in the
         *   agenda that are ready to execute.  We'll return true if we found
         *   an item to execute, nil if not.
         */
        executeAgenda()
        {
            local item;

            /* if we don't have an agenda, there are obviously no items */
            if (agendaList == nil)
                return nil;

            /* remove any items that are marked as done */
            while ((item = agendaList.lastValWhich({x: x.isDone})) != nil)
                agendaList.removeElement(item);

            /*
             *   Scan for an item that's ready to execute.  Since we keep the
             *   list sorted in ascending order of agendaOrder values, we can
             *   just pick the earliest item in the list that's ready to run,
             *   since that will be the ready-to-run item with the lowest
             *   agendaOrder number.
             */
            item = agendaList.valWhich({x: x.isReady});

            /* if we found an item, execute it */
            if (item != nil)
            {
                try
                {
                    /* execute the item */
                    item.invokeItem();
                }
                catch (RuntimeError err)
                {
                    /*
                     *   If an error occurs while executing the item, mark the
                     *   item as done.  This will ensure that we won't get
                     *   stuck in a loop trying to execute the same item over
                     *   and over, which will probably just run into the same
                     *   error on each attempt.
                     */
                    item.isDone = true;

                    /* re-throw the exception */
                    throw err;
                }

                /* tell the caller we found an item to execute */
                return true;
            }
            else
            {
                /* tell the caller we found no agenda item */
                return nil;
            }
        }

        /*
         *   Calculate the amount of bulk I'm holding directly.  By default,
         *   we'll simply add up the "actor-encumbering bulk" of each of our
         *   direct contents.
         *
         *   Note that we don't differentiate here based on whether or not an
         *   item is being worn, or anything else - we deliberately leave such
         *   distinctions up to the getEncumberingBulk routine, so that only
         *   the objects are in the business of deciding how bulky they are
         *   under different circumstances.
         */
        getBulkHeld()
        {
            local total;

            /* start with nothing */
            total = 0;

            /* add the bulks of directly-contained items */
            foreach (local cur in contents)
                total += cur.getEncumberingBulk(self);

            /* return the total */
            return total;
        }

        /*
         *   Calculate the total weight I'm holding.  By default, we'll add up
         *   the "actor-encumbering weight" of each of our direct contents.
         *
         *   Note that we deliberately only consider our direct contents.  If
         *   any of the items we are directly holding contain further items,
         *   getEncumberingWeight will take their weights into account; this
         *   frees us from needing any special knowledge of the internal
         *   structure of any items we're holding, and puts that knowledge in
         *   the individual items where it belongs.
         */
        getWeightHeld()
        {
            local total;

            /* start with nothing */
            total = 0;

            /* add the weights of directly-contained items */
            foreach (local cur in contents)
                total += cur.getEncumberingWeight(self);

            /* return the total */
            return total;
        }

        /*
         *   Try making room to hold the given object.  This is called when
         *   checking the "room to hold object" pre-condition, such as for the
         *   "take" verb.
         *
         *   If holding the new object would exceed the our maximum holding
         *   capacity, we'll go through our inventory looking for objects that
         *   can reduce our held bulk with implicit commands.  Objects with
         *   holding affinities - "bags of holding", keyrings, and the like -
         *   can implicitly shuffle the actor's possessions in a manner that
         *   is neutral as far as the actor is concerned, thereby reducing our
         *   active holding load.
         *
         *   Returns true if an implicit command was attempted, nil if not.
         */
        tryMakingRoomToHold(obj, allowImplicit)
        {
            local objWeight;
            local objBulk;
            local aff;

            /* get the amount of weight this will add if taken */
            objWeight = obj.getEncumberingWeight(self);

            /*
             *   If this object alone is too heavy for us, give up.  We
             *   distinguish this case from the case where the total (of
             *   everything held plus the new item) is too heavy: in the
             *   latter case we can tell the actor that they can pick this up
             *   by dropping something else first, whereas if this item alone
             *   is too heavy, no such advice is warranted.
             */
            if (objWeight > weightCapacity)
            {
                reportFailure(&tooHeavyForActorMsg, obj);
                exit;
            }

            /*
             *   if taking the object would push our total carried weight over
             *   our total carrying weight limit, give up
             */
            if (obj.whatIfHeldBy({: getWeightHeld()}, self) > weightCapacity)
            {
                reportFailure(&totalTooHeavyForMsg, obj);
                exit;
            }

            /* get the amount of bulk the object will add */
            objBulk = obj.getEncumberingBulk(self);

            /*
             *   if the object is simply too big to start with, we can't make
             *   room no matter what we do
             */
            if (objBulk > maxSingleBulk || objBulk > bulkCapacity)
            {
                reportFailure(&tooLargeForActorMsg, obj);
                exit;
            }

            /*
             *   Test what would happen to our bulk if we were to move the
             *   object into our directly held inventory.  Do this by running
             *   a "what if" scenario to test moving the object into our
             *   inventory, and check what effect it has on our held bulk.  If
             *   it fits, we can let the caller proceed without further work.
             */
            if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity)
                return nil;

            /*
             *   if we're not allowed to run implicit commands, we won't be
             *   able to accomplish anything, so give up
             */
            if (!allowImplicit)
            {
                reportFailure(&handsTooFullForMsg, obj);
                exit;
            }

            /*
             *   Get "bag of holding" affinity information for my immediate
             *   contents.  Consider only objects with encumbering bulk, since
             *   it will do us no good to move objects without any encumbering
             *   bulk.  Also ignore objects that aren't being held (some direct
             *   contents aren't considered to be held, such as clothing being
             *   worn).
             */
            aff = getBagAffinities(contents.subset(
                {x: x.getEncumberingBulk(self) != 0 && x.isHeldBy(self)}));

            /* if there are no bag affinities, we can't move anything around */
            if (aff.length() == 0)
            {
                reportFailure(&handsTooFullForMsg, obj);
                exit;
            }

            /*
             *   If we have at least four items, find the two that were picked
             *   up most recently (according to the "holding index" value) and
             *   move them to the end of the list.  In most cases, we'll only
             *   have to dispose of one or two items to free up enough space
             *   in our hands, so we'll probably never get to the last couple
             *   of items in our list, so we're effectively ruling out moving
             *   these two most recent items; but they'll be in the list if we
             *   do find we need to move them after all.
             *
             *   The point of this rearrangement is to avoid annoying cases of
             *   moving something we just picked up, especially if we just
             *   picked it up in order to carry out the command that's making
             *   us free up more space now.  This looks especially stupid when
             *   we perform some command that requires picking up two items
             *   automatically: we pick up the first, then we put it away in
             *   order to pick up the second, but then we find that we need
             *   the first again.
             */
            if (aff.length() >= 4)
            {
                local a, b;

                /* remove the two most recent items from the vector */
                a = BagAffinityInfo.removeMostRecent(aff);
                b = BagAffinityInfo.removeMostRecent(aff);

                /* re-insert them at the end of the vector */
                aff.append(b);
                aff.append(a);
            }

            /*
             *   Move each object in the list until we have reduced the bulk
             *   sufficiently.
             */
            foreach (local cur in aff)
            {
                /*
                 *   Try moving this object to its bag.  If the bag is itself
                 *   inside this object, don't even try, since that would be an
                 *   attempt at circular containment.
                 *
                 *   If the object we're trying to hold is inside this object,
                 *   don't move the object.  That might put the object we're
                 *   trying to hold out of reach, since moving an object into a
                 *   bag could involve closing the object or making its
                 *   contents not directly accessible.
                 */
                if (!cur.bag_.isIn(cur.obj_)
                    && !obj.isIn(cur.obj_)
                    && cur.bag_.tryPuttingObjInBag(cur.obj_))
                {
                    /*
                     *   this routine tried tried to move the object into the
                     *   bag - check our held bulk to see if we're in good
                     *   enough shape yet
                     */
                    if (obj.whatIfHeldBy({: getBulkHeld()}, self) <= bulkCapacity)
                    {
                        /*
                         *   We've met our condition - there's no need to look
                         *   any further.  Return, telling the caller we've
                         *   performed an implicit command.
                         */
                        return true;
                    }
                }
            }

            /*
             *   If we get this far, it means that we tried every child object
             *   but failed to find anything that could help.  Explain the
             *   problem and abort the command.
             */
            reportFailure(&handsTooFullForMsg, obj);
            exit;
        }

        /*
         *   Check a bulk change of one of my direct contents.
         */
        checkBulkChangeWithin(obj)
        {
            local objBulk;

            /* get the object's new bulk */
            objBulk = obj.getEncumberingBulk(self);

            /*
             *   if this change would cause the object to exceed our
             *   single-item bulk limit, don't allow it
             */
            if (objBulk > maxSingleBulk || objBulk > bulkCapacity)
            {
                reportFailure(&becomingTooLargeForActorMsg, obj);
                exit;
            }

            /*
             *   If our total carrying capacity is exceeded with this change,
             *   don't allow it.  Note that 'obj' is already among our
             *   contents when this routine is called, so we can simply check
             *   our current total bulk within.
             */
            if (getBulkHeld() > bulkCapacity)
            {
                reportFailure(&handsBecomingTooFullForMsg, obj);
                exit;
            }
        }

        /*
         *   Next available "holding index" value.  Each time we pick up an
         *   item, we'll assign it our current holding index value and then
         *   increment our value.  This gives us a simple way to keep track of
         *   the order in which we picked up items we're carrying.
         *
         *   Note that we make the simplifying assumption that an object can
         *   be held by only one actor at a time (multi-location items are
         *   generally not portable), which means that we can use a simple
         *   property in each object being held to store its holding index.
         */
        nextHoldingIndex = 1

        /* add an object to my contents */
        addToContents(obj)
        {
            /* assign the new object our next holding index */
            obj.holdingIndex = nextHoldingIndex++;

            /* inherit default handling */
            inherited(obj);
        }

        /*
         *   Go to sleep.  This is used by the 'Sleep' action to carry out the
         *   command.  By default, we simply say that we're not sleepy; actors
         *   can override this to cause other actions.
         */
        goToSleep()
        {
            /* simply report that we can't sleep now */
            mainReport(&cannotSleepMsg);
        }

        /*
         *   My current "posture," which specifies how we're positioned with
         *   respect to our container; this is one of the standard library
         *   posture enum values (Standing, etc.) or another posture added by
         *   the game.
         */
        posture = standing

        /*
         *   Get a default acknowledgment of a change to our posture.  This
         *   should acknowledge the posture so that it tells us the current
         *   posture.  This is used for a command such as "stand up" from a
         *   chair, so that we can report the appropriate posture status in
         *   our acknowledgment; we might end up being inside another nested
         *   container after standing up from the chair, so we might not
         *   simply be standing when we're done.
         */
        okayPostureChange()
        {
            /* get our nominal container for our current posture */
            local cont = location.getNominalActorContainer(posture);

            /* if the container is visible, let it handle it */
            if (cont != nil && gPlayerChar.canSee(cont))
            {
                /* describe via the container */
                cont.roomOkayPostureChange(self);
            }
            else
            {
                /* use the generic library message */
                defaultReport(&okayPostureChangeMsg, posture);
            }
        }

        /*
         *   Describe the actor as part of the EXAMINE description of a nested
         *   room containing the actor.  'povActor' is the actor doing the
         *   looking.
         */
        listActorPosture(povActor)
        {
            /* get our nominal container for our current posture */
            local cont = location.getNominalActorContainer(posture);

            /* if the container is visible, let it handle it */
            if (cont != nil && povActor.canSee(cont))
                cont.roomListActorPosture(self);
        }

        /*
         *   Stand up.  This is used by the 'Stand' action to carry out the
         *   command.
         */
        standUp()
        {
            /* if we're already standing, say so */
            if (posture == standing)
            {
                reportFailure(&alreadyStandingMsg);
                return;
            }

            /* ask the location to make us stand up */
            location.makeStandingUp();
        }

        /*
         *   Disembark.  This is used by the 'Get out' action to carry out the
         *   command.  By default, we'll let the room handle it.
         */
        disembark()
        {
            /* let the room handle it */
            location.disembarkRoom();
        }

        /*
         *   Set our posture to the given status.  By default, we'll simply
         *   set our posture property to the new status, but actors can
         *   override this to handle side effects of the change.
         */
        makePosture(newPosture)
        {
            /* remember our new posture */
            posture = newPosture;
        }

        /*
         *   Display a description of the actor's location from the actor's
         *   point of view.
         *
         *   If 'verbose' is true, then we'll show the full description in all
         *   cases.  Otherwise, we'll show the full description if the actor
         *   hasn't seen the location before, or the terse description if the
         *   actor has previously seen the location.
         */
        lookAround(verbose)
        {
            /* turn on the sense cache while we're looking */
            libGlobal.enableSenseCache();

            /* show a description of my immediate location, if I have one */
            if (location != nil)
                location.lookAroundPov(self, self, verbose);

            /* turn off the sense cache now that we're done */
            libGlobal.disableSenseCache();
        }

        /*
         *   Get my "look around" location name as a string.  This returns a
         *   string containing the location name that we display in the status
         *   line or at the start of a "look around" description of my
         *   location.
         */
        getLookAroundName()
        {
            return mainOutputStream.captureOutput(
                {: location.lookAroundWithinName(self, getVisualAmbient()) })
                .specialsToText();
        }

        /*
         *   Adjust a table of visible objects for 'look around'.  By default,
         *   we remove any explicitly excluded objects.
         */
        adjustLookAroundTable(tab, pov, actor)
        {
            /* remove any explicitly excluded objects */
            foreach (local cur in excludeFromLookAroundList)
                tab.removeElement(cur);

            /* inherit the base handling */
            inherited(tab, pov, actor);
        }

        /*
         *   Add an object to the 'look around' exclusion list.  Returns true
         *   if the object was already in the list, nil if not.
         */
        excludeFromLookAround(obj)
        {
            /*
             *   if the object is already in the list, don't add it again -
             *   just tell the caller it's already there
             */
            if (excludeFromLookAroundList.indexOf(obj) != nil)
                return true;

            /* add it to the list and tell the caller it wasn't already there */
            excludeFromLookAroundList.append(obj);
            return nil;
        }

        /* remove an object from the 'look around' exclusion list */
        unexcludeFromLookAround(obj)
        {
            excludeFromLookAroundList.removeElement(obj);
        }

        /*
         *   Our list of objects explicitly excluded from 'look around'.  These
         *   objects will be suppressed from any sort of listing (including in
         *   the room's contents list and in special descriptions) in 'look
         *   around' when this actor is doing the looking.
         */
        excludeFromLookAroundList = perInstance(new Vector(5))

        /*
         *   Get the location into which objects should be moved when the
         *   actor drops them with an explicit 'drop' command.  By default, we
         *   return the drop destination of our current container.
         */
        getDropDestination(objToDrop, path)
        {
            return (location != nil
                    ? location.getDropDestination(objToDrop, path)
                    : nil);
        }

        /*
         *   The senses that determine scope for this actor.  An actor might
         *   possess only a subset of the defined sense.
         *
         *   By default, we give each actor all of the human senses that we
         *   define, except touch.  In general, merely being able to touch an
         *   object doesn't put the object in scope, because if an object
         *   isn't noticed through some other sense, touch would only make an
         *   object accessible if it's within arm's reach, which for our
         *   purposes means that the object is being held directly by the
         *   actor.  Imagine an actor in a dark room: lots of things might be
         *   touchable in the sense that there's no physical barrier to
         *   touching them, but without some other sense to locate the
         *   objects, the actor wouldn't have any way of knowing where to
         *   reach to touch things, so they're not in scope.  So, touch isn't
         *   a scope sense.
         */
        scopeSenses = [sight, sound, smell]

        /*
         *   "Sight-like" senses: these are the senses that operate like sight
         *   for the actor, and which the actor can use to determine the names
         *   of objects and the spatial relationships between objects.  These
         *   senses should operate passively, in the sense that they should
         *   tend to collect sensory input continuously and without explicit
         *   action by the actor, the way sight does and the way touch, for
         *   example, does not.  These senses should also operate instantly,
         *   in the sense that the sense can reasonably take in most or all of
         *   a location at one time.
         *
         *   These senses are used to determine what objects should be listed
         *   in room descriptions, for example.
         *
         *   By default, the only sight-like sense is sight, since other human
         *   senses don't normally provide a clear picture of the spatial
         *   relationships among objects.  (Touch could with some degree of
         *   effort, but it can't operate passively or instantly, since
         *   deliberate and time-consuming action would be necessary.)
         *
         *   An actor can have more than one sight-like sense, in which case
         *   the senses will act effectively as one sense that can reach the
         *   union of objects reachable through the individual senses.
         */
        sightlikeSenses = [sight]

        /*
         *   Hearing-like senses.  These are senses that the actor can use to
         *   hear objects.
         */
        hearinglikeSenses = [sound]

        /*
         *   Smell-like senses.  These are senses that the actor can use to
         *   smell objects.
         */
        smelllikeSenses = [smell]

        /*
         *   Communication senses: these are the senses through which the
         *   actor can communicate directly with other actors through commands
         *   and messages.
         *
         *   Conceptually, these senses are intended to be only those senses
         *   that the actors would *naturally* use to communicate, because
         *   senses in this list allow direct communications via the most
         *   ordinary game commands, such as "bob, go east".
         *
         *   If some form of indirect communication is possible via a sense,
         *   but that form is not something the actor would think of as the
         *   most natural, default form of communication, it should *not* be
         *   in this list.  For example, two sighted persons who can see one
         *   another but cannot hear one another could still communicate by
         *   writing messages on pieces of paper, but they would ordinarily
         *   communicate by talking.  In such a case, sound should be in the
         *   list but sight should not be, because sight is not a natural,
         *   default form of communications for the actors.
         */
        communicationSenses = [sound]

        /*
         *   Determine if I can communicate with the given character via a
         *   natural, default form of communication that we share with the
         *   other character.  This determines if I can talk to the other
         *   character.  We'll return true if I can talk to the other actor,
         *   nil if not.
         *
         *   In order for the player character to issue a command to a
         *   non-player character (as in "bob, go east"), the NPC must be able
         *   to sense the PC via at least one communication sense that the two
         *   actors have in common.
         *
         *   Likewise, in order for a non-player character to say something to
         *   the player, the player must be able to sense the NPC via at least
         *   one communication sense that the two actors have in common.
         */
        canTalkTo(actor)
        {
            local common;

            /*
             *   first, get a list of the communications senses that we have
             *   in common with the other actor - we must have a sense channel
             *   via this sense
             */
            common = communicationSenses.intersect(actor.communicationSenses);

            /*
             *   if there are no common senses, we can't communicate,
             *   regardless of our physical proximity
             */
            if (common == [])
                return nil;

            /*
             *   Determine how well the other actor can sense me in these
             *   senses.  Note that all that matters it that the actor can
             *   hear me, because we're determine if I can talk to the other
             *   actor - it doesn't matter if I can hear the other actor.
             */
            foreach (local curSense in common)
            {
                local result;

                /*
                 *   determine how well the other actor can sense me in this
                 *   sense
                 */
                result = actor.senseObj(curSense, self);

                /* check whether or not this is good enough */
                if (actor.canBeTalkedTo(self, curSense, result))
                    return true;
            }

            /*
             *   if we get this far, we didn't find any senses with a clear
             *   enough communications channel - we can't talk to the other
             *   actor
             */
            return nil;
        }

        /*
         *   Determine whether or not I can understand an attempt by another
         *   actor to talk to me.  'talker' is the actor doing the talking.
         *   'sense' is the sense we're testing; this will always be a sense
         *   in our communicationSenses list, and will always be a
         *   communications sense we have in common with the other actor.
         *   'info' is a SenseInfo object giving information on the clarity of
         *   the sense path to the other actor.
         *
         *   We return true if we can understand the communication, nil if
         *   not.  There is no middle ground where we can partially
         *   understand; we can either understand or not.
         *
         *   Note that this routine is concerned only with our ability to
         *   sense the communication.  The result here should NOT pay any
         *   attention to whether or not we can actually communicate given a
         *   clear sense path - for example, this routine should not reflect
         *   whether or not we have a spoken language in common with the other
         *   actor.
         *
         *   This is a service method for canTalkTo.  This is broken out as a
         *   separate method so that individual actors can override the
         *   necessary conditions for communications in particular senses.
         */
        canBeTalkedTo(talker, sense, info)
        {
            /*
             *   By default, we allow communication if the sense path is
             *   transparent or distant.  We don't care what the sense is,
             *   since we know we'll never be asked about a sense that's not
             *   in our communicationSenses list.
             */
            return info.trans is in (transparent, distant);
        }

        /*
         *   Flag: we wait for commands issued to other actors to complete
         *   before we get another turn.  If this is true, then whenever we
         *   issue a command to another actor ("bob, go north"), we will not
         *   get another turn until the other actor has finished executing the
         *   full set of commands we issued.
         *
         *   By default, this is true, which means that we wait for other
         *   actors to finish all of the commands we issue before we take
         *   another turn.
         *
         *   If this is set to nil, we'll continue to take turns while the
         *   other actor carries out our commands.  In this case, the only
         *   time cost to us of issuing a command is given by orderingTime(),
         *   which normally takes one turn for issuing a command, regardless
         *   of the command's complexity.  Some games might wish to use this
         *   mode for interesting effects with NPC's carrying out commands in
         *   parallel with the player, but it's an unconventional style that
         *   some players might find confusing, so we don't use this mode by
         *   default.
         */
        issueCommandsSynchronously = true

        /*
         *   Flag: the "target actor" of the command line automatically reverts
         *   to this actor at the end of a sentence, when this actor is the
         *   issuer of a command.  If this flag is nil, an explicit target
         *   actor stays in effect until the next explicit target actor (or the
         *   end of the entire command line, if no other explicit target actors
         *   are named); if this flag is true, a target actor is in effect only
         *   until the end of a sentence.
         *
         *   Consider this command line:
         *
         *   >Bob, go north and get fuel cell. Get log tape.
         *
         *   If this flag is nil, then the second sentence ("get log tape") is
         *   interpreted as a command to Bob, because Bob is explicitly
         *   designated as the target of the command, and this remains in
         *   effect until the end of the entire command line.
         *
         *   If this flag is true, on the other hand, then the second sentence
         *   is interpreted as a command to the player character, because the
         *   target actor designation ("Bob,") lasts only until the end of the
         *   sentence.  Once a new sentence begins, we revert to the issuing
         *   actor (the player character, since the command came from the
         *   player via the keyboard).
         */
        revertTargetActorAtEndOfSentence = nil

        /*
         *   The amount of time, in game clock units, it takes me to issue an
         *   order to another actor.  By default, it takes one unit (which is
         *   usually equal to one turn) to issue a command to another actor.
         *   However, if we are configured to wait for our issued commands to
         *   complete in full, the ordering time is zero; we don't need any
         *   extra wait time in this case because we'll wait the full length
         *   of the issued command to begin with.
         */
        orderingTime(targetActor)
        {
            return issueCommandsSynchronously ? 0 : 1;
        }

        /*
         *   Wait for completion of a command that we issued to another actor.
         *   The parser calls this routine after each time we issue a command
         *   to another actor.
         *
         *   If we're configured to wait for completion of orders given to
         *   other actors before we get another turn, we'll set ourselves up
         *   in waiting mode.  Otherwise, we'll do nothing.
         */
        waitForIssuedCommand(targetActor)
        {
            /* if we can issue commands asynchronously, there's nothing to do */
            if (!issueCommandsSynchronously)
                return;

            /*
             *   Add an empty pending command at the end of the target actor's
             *   queue.  This command won't do anything when executed; its
             *   purpose is to let us track whether or not the target is still
             *   working on commands we have issued up to this point, which we
             *   can tell by looking to see whether our empty command is still
             *   in the actor's queue.
             *
             *   Note that we can't simply wait until the actor's queue is
             *   empty, because the actor could acquire new commands while
             *   it's working on our pending commands, and we wouldn't want to
             *   wait for those to finish.  Adding a dummy pending command is
             *   a reliable way of tracking the actor's queue, because any
             *   changes to the target actor's command queue will leave our
             *   dummy command in its proper place until the target actor gets
             *   around to executing it, at which point it will be removed.
             *
             *   Remember the dummy pending command in a property of self, so
             *   that we can check later to determine when the command has
             *   finished.
             */
            waitingForActor = targetActor;
            waitingForInfo = new PendingCommandMarker(self);
            targetActor.pendingCommand.append(waitingForInfo);
        }

        /*
         *   Synchronous command processing: the target actor and dummy
         *   pending command we're waiting for.  When these are non-nil, we
         *   won't take another turn until the given PendingCommandInfo has
         *   been removed from the given target actor's command queue.
         */
        waitingForActor = nil
        waitingForInfo = nil

        /*
         *   Add the given actor to the list of actors accompanying my travel
         *   on the current turn.  This does NOT set an actor in "follow mode"
         *   or "accompany mode" or anything like that - don't use this to make
         *   an actor follow me around.  Instead, this makes the given actor go
         *   with us for the CURRENT travel only - the travel we're already in
         *   the process of performing to process the current TravelVia action.
         */
        addAccompanyingActor(actor)
        {
            /* if we don't have the accompanying actor vector yet, create it */
            if (accompanyingActors == nil)
                accompanyingActors = new Vector(8);

            /* add the actor to my list */
            accompanyingActors.append(actor);
        }

        /*
         *   My vector of actors who are accompanying me.
         *
         *   This is for internal bookkeeping only, and it applies to the
         *   current travel only.  This is NOT a general "follow mode" setting,
         *   and it shouldn't be used to get me to follow another actor or
         *   another actor to follow me.  To make me accompany another actor,
         *   simply override accompanyTravel() so that it returns a suitable
         *   ActorState object.
         */
        accompanyingActors = nil

        /*
         *   Get the list of objects I can follow.  This is a list of all of
         *   the objects which I have seen departing a location - these are
         *   all in scope for 'follow' commands.
         */
        getFollowables()
        {
            /* return the list of the objects we know about */
            return followables_.mapAll({x: x.obj});
        }

        /*
         *   Do I track departing objects for following the given object?
         *
         *   By default, the player character tracks everyone, and NPC's track
         *   only the actor they're presently tasked to follow.  Most NPC's
         *   will never accept 'follow' commands, so there's no need to track
         *   everyone all the time; for efficiency, we take advantage of this
         *   assumption so that we can avoid storing a bunch of tracking
         *   information that will never be used.
         */
        wantsFollowInfo(obj)
        {
            /*
             *   by default, the player character tracks everyone, and NPC's
             *   track only the object (if any) they're currently tasked to
             *   follow
             */
            return isPlayerChar() || followingActor == obj;
        }

        /*
         *   Receive notification that an object is leaving its current
         *   location as a result of the action we're currently processing.
         *   Actors (and possibly other objects) will broadcast this
         *   notification to all Actor objects connected in any way by
         *   containment when they move under their own power (such as with
         *   Actor.travelTo) to a new location.  We'll keep tracking
         *   information if we are configured to keep tracking information for
         *   the given object and we can see the given object.  Note that this
         *   is called when the object is still at the source end of the travel
         *   - the important thing is that we see the object departing.
         *
         *   'obj' is the object that is seen to be leaving, and 'conn' is the
         *   TravelConnector it is taking.
         *
         *   'conn' is the connector being traversed.  If we're simply being
         *   observed in this location (as in a call to setHasSeen), rather
         *   than being observed to leave the location, the connector will be
         *   nil.
         *
         *   'from' is the effective starting location of the travel.  This
         *   isn't necessarily the departing object's location, since the
         *   departing object could be inside a vehicle or some other kind of
         *   traveler object.
         *
         *   Note that this notification is sent only to actors with some sort
         *   of containment connection to the object that's moving, because a
         *   containment connection is necessary for there to be a sense
         *   connection.
         */
        trackFollowInfo(obj, conn, from)
        {
            local info;

            /*
             *   If we're not tracking the given object, or we can't see the
             *   given object, ignore the notification.  In addition, we
             *   obviously have no need to track ourselves.x
             */
            if (obj == self || !wantsFollowInfo(obj) || !canSee(obj))
                return;

            /*
             *   If we already have a FollowInfo for the given object, re-use
             *   the existing one; otherwise, create a new one and add it to
             *   our tracking list.
             */
            info = followables_.valWhich({x: x.obj == obj});
            if (info == nil)
            {
                /* we don't have an existing one - create a new one */
                info = new FollowInfo();
                info.obj = obj;

                /* add it to our list */
                followables_ += info;
            }

            /* remember information about the travel */
            info.connector = conn;
            info.sourceLocation = from;
        }

        /*
         *   Get information on what to do to make this actor follow the given
         *   object.  This returns a FollowInfo object that reports our last
         *   knowledge of the given object's location and departure, or nil if
         *   we don't know anything about how to follow the actor.
         */
        getFollowInfo(obj)
        {
            return followables_.valWhich({x: x.obj == obj});
        }

        /*
         *   By default, all actors are followable.
         */
        verifyFollowable()
        {
            return true;
        }

        /*
         *   Verify a "follow" command being performed by this actor.
         */
        actorVerifyFollow(obj)
        {
            /*
             *   check to see if we're in the same effective follow location
             *   as the target; if we are, it makes no sense to follow the
             *   target, since we're already effectively at the same place
             */
            if (obj.location != nil
                && (location.effectiveFollowLocation
                    == obj.location.effectiveFollowLocation))
            {
                /*
                 *   We're in the same location as the target.  If we're the
                 *   player character, this makes no sense, because the player
                 *   character can't go into follow mode (as that would take
                 *   away the player's ability to control the player
                 *   character).  If we're an NPC, though, this simply tells
                 *   us to go into follow mode for the target, so there's
                 *   nothing wrong with it.
                 */
                if (isPlayerChar)
                {
                    /*
                     *   The target is right here, but we're the player
                     *   character, so it makes no sense for us to go into
                     *   follow mode.  If we can see the target, complain that
                     *   it's already here; if not, we can only assume it's
                     *   here, but we can't know for sure.
                     */
                    if (canSee(obj))
                        illogicalNow(&followAlreadyHereMsg);
                    else
                        illogicalNow(&followAlreadyHereInDarkMsg);
                }
            }
            else if (!canSee(obj))
            {
                /*
                 *   The target isn't here, and we can't see it from here, so
                 *   we must want to follow it to its current location.  Get
                 *   information on how we will follow the target.  If there's
                 *   no such information, we obviously can't do any following
                 *   because we never saw the target go anywhere in the first
                 *   place.
                 */
                if (getFollowInfo(obj) == nil)
                {
                    /* we've never heard of the target */
                    illogicalNow(&followUnknownMsg);
                }
            }
        }

        /*
         *   Carry out a "follow" command being performed by this actor.
         */
        actorActionFollow(obj)
        {
            local canSeeObj;

            /* note whether or not we can see the target */
            canSeeObj = canSee(obj);

            /*
             *   If we're not the PC, check to see if this is a follow-mode
             *   request; otherwise, try to go to the location of the target.
             */
            if (!isPlayerChar && canSeeObj)
            {
                /*
                 *   If we're not already following this actor, acknowledge the
                 *   request and go into 'follow' mode.  If we're already
                 *   following this actor, and we didn't issue the command to
                 *   ourself, let them know we're already in the requested
                 *   mode.  Otherwise, ignore it silently - if we issued the
                 *   command to ourself, it's because we're just executing our
                 *   own 'follow' mode imperative.
                 */
                if (followingActor != obj)
                {
                    /* let them know we're going to follow the actor now */
                    reportAfter(&okayFollowModeMsg);

                    /* go into follow mode */
                    followingActor = obj;
                }
                else if (gIssuingActor != self)
                {
                    /* let them know we're already in follow mode */
                    reportAfter(&alreadyFollowModeMsg);
                }

                /*
                 *   if we're already in the target's effective follow
                 *   location, that's all we need to do
                 */
                if (location.effectiveFollowLocation
                    == obj.location.effectiveFollowLocation)
                    return;
            }

            /*
             *   If we can see the target, AND we're in the same top-level
             *   location as the target, then simply use a "local travel"
             *   operation to move into the same location.  This only works
             *   with targets that are within the same top-level location,
             *   since that's the whole point of the local-travel routines.
             *   For non-local travel, we need to perform a full-fledged travel
             *   command instead.
             */
            if (canSeeObj && isIn(obj.getOutermostRoom()))
            {
                /*
                 *   We have no information, so we will only have made it past
                 *   verification if we can see the other actor from our
                 *   current location.  Try moving to the other actor's
                 *   effective follow location.
                 */
                obj.location.effectiveFollowLocation.checkMovingActorInto(true);

                /*
                 *   Since checkMovingActorInto will do its work through
                 *   implicit actions, if we're the player character, then the
                 *   entire action will have been performed implicitly, so we
                 *   won't have a real report for the series of generated
                 *   actions, just implied action announcements.  If we're an
                 *   NPC, on the other hand, we'll generate the full reports,
                 *   since NPC implied actions simply show what the actor is
                 *   doing.  So, if we're the PC, generate an additional
                 *   default acknowledgment of the 'follow' action.
                 */
                if (isPlayerChar)
                    defaultReport(&okayFollowInSightMsg,
                                  location.effectiveFollowLocation);
            }
            else
            {
                local info;
                local srcLoc;

                /* get the information on how to follow the target */
                info = getFollowInfo(obj);

                /* get the effective follow location we have to be in */
                srcLoc = info.sourceLocation.effectiveFollowLocation;

                /* if there's no connector, we can't go anywhere */
                if (info.connector == nil)
                {
                    /*
                     *   We have no departure information, so we can't follow
                     *   the actor.  If we're currently within sight of the
                     *   location where we last saw the actor, it means that we
                     *   saw the actor here, then went somewhere else, then
                     *   came back, and in our absence the actor itself
                     *   departed.  In this case, report that we don't know
                     *   where the actor went.
                     *
                     *   If we're not in sight of the location where we last
                     *   saw the actor, then instead remind the player of where
                     *   that was.
                     */
                    if (canSee(srcLoc))
                    {
                        /*
                         *   we're where we last saw the actor, but the actor
                         *   must have departed while we were away - so we
                         *   simply don't know where the actor went
                         */
                        reportFailure(&followUnknownMsg);
                    }
                    else
                    {
                        /*
                         *   we've gone somewhere else since we last saw the
                         *   actor, so remind the player of where it was that
                         *   we saw the actor
                         */
                        reportFailure(&cannotFollowFromHereMsg, srcLoc);
                    }

                    /* in any case, that's all we can do now */
                    return;
                }

                /*
                 *   Before we can follow the target, we must be in the same
                 *   effective location that the target was in when we
                 *   observed the target leaving.
                 */
                if (location.effectiveFollowLocation != srcLoc)
                {
                    /*
                     *   If we can't even see the effective follow location, we
                     *   must have last observed the other actor traveling from
                     *   an unrelated location.  In this case, simply say that
                     *   we don't know where the followee went.
                     */
                    if (!canSee(srcLoc))
                    {
                        reportFailure(&cannotFollowFromHereMsg, srcLoc);
                        return;
                    }

                    /*
                     *   Try moving into the same location, by invoking the
                     *   pre-condition handler for moving me into the
                     *   effective follow location from our memory of the
                     *   actor's travel.  We *could* run this as an actual
                     *   precondition, but it's easier to run it here now that
                     *   we've sorted out exactly what we want to do.
                     */
                    srcLoc.checkMovingActorInto(true);
                }

                /* perform a TravelVia action on the connector */
                nestedAction(TravelVia, info.connector);
            }
        }

        /*
         *   Our list of followable information.  Each entry in this list is a
         *   FollowInfo object that tracks a particular followable.
         */
        followables_ = []

        /* determine if I've ever seen the given object */
        hasSeen(obj) { return obj.(seenProp); }

        /* mark the object to remember that I've seen it */
        setHasSeen(obj) { obj.noteSeenBy(self, seenProp); }

        /* receive notification that another actor is observing us */
        noteSeenBy(actor, prop)
        {
            /* do the standard work to remember that we've been seen */
            inherited(actor, prop);

            /*
             *   Update the follow tracking information with the latest
             *   observed location.  We're merely observing the fact that the
             *   actor is here, not that the actor is departing, so the
             *   connector is nil.
             *
             *   The point of noting the actor's presence in the "follow info"
             *   is that we want to replace any previous memory we have of the
             *   actor departing from another location.  Now that we know where
             *   the actor is, any old memory of the actor having left another
             *   location is now irrelevant.  We only keep track of one "follow
             *   info" record per actor, so this new record will replace any
             *   older record.
             */
            actor.trackFollowInfo(self, nil, location);
        }

        /*
         *   Determine if I know about the given object.  I know about an
         *   object if it's specifically marked as known to me; I also know
         *   about the object if I can see it now, or if I've ever seen it in
         *   the past.
         */
        knowsAbout(obj) { return canSee(obj) || hasSeen(obj) || obj.(knownProp); }

        /* mark the object as known to me */
        setKnowsAbout(obj) { obj.(knownProp) = true; }

        /*
         *   My 'seen' property.  By default, this is simply 'seen', which
         *   means that we don't distinguish who's seen what - in other words,
         *   there's a single, global 'seen' flag per object, and if anyone's
         *   ever seen something, then we consider that to mean everyone has
         *   seen it.
         *
         *   Some games might want to track each NPC's sight memory
         *   separately, or at least they might want to track it individually
         *   for a few specific NPC's.  You can do this by making up a new
         *   property name for each NPC whose sight memory you want to keep
         *   separate, and simply setting 'seenProp' to that property name for
         *   each such NPC.  For example, for Bob, you could make the property
         *   bobHasSeen, so in Bob you'd define 'sightProp = &bobHasSeen'.
         */
        seenProp = &seen

        /*
         *   My 'known' property.  By default, this is simply 'known', which
         *   means that we don't distinguish who knows what.
         *
         *   As with 'seenProp' above, if you want to keep track of each NPC's
         *   knowledge separately, you must override this property for each
         *   NPC who's to have its own knowledge base to use a separate
         *   property name.  For example, if you want to keep track of what
         *   Bob knows individually, you could define 'knownProp = &bobKnows'
         *   in Bob.
         */
        knownProp = &isKnown

        /*
         *   Determine if the actor recognizes the given object as a "topic,"
         *   which is an object that represents some knowledge the actor can
         *   use in conversations, consultations, and the like.
         *
         *   By default, we'll recognize any Topic object marked as known, and
         *   we'll recognize any game object for which our knowsAbout(obj)
         *   returns true.  Games might wish to override this in some cases to
         *   limit or expand an actor's knowledge according to what the actor
         *   has experienced of the setting or story.  Note that it's often
         *   easier to control actor knowledge using the lower-level
         *   knowsAbout() and setKnowsAbout() methods, though.
         */
        knowsTopic(obj)
        {
            /* we know the object as a topic if we know about it at all */
            return knowsAbout(obj);
        }

        /*
         *   Determine if the given object is a likely topic for a
         *   conversational action performed by this actor.  By default, we'll
         *   return true if the topic is known, nil if not.
         */
        isLikelyTopic(obj)
        {
            /* if the object is known, it's a possible topic */
            return knowsTopic(obj);
        }

        /* we are the owner of any TopicEntry objects contained within us */
        getTopicOwner() { return self; }

        /*
         *   Suggest topics of conversation.  This is called by the TOPICS
         *   command (in which case 'explicit' is true), and whenever we first
         *   engage a character in a stateful conversation (in which case
         *   'explicit' is nil).
         *
         *   We'll show the list of suggested topics associated with our
         *   current conversational partner.  If there are no topics, we'll say
         *   nothing unless 'explicit' is true, in which case we'll simply say
         *   that there are no topics that the player character is thinking
         *   about.
         *
         *   The purpose of this method is to let the game author keep an
         *   "inventory" of topics with this actor for a given conversational
         *   partner.  This inventory is meant to represent the topics that on
         *   the player character's mind - things the player character wants to
         *   talk about with the other actor.  Note that we're talking about
         *   what the player *character* is thinking about - obviously we don't
         *   know what's on the player's mind.
         *
         *   When we enter conversation, or when the player asks for advice,
         *   we'll show this inventory.  The idea is to help guide the player
         *   through a conversation without the more heavy-handed device of a
         *   formal conversation menu system, so that conversations have a more
         *   free-form feel without leaving the player hunting in the dark for
         *   the magic ASK ABOUT topic.
         *
         *   The TOPICS system is entirely optional.  If a game doesn't specify
         *   any SuggestedTopic objects, then this routine will simply never be
         *   called, and the TOPICS command won't be allowed.  Some authors
         *   think it gives away too much to provide a list of topic
         *   suggestions like this, and others don't like anything that smacks
         *   of a menu system because they think it destroys the illusion
         *   created by the text-input command line that the game is boundless.
         *   Authors who feel this way can just ignore the TOPICS system.  But
         *   be aware that the illusion of boundlessness isn't always a good
         *   thing for players; hunting around for ASK ABOUT topics can make
         *   the game's limits just as obvious, if not more so, by exposing the
         *   vast number of inputs for which the actor doesn't have a good
         *   response.  Players aren't stupid - a string of variations on "I
         *   don't know about that" is just as obviously mechanistic as a
         *   numbered list of menu choices.  Using the TOPICS system might be a
         *   good compromise for many authors, since the topic list can help
         *   guide the player to the right questions without making the player
         *   feel straitjacketed by a menu list.
         */
        suggestTopics(explicit)
        {
            local actor;

            /*
             *   if we're talking to someone, look up their suggested topics;
             *   otherwise, we have nothing to suggest
             */
            if ((actor = getCurrentInterlocutor()) != nil)
            {
                /*
                 *   we're talking to someone - suggest topics appropriate to
                 *   the person we're talking to
                 */
                actor.suggestTopicsFor(self, explicit);
            }
            else if (explicit)
            {
                /* we're not talking to anyone, so there's nothing to suggest */
                gLibMessages.noTopicsNotTalking;
            }
        }

        /*
         *   Suggest topics that the given actor might want to talk to us
         *   about.  The given actor is almost always the player character,
         *   since generally NPC's don't talk to one another using
         *   conversation commands (there'd be no point; they're simple
         *   programmed automata, not full-blown AI's).
         */
        suggestTopicsFor(actor, explicit)
        {
            /* by default, let our state suggest topics */
            curState.suggestTopicsFor(actor, explicit);
        }

        /*
         *   Receive notification that a command is being carried out in our
         *   presence.
         */
        beforeAction()
        {
            /*
             *   If another actor is trying to take something in my inventory,
             *   by default, do not allow it.
             */
            if (gActor != self
                && (gActionIs(Take) || gActionIs(TakeFrom))
                && gDobj.isIn(self))
            {
                /* check to see if we want to allow this action */
                checkTakeFromInventory(gActor, gDobj);
            }

            /* let our state object take a look at the action */
            curState.beforeAction();
        }

        /*
         *   Perform any actor-specific processing for an action.  The main
         *   command processor invokes this on gActor after notifying nearby
         *   objects via beforeAction(), but before carrying out the main
         *   action of the command.
         */
        actorAction()
        {
            /* do nothing by default */
        }

        /*
         *   Receive notification that a command has just been carried out in
         *   our presence.
         */
        afterAction()
        {
            /* let the state object handle it */
            curState.afterAction();
        }

        /* receive a notification that someone is about to travel */
        beforeTravel(traveler, connector)
        {
            /* let the state object handle it */
            curState.beforeTravel(traveler, connector);

            /*
             *   If desired, track the departure so that we can follow the
             *   traveler later.  First, track the departure of each actor
             *   traveling with the traveler.
             */
            traveler.forEachTravelingActor(
                {actor: trackFollowInfo(actor, connector, traveler.location)});

            /*
             *   if the traveler is distinct from the actors traveling, track
             *   it as well
             */
            if (!traveler.isActorTraveling(traveler))
                trackFollowInfo(traveler, connector, traveler.location);
        }

        /* receive a notification that someone has just traveled here */
        afterTravel(traveler, connector)
        {
            /* let the state object handle it */
            curState.afterTravel(traveler, connector);
        }

        /*
         *   Receive notification that I'm initiating travel.  This is called
         *   on the actor performing the travel action before the travel is
         *   actually carried out.
         */
        actorTravel(traveler, connector)
        {
            /*
             *   If other actors are accompanying me on this travel, run the
             *   same travel action on the accompanying actors, using nested
             *   actions.
             */
            if (accompanyingActors != nil
                && accompanyingActors.length() != 0)
            {
                /*
                 *   Run the same travel action as a nested action on each
                 *   accompanying actor.  Skip this for any accompanying actor
                 *   we're carrying, as they'll naturally go with us as a
                 *   result of being carried.
                 */
                foreach (local cur in accompanyingActors)
                {
                    /* if the actor's not being carried, run the same action */
                    if (!cur.isIn(self))
                        nestedActorAction(cur, TravelVia, gDobj);
                }

                /*
                 *   The accompanying actor list applies for this single group
                 *   travel command, so now that we've moved everyone, we have
                 *   no further need for the list.  Clear it out.
                 */
                accompanyingActors.removeRange(1, accompanyingActors.length());
            }
        }

        /*
         *   Check to see if we want to allow another actor to take something
         *   from my inventory.  By default, we won't allow it - we'll always
         *   fail the command.
         */
        checkTakeFromInventory(actor, obj)
        {
            /* don't allow it - show an error and terminate the command */
            mainReport(&willNotLetGoMsg, self, obj);
            exit;
        }

        /*
         *   Build a list of the objects that are explicitly registered to
         *   receive notification when I'm the actor in a command.
         */
        getActorNotifyList()
        {
            return actorNotifyList;
        }

        /*
         *   Add an item to our registered notification items.  These items
         *   are to receive notifications when we're the actor performing a
         *   command.
         *
         *   Items can be added here if they must be notified of actions
         *   performed by the actor even when the items aren't connected by
         *   containment with the actor at the time of the action.  All items
         *   connected to the actor by containment are automatically notified
         *   of each action; only items that must receive notification even
         *   when not in scope need to be registered here.
         */
        addActorNotifyItem(obj)
        {
            actorNotifyList += obj;
        }

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

        /* our list of registered actor notification items */
        actorNotifyList = []

        /*
         *   Get the ambient light level in the visual senses at this actor.
         *   This is the ambient level at the actor.
         */
        getVisualAmbient()
        {
            local ret;
            local cache;

            /* check for a cached value */
            if ((cache = libGlobal.actorVisualAmbientCache) != nil
                && (ret = cache[self]) != nil)
            {
                /* found a cached entry - use it */
                return ret;
            }

            /* get the maximum ambient level at self for my sight-like senses */
            ret = senseAmbientMax(sightlikeSenses);

            /* if caching is active, cache our result for next time */
            if (cache != nil)
                cache[self] = ret;

            /* return the result */
            return ret;
        }

        /*
         *   Determine if my location is lit for my sight-like senses.
         */
        isLocationLit()
        {
            /*
             *   Check for a simple, common case before doing the full
             *   sense-path calculation: if our location is providing its own
             *   light to its interior, then the location is lit.  Most simple
             *   rooms are always lit.
             */
            if (sightlikeSenses.indexOf(sight) != nil
                && location != nil
                && location.brightness > 1
                && location.transSensingOut(sight) == transparent)
                return true;

            /*
             *   We don't have the simple case of light directly from our
             *   location, so run the full sense path check and get our
             *   maximum visual ambience level.  If it's above the "self-lit"
             *   level of 1, then we can see.
             */
            return (getVisualAmbient() > 1);
        }

        /*
         *   Get the best (most transparent) sense information for one of our
         *   visual senses to the given object.
         */
        bestVisualInfo(obj)
        {
            local best;

            /* we don't have a best value yet */
            best = nil;

            /* check each sight-like sense */
            foreach (local sense in sightlikeSenses)
            {
                /*
                 *   get the information for the object in this sense, and keep
                 *   the best (most transparent) info we've seen so far
                 */
                best = SenseInfo.selectMoreTrans(best, senseObj(sense, obj));
            }

            /* return the best one we found */
            return best;
        }

        /*
         *   Build a list of all of the objects of which an actor is aware.
         *
         *   An actor is aware of an object if the object is within reach of
         *   the actor's senses, and has some sort of presence in that sense.
         *   Note that both of these conditions must be true for at least one
         *   sense possessed by the actor; an object that is within earshot,
         *   but not within reach of any other sense, is in scope only if the
         *   object is making some kind of noise.
         *
         *   In addition, objects that the actor is holding (i.e., those
         *   contained by the actor directly) are always in scope, regardless
         *   of their reachability through any sense.
         */
        scopeList()
        {
            local lst;

            /* we have nothing in our master list yet */
            lst = new Vector(32);

            /* oneself is always in one's own scope list */
            lst.append(self);

            /* iterate over each sense */
            foreach (local sense in scopeSenses)
            {
                /*
                 *   get the list of objects with a presence in this sense
                 *   that can be sensed from our point of view, and and append
                 *   it to our master list
                 */
                lst.appendUnique(sensePresenceList(sense));
            }

            /* add all of the items we are directly holding */
            lst.appendUnique(contents);

            /*
             *   ask each of our direct contents to add any contents of their
             *   own that are in scope by virtue of their containers being in
             *   scope
             */
            foreach (local cur in contents)
                cur.appendHeldContents(lst);

            /* add any items that are specially in scope in the location */
            if (location != nil)
            {
                /* get the extra scope items */
                local extra = location.getExtraScopeItems(self);

                /* if this is a non-nil list, add it to our list */
                if (extra.length() != 0)
                    lst.appendUnique(extra);
            }

            /*
             *   Finally, add anything extra each item already in scope wants
             *   to add.  Note that we keep going until we've visited each
             *   element of the vector at its current length on each iteration,
             *   so if we add any new items, we'll check them to see if they
             *   want to add any new items, and so on.
             */
            for (local i = 1 ; i <= lst.length() ; ++i)
            {
                local extra;

                /* get the extra scope items for this item */
                extra = lst[i].getExtraScopeItems(self);

                /* if the extra item list is non-nil, add it to our list */
                if (extra.length() != 0)
                    lst.appendUnique(extra);
            }

            /* return the result */
            return lst.toList();
        }

        /*
         *   Determine if I can see the given object.  This returns true if
         *   the object can be sensed at all in one of my sight-like senses,
         *   nil if not.
         */
        canSee(obj)
        {
            /* try each sight-like sense */
            foreach (local sense in sightlikeSenses)
            {
                /*
                 *   if I can sense the object in this sense, I can sense the
                 *   object
                 */
                if (senseObj(sense, obj).trans != opaque)
                    return true;
            }

            /* we didn't find any sight-like sense where we can see the object */
            return nil;
        }

        /*
         *   Determine if I can hear the given object.
         */
        canHear(obj)
        {
            /* try each hearling-like sense */
            foreach (local sense in hearinglikeSenses)
            {
                /*
                 *   if I can sense the object in this sense, I can sense the
                 *   object
                 */
                if (senseObj(sense, obj).trans != opaque)
                    return true;
            }

            /* we found no hearing-like sense that lets us hear the object */
            return nil;
        }

        /*
         *   Determine if I can smell the given object.
         */
        canSmell(obj)
        {
            /* try each hearling-like sense */
            foreach (local sense in smelllikeSenses)
            {
                /*
                 *   if I can sense the object in this sense, I can sense the
                 *   object
                 */
                if (senseObj(sense, obj).trans != opaque)
                    return true;
            }

            /* we found no smell-like sense that lets us hear the object */
            return nil;
        }

        /*
         *   Find the object that prevents us from seeing the given object.
         */
        findVisualObstructor(obj)
        {
            /* try to find an opaque obstructor in one of our visual senses */
            foreach (local sense in sightlikeSenses)
            {
                local obs;

                /* cache path information for this sense */
                cacheSenseInfo(connectionTable(), sense);

                /* if we find an obstructor in this sense, return it */
                if ((obs = findOpaqueObstructor(sense, obj)) != nil)
                    return obs;
            }

            /* we didn't find any obstructor */
            return nil;
        }

        /*
         *   Build a table of full sensory information for all of the objects
         *   visible to the actor through the actor's sight-like senses.
         *   Returns a lookup table with the same set of information as
         *   senseInfoTable().
         */
        visibleInfoTable()
        {
            /* return objects visible from my own point of view */
            return visibleInfoTableFromPov(self);
        }

        /*
         *   Build a table of full sensory information for all of the objects
         *   visible to me from a particular point of view through my
         *   sight-like senses.
         */
        visibleInfoTableFromPov(pov)
        {
            local tab;

            /* we have no master table yet */
            tab = nil;

            /* iterate over each sense */
            foreach (local sense in sightlikeSenses)
            {
                local cur;

                /* get information for all objects for the current sense */
                cur = pov.senseInfoTable(sense);

                /* merge the table so far with the new table */
                tab = mergeSenseInfoTable(cur, tab);
            }

            /* return the result */
            return tab;
        }

        /*
         *   Build a lookup table of the objects that can be sensed for the
         *   purposes of taking inventory.  We'll include everything in the
         *   normal visual sense table, plus everything directly held.
         */
        inventorySenseInfoTable()
        {
            local visInfo;
            local cont;
            local ambient;
            local info;

            /*
             *   Start with the objects visible to the actor through the
             *   actor's sight-like senses.
             */
            visInfo = visibleInfoTable();

            /* get the ambient light level at the actor */
            if ((info = visInfo[self]) != nil)
                ambient = info.ambient;
            else
                ambient = 0;

            /*
             *   We'll assume that, for each item that the actor is directly
             *   holding AND knows about, the actor can still identify the item
             *   by touch, even if it's not visible.  This way, when we're in a
             *   dark room, we'll still be able to refer to the objects we're
             *   directly holding, as long as we already know about them.
             *
             *   Likewise, add items within our direct contents that are
             *   considered equally held.
             */
            cont = new Vector(32);
            foreach (local cur in contents)
            {
                /* add this item from our contents */
                cont.append(cur);

                /* add its contents that are themselves equally as held */
                cur.appendHeldContents(cont);
            }

            /*
             *   Make a fully-sensible entry for each of our held items.  We
             *   can simply replace any existing entry in the table that we got
             *   from the visual senses, since a fully transparent entry will
             *   be at least as good as anything we got from the normal visual
             *   list.  Only include items that the actor knows about; we'll
             *   assume that we can identify by touch anything we're holding if
             *   we already know what it is, but not otherwise.
             */
            foreach (local cur in cont)
            {
                /* if we know about the object, make it effectively visible */
                if (knowsAbout(cur))
                    visInfo[cur] = new SenseInfo(cur, transparent, nil, ambient);
            }

            /* return the table */
            return visInfo;
        }

        /*
         *   Show what the actor is carrying.
         */
        showInventory(tall)
        {
            /*
             *   show our inventory with our default listers as given by our
             *   inventory/wearing lister properties
             */
            showInventoryWith(tall, inventoryLister);
        }

        /*
         *   Show what the actor is carrying, using the given listers.
         *
         *   Note that this method must be overridden if the actor does not
         *   use a conventional 'contents' list property to store its full set
         *   of contents.
         */
        showInventoryWith(tall, inventoryLister)
        {
            local infoTab;

            /* get the table of objects sensible for inventory */
            infoTab = inventorySenseInfoTable();

            /* list in the appropriate mode ("wide" or "tall") */
            inventoryLister.showList(self, self, contents,
                                     ListRecurse | (tall ? ListTall : 0),
                                     0, infoTab, nil);

            /* mention sounds coming from inventory items */
            inventorySense(sound, inventoryListenLister);

            /* mention odors coming from inventory items */
            inventorySense(smell, inventorySmellLister);
        }

        /*
         *   Add to an inventory description a list of things we notice
         *   through a specific sense.
         */
        inventorySense(sense, lister)
        {
            local infoTab;
            local presenceList;

            /* get the information table for the desired sense */
            infoTab = senseInfoTable(sense);

            /*
             *   get the list of everything with a presence in this sense that
             *   I'm carrying
             */
            presenceList = senseInfoTableSubset(infoTab,
                {obj, info: obj.isIn(self) && obj.(sense.presenceProp)});

            /* add a paragraph break */
            cosmeticSpacingReport('<.p>');

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

        /*
         *   The Lister object that we use for inventory listings.  By
         *   default, we use actorInventoryLister, but this can be overridden
         *   if desired to use a different listing style.
         */
        inventoryLister = actorInventoryLister

        /*
         *   The Lister for inventory listings, for use in a full description
         *   of the actor.  By default, we use the "long form" inventory
         *   lister, on the assumption that most actors have relatively lengthy
         *   descriptive text.  This can be overridden to use other formats;
         *   the short-form lister, for example, is useful for actors with only
         *   brief descriptions.
         */
        holdingDescInventoryLister = actorHoldingDescInventoryListerLong

        /*
         *   Perform library pre-initialization on the actor
         */
        initializeActor()
        {
            /* set up an empty pending command list */
            pendingCommand = new Vector(5);

            /* create a default inventory lister if we don't have one already */
            if (inventoryLister == nil)
                inventoryLister = actorInventoryLister;

            /* create our antecedent tables */
            antecedentTable = new LookupTable(8, 8);
            possAnaphorTable = new LookupTable(8, 8);

            /* if we don't have a state object, create a default */
            if (curState == nil)
                setCurState(new ActorState(self));

            /* create our pending-conversation list */
            pendingConv = new Vector(5);
        }

        /*
         *   Note conditions before an action or other event.  By default, we
         *   note our location and light/dark status, so that we comment on
         *   any change in the light/dark status after the event if we're
         *   still in the same location.
         */
        noteConditionsBefore()
        {
            /* note our original location and light/dark status */
            locationBefore = location;
            locationLitBefore = isLocationLit();
        }

        /*
         *   Note conditions after an action or other event.  By default, if
         *   we are still in the same location we were in when
         *   noteConditionsBefore() was last called, and the light/dark status
         *   has changed, we'll mention the change in light/dark status.
         */
        noteConditionsAfter()
        {
            /*
             *   If our location hasn't changed but our light/dark status has,
             *   note the new status.  We don't make any announcement if the
             *   location has changed, since the travel routine will
             *   presumably have shown us the new location's light/dark status
             *   implicitly as part of the description of the new location
             *   after travel.
             */
            if (location == locationBefore
                && isLocationLit() != locationLitBefore)
            {
                /* consider this the start of a new turn */
                "<.commandsep>";

                /* note the change with a new 'NoteDarkness' action */
                newActorAction(self, NoteDarkness);

                /*
                 *   start another turn, in case this occurred during an
                 *   implicit action or the like
                 */
                "<.commandsep>";
            }
        }

        /* conditions we noted in noteConditionsBefore() */
        locationBefore = nil
        locationLitBefore = nil

        /* let the actor have a turn as soon as the game starts */
        nextRunTime = 0

        /*
         *   Scheduling order - this determines the order of execution when
         *   several items are schedulable at the same game clock time.
         *
         *   We choose a scheduling order that schedules actors in this
         *   relative order:
         *
         *   100 player character, ready to execute
         *.  200 NPC, ready to execute
         *.  300 player character, idle
         *.  400 NPC, idle
         *
         *   An "idle" actor is one that is waiting for another character to
         *   complete a command, or an NPC with no pending commands to
         *   perform.  (For the player character, it doesn't matter whether or
         *   not there's a pending command, because if the PC has no pending
         *   command, we ask the player for one.)
         *
         *   This ordering ensures that each actor gets a chance to run each
         *   turn, but that actors with work to do go first, and other things
         *   being equal, the player character goes ahead of NPC's.
         */
        scheduleOrder = 100

        /* calculate the scheduling order */
        calcScheduleOrder()
        {
            /* determine if we're ready to run */
            if (readyForTurn())
                scheduleOrder = isPlayerChar() ? 100 : 200;
            else
                scheduleOrder = isPlayerChar() ? 300 : 400;

            /* return the scheduling order */
            return scheduleOrder;
        }

        /*
         *   Determine if we're ready to do something on our turn.  We're
         *   ready to do something if we're not waiting for another actor to
         *   finish doing something and either we're the player character or
         *   we already have a pending command in our command queue.
         */
        readyForTurn()
        {
            /*
             *   if we're waiting for another actor, we're not ready to do
             *   anything
             */
            if (checkWaitingForActor())
                return nil;

            /*
             *   if we're the player character, we're always ready to take a
             *   turn as long as we're not waiting for another actor (which we
             *   now know we're not), because we can either execute one of our
             *   previously queued commands, or we can ask for a new command
             *   to perform
             */
            if (isPlayerChar())
                return true;

            /*
             *   if we have something other than placeholders in our command
             *   queue, we're ready to take a turn, because we can execute the
             *   next command in our queue
             */
            if (pendingCommand.indexWhich({x: x.hasCommand}) != nil)
                return true;

            /*
             *   we have no specific work to do, so we're not ready for our
             *   next turn
             */
            return nil;
        }

        /*
         *   Check to see if we're waiting for another actor to do something.
         *   Return true if so, nil if not.  If we've been waiting for another
         *   actor, and the actor has finished the task we've been waiting for
         *   since the last time we checked, we'll clean up our internal state
         *   relating to the wait and return nil.
         */
        checkWaitingForActor()
        {
            local cmdIdx;
            local idx;

            /* if we're not waiting for an actor, simply return nil */
            if (waitingForActor == nil)
                return nil;

            /*
             *   We're waiting for an actor to complete a command.  Check to
             *   see if the completion marker is still in the actor's queue; if
             *   it's not, then the other actor has already completed our task.
             *   If the completion marker is in the other actor's queue, but
             *   there are no command entries before it, then we're also done
             *   waiting, because we're not actually waiting for the completion
             *   marker but instead for the tasks that were ahead of it in the
             *   main game execution loop.
             *
             *   So, find the index of our marker in the queue, and find the
             *   index of the first real command in the queue.  If our marker
             *   is still in the queue, and there's a command in the queue
             *   before our marker, the actor we're waiting for still has
             *   things to do before we're ready, so we're still waiting.
             */
            idx = waitingForActor.pendingCommand.indexOf(waitingForInfo);
            cmdIdx = waitingForActor.pendingCommand.indexWhich({x: x.hasCommand});
            if (idx != nil && cmdIdx != nil && idx > cmdIdx)
            {
                /*
                 *   The marker is still in the queue, and there's at least
                 *   one other command ahead of it, so the other actor hasn't
                 *   finished the task we've been waiting for.  Tell the
                 *   caller that we are indeed still waiting for someone.
                 */
                return true;
            }

            /*
             *   The other actor has disposed of our end-marker (or is about
             *   to, because it's the next thing left in the actor's queue),
             *   so it has finished with all of the commands we have been
             *   waiting for.  However, if I haven't caught up in game clock
             *   time with the actor I've been waiting for, I'm still waiting.
             */
            if (waitingForActor.nextRunTime > nextRunTime)
                return true;

            /* we're done waiting - forget our wait status information */
            waitingForActor = nil;
            waitingForInfo = nil;

            /* tell the caller we're no longer waiting for anyone */
            return nil;
        }

        /* the action the actor performed most recently */
        mostRecentAction = nil

        /*
         *   Add busy time.  An action calls this when we are the actor
         *   performing the action, and the action consumes game time.  This
         *   marks us as busy for the given time units.
         */
        addBusyTime(action, units)
        {
            /* note the action being performed */
            mostRecentAction = action;

            /* adjust the next run time by the busy time */
            nextRunTime += units;
        }

        /*
         *   When it's our turn and we don't have any command to perform,
         *   we'll call this routine, which can perform a scripted operation
         *   if desired.
         */
        idleTurn()
        {
            local tCur = Schedulable.gameClockTime;
            local origNextRunTime = nextRunTime;

            /*
             *   if we haven't been targeted for conversation on this turn,
             *   see if we have a conversation we want to start
             */
            if (lastConvTime < tCur)
            {
                /* check for a conversation that's ready to go */
                local info = pendingConv.valWhich({x: tCur >= x.time_});

                /* if we found one, kick it off */
                if (info != nil)
                {
                    /* remove it from the list */
                    pendingConv.removeElement(info);

                    /* start the conversation */
                    initiateConversation(info.state_, info.node_);
                }
            }

            /* notify our state object that we're taking a turn */
            curState.takeTurn();

            /*
             *   If we haven't already adjusted our next run time, consume a
             *   turn, so we're not ready to run again until the next game time
             *   increment.  In some cases, we'll already have made this
             *   adjustment; for example, we might have run a nested command
             *   within our state object's takeTurn() method.
             */
            if (nextRunTime == origNextRunTime)
                ++nextRunTime;
        }

        /*
         *   Receive notification that this is a non-idle turn.  This is
         *   called whenever a command in our pending command queue is about
         *   to be executed.
         *
         *   This method need not do anything at all, since the caller will
         *   take care of running the pending command.  The purpose of this
         *   method is to take care of any changes an actor wants to make when
         *   it receives an explicit command, as opposed to running its own
         *   autonomous activity.
         *
         *   By default, we cancel follow mode if it's in effect.  It usually
         *   makes sense for an explicit command to interrupt follow mode;
         *   follow mode is usually started by an explicit command in the
         *   first place, so it is usually sensible for a new command to
         *   replace the one that started follow mode.
         */
        nonIdleTurn()
        {
            /* by default, cancel follow mode */
            followingActor = nil;
        }

        /*
         *   If we're following an actor, this keeps track of the actor we're
         *   following.  NPC's can use this to follow around another actor
         *   whenever possible.
         */
        followingActor = nil

        /*
         *   Handle a situation where we're trying to follow an actor but
         *   can't.  By default, this simply cancels our follow mode.
         *
         *   Actors might want to override this to be more tolerant.  For
         *   example, an actor might want to wait until five turns elapse to
         *   give up on following, in case the target actor returns after a
         *   brief digression; or an actor could stay in follow mode until it
         *   received other instructions, or found something better to do.
         */
        cannotFollow()
        {
            /*
             *   by default, simply cancel follow mode by forgetting about the
             *   actor we're following
             */
            followingActor = nil;
        }

        /*
         *   Execute one "turn" - this is a unit of time passing.  The player
         *   character generally is allowed to execute one command in the
         *   course of a turn; a non-player character with a programmed task
         *   can perform an increment of the task.
         *
         *   We set up an ActorTurnAction environment and invoke our
         *   executeActorTurn() method.  In most cases, subclasses should
         *   override executeActorTurn() rather than this method, since
         *   overriding executeTurn() directly will lose the action
         *   environment.
         */
        executeTurn()
        {
            /* start a new command visually when a new actor is taking over */
            "<.commandsep>";

            /*
             *   Execute the turn in a daemon action context, and in the sight
             *   context of the actor.  The sense context will ensure that we
             *   report the results of the action only if the actor is visible
             *   to the player character; in most cases, the actor's
             *   visibility is equivalent to the visibility of the effects, so
             *   this provides a simple way of ensuring that the results of
             *   the action are reported if and only if they're visible to the
             *   player character.
             *
             *   Note that if we are the player character, don't use the sense
             *   context filtering -- we normally want full reports for
             *   everything the player character does.
             */
            return withActionEnv(EventAction, self,
                {: callWithSenseContext(isPlayerChar() ? nil : self, sight,
                                        {: executeActorTurn() }) });
        }

        /*
         *   The main processing for an actor's turn.  In most cases,
         *   subclasses should override this method (rather than executeTurn)
         *   to specialize an actor's turn processing.
         */
        executeActorTurn()
        {
            /*
             *   If we have a pending response, and we're in a position to
             *   deliver it, our next work is to deliver the pending response.
             */
            if (pendingResponse != nil && canTalkTo(pendingResponse.issuer_))
            {
                /*
                 *   We have a pending response, and the command issuer from
                 *   the pending response can hear us now, so we can finally
                 *   deliver the response.
                 *
                 *   If the issuer is the player character, send to the player
                 *   using our deferred message generator; otherwise, call the
                 *   issuer's notification routine, since it's an NPC-to-NPC
                 *   notification.
                 */
                if (pendingResponse.issuer_.isPlayerChar())
                {
                    /*
                     *   we're notifying the player - use the deferred message
                     *   generator
                     */
                    getParserDeferredMessageObj().(pendingResponse.prop_)(
                        self, pendingResponse.args_...);
                }
                else
                {
                    /* it's an NPC-to-NPC notification - notify the issuer */
                    pendingResponse.issuer_.notifyIssuerParseFailure(
                        self, pendingResponse.prop_, pendingResponse.args_);
                }

                /*
                 *   in either case, we've gotten this out of our system now,
                 *   so we can forget about the pending response
                 */
                pendingResponse = nil;
            }

            /* check to see if we're waiting for another actor */
            if (checkWaitingForActor())
            {
                /*
                 *   we're still waiting, so there's nothing for us to do; take
                 *   an idle turn and return
                 */
                idleTurn();
                return true;
            }

            /*
             *   if we're the player character, and we have no pending commands
             *   to execute, our next task will be to read and execute a
             *   command
             */
            if (pendingCommand.length() == 0 && isPlayerChar())
            {
                local toks;

                /* read a command line and get the resulting token list */
                toks = readMainCommandTokens(rmcCommand);

                /*
                 *   re-activate the main transcript - reading the command
                 *   line will have deactivated the transcript, but we want it
                 *   active again now that we're about to start executing the
                 *   command
                 */
                gTranscript.activate();

                /*
                 *   If it came back nil, it means that the input was fully
                 *   processed in pre-parsing; this means that we don't have
                 *   any more work to do on this turn, so we can simply end our
                 *   turn now.
                 */
                if (toks == nil)
                    return true;

                /* retrieve the token list from the command line */
                toks = toks[2];

                /*
                 *   Add it to our pending command queue.  Since we read the
                 *   command from the player, and we're the player character,
                 *   we treat the command as coming from myself.
                 *
                 *   Since this is a newly-read command line, we're starting a
                 *   new sentence.
                 */
                addPendingCommand(true, self, toks);
            }

            /*
             *   Check to see if we have any pending command to execute.  If
             *   so, our next task is to execute the pending command.
             */
            if (pendingCommand.length() != 0)
            {
                local cmd;

                /* remove the first pending command from our queue */
                cmd = pendingCommand[1];
                pendingCommand.removeElementAt(1);

                /* if this is a real command, note the non-idle turn */
                if (cmd.hasCommand)
                    nonIdleTurn();

                /* execute the first pending command */
                cmd.executePending(self);

                /*
                 *   We're done with this turn.  If we no longer have any
                 *   pending commands, tell the scheduler to refigure the
                 *   execution order, since another object might now be ready
                 *   to run ahead of our idle activity.
                 */
                if (pendingCommand.indexWhich({x: x.hasCommand}) == nil)
                    return nil;
                else
                    return true;
            }

            /*
             *   If we're following an actor, and the actor isn't in sight, see
             *   if we can catch up.
             */
            if (followingActor != nil
                && location != nil
                && (followingActor.location.effectiveFollowLocation
                    != location.effectiveFollowLocation))
            {
                local info;

                /* see if we have enough information to follow */
                info = getFollowInfo(followingActor);

                /*
                 *   Check to see if we have enough information to follow the
                 *   actor.  We can only follow if we saw the actor depart at
                 *   some point, and we're in the same location where we last
                 *   saw the actor depart.  (We have to be in the same
                 *   location, because we follow by performing the same command
                 *   we saw the actor perform when we last saw the actor
                 *   depart.  Repeating the command will obviously be
                 *   ineffective unless we're in the same location as the actor
                 *   was.)
                 */
                if (info != nil)
                {
                    local success;

                    /*
                     *   we know how to follow the actor, so simply perform
                     *   the same command we saw the actor perform.
                     */
                    newActorAction(self, Follow, followingActor);

                    /* note whether or not we succeeded */
                    success = (location.effectiveFollowLocation ==
                               followingActor.location.effectiveFollowLocation);

                    /* notify the state object of our attempt */
                    curState.justFollowed(success);

                    /*
                     *   if we failed to track the actor, note that we are
                     *   unable to follow the actor
                     */
                    if (!success)
                    {
                        /* note that we failed to follow the actor */
                        cannotFollow();
                    }

                    /* we're done with this turn */
                    return true;
                }
                else
                {
                    /*
                     *   we don't know how to follow this actor - call our
                     *   cannot-follow handler
                     */
                    cannotFollow();
                }
            }

            /* we have no pending work to perform, so take an idle turn */
            idleTurn();

            /* no change in scheduling priority */
            return true;
        }

        /*
         *   By default, all actors are likely command targets.  This should
         *   be overridden for actors who are obviously not likely to accept
         *   commands of any kind.
         *
         *   This is used to disambiguate target actors in commands, so this
         *   should provide an indication of what should be obvious to a
         *   player, because the purpose of this information is to guess what
         *   the player is likely to take for granted in specifying a target
         *   actor.
         */
        isLikelyCommandTarget = true

        /*
         *   Determine if we should accept a command.  'issuingActor' is the
         *   actor who issued the command: if the player typed the command on
         *   the command line, this will be the player character actor.
         *
         *   This routine performs only the simplest check, since it doesn't
         *   have access to the specific action being performed.  This is
         *   intended as a first check, to allow us to bypass noun resolution
         *   if the actor simply won't accept any command from the issuer.
         *
         *   Returns true to accept a command, nil to reject it.  If this
         *   routine returns nil, and the command came from the player
         *   character, a suitable message should be displayed.
         *
         *   Note that most actors should not override this routine simply to
         *   express the will of the actor to accept a command, since this
         *   routine performs a number of checks for the physical ability of
         *   the actor to execute a command from the issuer.  To determine
         *   whether or not the actor should obey physically valid commands
         *   from the issuer, override obeyCommand().
         */
        acceptCommand(issuingActor)
        {
            /* if we're the current player character, accept any command */
            if (isPlayerChar())
                return true;

            /* if we can't hear the issuer, we can't talk to it */
            if (issuingActor != self && !issuingActor.canTalkTo(self))
            {
                /* report that the target actor can't hear the issuer */
                reportFailure(&objCannotHearActorMsg, self);

                /* tell the caller that the command cannot proceed */
                return nil;
            }

            /* if I'm busy doing something else, say so */
            if (nextRunTime > Schedulable.gameClockTime)
            {
                /* tell the issuing actor I'm busy */
                notifyParseFailure(issuingActor, &refuseCommandBusy,
                                   [issuingActor]);

                /* tell the caller to abandon the command */
                return nil;
            }

            /* check to see if I have other work to perform first */
            if (!acceptCommandBusy(issuingActor))
                return nil;

            /* we didn't find any reason to object, so allow the command */
            return true;
        }

        /*
         *   Check to see if I'm busy with pending commands, and if so,
         *   whether or not I should accept a new command.  Returns true if we
         *   should accept a command, nil if not.  If we return nil, we must
         *   notify the issuer of the rejection.
         *
         *   By default, we won't accept a command if we have any work
         *   pending.
         */
        acceptCommandBusy(issuingActor)
        {
            /* if we have any pending commands, don't accept a new command */
            if (pendingCommand.length() != 0)
            {
                /*
                 *   if we have only commands from the same issuer pending,
                 *   cancel all of the pending commands and accept the new
                 *   command instead
                 */
                foreach (local info in pendingCommand)
                {
                    /*
                     *   if this is from a different issuer, don't accept a
                     *   new command
                     */
                    if (info.issuer_ != issuingActor)
                    {
                        /* tell the other actor that we're busy */
                        notifyParseFailure(issuingActor, &refuseCommandBusy,
                                           [issuingActor]);

                        /* tell the caller to abandon the command */
                        return nil;
                    }
                }

                /*
                 *   all of the pending commands were from the same issuer, so
                 *   presumably the issuer wants to override those commands;
                 *   remove the old ones from our pending queue
                 */
                pendingCommand.removeRange(1, pendingCommand.length());
            }

            /* we didn't find any problems */
            return true;
        }

        /*
         *   Determine whether or not we want to obey a command from the given
         *   actor to perform the given action.  We only get this far when we
         *   determine that it's possible for us to accept a command, given
         *   the sense connections between us and the issuing actor, and given
         *   our pending command queue.
         *
         *   When this routine is called, the action has been determined, and
         *   the noun phrases have been resolved.  However, we haven't
         *   actually started processing the action yet, so the globals for
         *   the noun slots (gDobj, gIobj, etc) are NOT available.  If the
         *   routine needs to know which objects are involved, it must obtain
         *   the full list of resolved objects from the action (using, for
         *   example, getResolvedDobjList()).
         *
         *   When there's a list of objects to be processed (as in GET ALL),
         *   we haven't started working on any one of them yet - this check is
         *   made once for the entire command, and applies to the entire list
         *   of objects.  If the actor wants to respond specially to
         *   individual objects, you can do that by overriding actorAction()
         *   instead of this routine.
         *
         *   This routine should display an appropriate message and return nil
         *   if the command is not to be accepted, and should simply return
         *   true to accept the command.
         *
         *   By default, we'll let our state object handle this.
         *
         *   Note that actors that override this might also need to override
         *   wantsFollowInfo(), since an actor that accepts "follow" commands
         *   will need to keep track of the movements of other actors if it is
         *   to carry out any following.
         */
        obeyCommand(issuingActor, action)
        {
            /* note that the issuing actor is targeting me in conversation */
            issuingActor.noteConversation(self);

            /* let the state object handle it */
            return curState.obeyCommand(issuingActor, action);
        }

        /*
         *   Say hello/goodbye/yes/no to the given actor.  We'll greet the
         *   target actor is the target actor was specified (i.e., actor !=
         *   self); otherwise, we'll greet our current default conversational
         *   partner, if we have one.
         */
        sayHello(actor) { sayToActor(actor, helloTopicObj, helloConvType); }
        sayGoodbye(actor) { sayToActor(actor, byeTopicObj, byeConvType); }
        sayYes(actor) { sayToActor(actor, yesTopicObj, yesConvType); }
        sayNo(actor) { sayToActor(actor, noTopicObj, noConvType); }

        /* handle one of the conversational addresses */
        sayToActor(actor, topic, convType)
        {
            /*
             *   If the target actor is the same as the issuing actor, then no
             *   target actor was specified in the command, so direct the
             *   address to our current conversational partner, if we have
             *   one.
             */
            if (actor == self)
                actor = getDefaultInterlocutor();

            /*
             *   if we found an actor, send the address to the actor's state
             *   object; otherwise, handle it with the given default message
             */
            if (actor != nil)
            {
                /* make sure we can talk to the other actor */
                if (!canTalkTo(actor))
                {
                    /* can't talk to them - say so and give up */
                    reportFailure(&objCannotHearActorMsg, actor);
                    exit;
                }

                /* remember our current conversational partner */
                noteConversation(actor);

                /* handle it as a topic */
                actor.curState.handleConversation(self, topic, convType);
            }
            else
            {
                /*
                 *   we don't know whom we're addressing; just show the default
                 *   message for an unknown interlocutor
                 */
                mainReport(convType.unknownMsg);
            }
        }

        /*
         *   Handle the XSPCLTOPIC pseudo-command.  This command is generated
         *   by the SpecialTopic pre-parser when it recognizes the player's
         *   input as matching an active SpecialTopic's custom syntax.  Our
         *   job is to route this back to our current interlocutor's active
         *   ConvNode, so that it can find the SpecialTopic that it matched in
         *   pre-parsing and show its response.
         */
        saySpecialTopic()
        {
            local actor;

            /* send it to our interlocutor */
            if ((actor = getCurrentInterlocutor()) == nil
                || actor.curConvNode == nil)
            {
                /*
                 *   We don't seem to have a current interlocutor, or the
                 *   interlocutor doesn't have a current conversation node.
                 *   This is inconsistent; there's no way we could have
                 *   generated XSPCLTOPIC from our pre-parser under these
                 *   conditions.  The most likely thing is that the player
                 *   tried typing in XSPCLTOPIC manually.  Politely ignore it.
                 */
                gLibMessages.commandNotPresent;
            }
            else
            {
                /* note the conversation directed to the other actor */
                noteConversation(actor);

                /* send the request to the ConvNode for processing */
                actor.curConvNode.saySpecialTopic(self);
            }
        }

        /*
         *   Add a command to our pending command list.  The new command is
         *   specified as a list of tokens to be parsed, and it is added after
         *   any commands already in our pending list.
         */
        addPendingCommand(startOfSentence, issuer, toks)
        {
            /* add a descriptor to the pending command list */
            pendingCommand.append(
                new PendingCommandToks(startOfSentence, issuer, toks));
        }

        /*
         *   Insert a command at the head of our pending command list.  The
         *   new command is specified as a list of tokens to parse, and it is
         *   inserted into our pending command list before any commands
         *   already in the list.
         */
        addFirstPendingCommand(startOfSentence, issuer, toks)
        {
            /* add a descriptor to the start of our list */
            pendingCommand.insertAt(
                1, new PendingCommandToks(startOfSentence, issuer, toks));
        }

        /*
         *   Add a resolved action to our pending command list.  The new
         *   command is specified as a resolved Action object; it is added
         *   after any commands already in our list.
         */
        addPendingAction(startOfSentence, issuer, action, [objs])
        {
            /* add a descriptor to the pending command list */
            pendingCommand.append(new PendingCommandAction(
                startOfSentence, issuer, action, objs...));
        }

        /*
         *   Insert a resolved action at the start of our pending command
         *   list.  The new command is specified as a resolved Action object;
         *   it is added before any commands already in our list.
         */
        addFirstPendingAction(startOfSentence, issuer, action, [objs])
        {
            /* add a descriptor to the pending command list */
            pendingCommand.insertAt(1, new PendingCommandAction(
                startOfSentence, issuer, action, objs...));
        }

        /* pending commands - this is a list of PendingCommandInfo objects */
        pendingCommand = nil

        /*
         *   pending response - this is a single PendingResponseInfo object,
         *   which we'll deliver as soon as the issuing actor is in a position
         *   to hear us
         */
        pendingResponse = nil

        /*
         *   get the library message object for a parser message addressed to
         *   the player character
         */
        getParserMessageObj()
        {
            /*
             *   If I'm the player character, use the player character message
             *   object; otherwise, use the default non-player character
             *   message object.
             *
             *   To customize parser messages from a particular actor, create
             *   an object based on npcMessages, and override this routine in
             *   the actor so that it returns the custom object rather than
             *   the standard npcMessages object.  To customize messages for
             *   ALL of the NPC's in a game, simply modify npcMessages itself,
             *   since it's the default for all non-player characters.
             */
            return isPlayerChar() ? playerMessages : npcMessages;
        }

        /*
         *   Get the deferred library message object for a parser message
         *   addressed to the player character.  We only use this to generate
         *   messages deferred from non-player characters.
         */
        getParserDeferredMessageObj() { return npcDeferredMessages; }

        /*
         *   Get the library message object for action responses.  This is
         *   used to generate library responses to verbs.
         */
        getActionMessageObj()
        {
            /*
             *   return the default player character or NPC message object,
             *   depending on whether I'm the player or not; individual actors
             *   can override this to supply actor-specific messages for
             *   library action responses
             */
            return isPlayerChar() ? playerActionMessages: npcActionMessages;
        }

        /*
         *   Notify an issuer that a command sent to us resulted in a parsing
         *   failure.  We are meant to reply to the issuer to let the issuer
         *   know about the problem.  messageProp is the libGlobal message
         *   property describing the error, and args is a list with the
         *   (varargs) arguments to the message property.
         */
        notifyParseFailure(issuingActor, messageProp, args)
        {
            /*
             *   In case the actor is in a remote location but in scope for the
             *   purposes of the conversation only (such as over a phone or
             *   radio), run this in a neutral sense context.  Since we're
             *   reporting a parser failure, we want the message to be
             *   displayed no matter what the scope situation is.
             */
            callWithSenseContext(nil, nil, function()
            {
                /* check who's talking to whom */
                if (issuingActor.isPlayerChar())
                {
                    /*
                     *   The player issued the command.  If the command was
                     *   directed to an NPC (i.e., we're not the player), check
                     *   to see if the player character is in scope from our
                     *   perspective.
                     */
                    if (issuingActor != self && !canTalkTo(issuingActor))
                    {
                        /*
                         *   The player issued the command to an NPC, but the
                         *   player is not capable of hearing the NPC's
                         *   response.
                         */
                        cannotRespondToCommand(issuingActor, messageProp, args);
                    }
                    else
                    {
                        /*
                         *   generate a message using the appropriate message
                         *   generator object
                         */
                        getParserMessageObj().(messageProp)(self, args...);
                    }
                }
                else
                {
                    /*
                     *   the command was issued from one NPC to another -
                     *   notify the issuer of the problem, but don't display
                     *   any messages, since this interaction is purely among
                     *   the NPC's
                     */
                    issuingActor.
                        notifyIssuerParseFailure(self, messageProp, args);
                }
            });
        }

        /*
         *   We have a parser error to report to the player, but we cannot
         *   respond at the moment because the player is not capable of
         *   hearing us (there is no sense path for our communications senses
         *   from us to the player actor).  Defer reporting the message until
         *   later.
         */
        cannotRespondToCommand(issuingActor, messageProp, args)
        {
            /*
             *   Remember the problem for later deliver.  If we already have a
             *   deferred response, forget it - just report the latest
             *   problem.
             */
            pendingResponse =
                new PendingResponseInfo(issuingActor, messageProp, args);

            /*
             *   Some actors might want to override this to start searching
             *   for the player character.  We don't have any generic
             *   mechanism to conduct such a search, but a game that
             *   implements one might want to make use of it here.
             */
        }

        /*
         *   Receive notification that a command we sent to another NPC
         *   failed.  This is only called when one NPC sends a command to
         *   another NPC; this is called on the issuer to let the issuer know
         *   that the target can't perform the command because of the given
         *   resolution failure.
         *
         *   By default, we don't do anything here, because we don't have any
         *   default code to send a command from one NPC to another.  Any
         *   custom NPC actor that sends a command to another NPC actor might
         *   want to use this to deal with problems in processing those
         *   commands.
         */
        notifyIssuerParseFailure(targetActor, messageProp, args)
        {
            /* by default, we do nothing */
        }

        /*
         *   Antecedent lookup table.  Each actor keeps its own table of
         *   antecedents indexed by pronoun type, so that we can
         *   simultaneously have different antecedents for different pronouns.
         */
        antecedentTable = nil

        /*
         *   Possessive anaphor lookup table.  In almost all cases, the
         *   possessive anaphor for a given pronoun will be the same as the
         *   corresponding regular pronoun: HIS indicates possession by HIM,
         *   for example.  In a few cases, though, the anaphoric quality of
         *   possessives takes precedence, and these will differ.  For
         *   example, in TELL BOB TO DROP HIS BOOK, "his" refers back to Bob,
         *   while in TELL BOB TO HIT HIM, "him" refers to whatever it
         *   referred to before the command.
         */
        possAnaphorTable = nil

        /*
         *   set the antecedent for the neuter singular pronoun ("it" in
         *   English)
         */
        setIt(obj)
        {
            setPronounAntecedent(PronounIt, obj);
        }

        /* set the antecedent for the masculine singular ("him") */
        setHim(obj)
        {
            setPronounAntecedent(PronounHim, obj);
        }

        /* set the antecedent for the feminine singular ("her") */
        setHer(obj)
        {
            setPronounAntecedent(PronounHer, obj);
        }

        /* set the antecedent list for the ungendered plural pronoun ("them") */
        setThem(lst)
        {
            setPronounAntecedent(PronounThem, lst);
        }

        /* look up a pronoun's value */
        getPronounAntecedent(typ)
        {
            /* get the stored antecedent for this pronoun */
            return antecedentTable[typ];
        }

        /* set a pronoun's antecedent value */
        setPronounAntecedent(typ, val)
        {
            /* remember the value in the antecedent table */
            antecedentTable[typ] = val;

            /* set the same value for the possessive anaphor */
            possAnaphorTable[typ] = val;
        }

        /* set a possessive anaphor value */
        setPossAnaphor(typ, val)
        {
            /* set the value in the possessive anaphor table only */
            possAnaphorTable[typ] = val;
        }

        /* get a possessive anaphor value */
        getPossAnaphor(typ) { return possAnaphorTable[typ]; }

        /* forget the possessive anaphors */
        forgetPossAnaphors()
        {
            /* copy all of the antecedents to the possessive anaphor table */
            antecedentTable.forEachAssoc(
                {key, val: possAnaphorTable[key] = val});
        }

        /*
         *   Copy pronoun antecedents from the given actor.  This should be
         *   called whenever an actor issues a command to us, so that pronouns
         *   in the command are properly resolved relative to the issuer.
         */
        copyPronounAntecedentsFrom(issuer)
        {
            /* copy every element from the issuer's table */
            issuer.antecedentTable.forEachAssoc(
                {key, val: setPronounAntecedent(key, val)});
        }

        /* -------------------------------------------------------------------- */
        /*
         *   Verb processing
         */

        /* show a "take from" message as indicating I don't have the dobj */
        takeFromNotInMessage = &takeFromNotInActorMsg

        /* verify() handler to check against applying an action to 'self' */
        verifyNotSelf(msg)
        {
            /* check to make sure we're not trying to do this to myself */
            if (self == gActor)
                illogicalSelf(msg);
        }

        /* macro to verify we're not self, and inherit the default behavior */
    #define verifyNotSelfInherit(msg) \
        verify() \
        { \
            verifyNotSelf(msg); \
            inherited(); \
        }

        /*
         *   For the basic physical manipulation verbs (TAKE, DROP, PUT ON,
         *   etc), it's illogical to operate on myself, so check for this in
         *   verify().  Otherwise, handle these as we would ordinary objects,
         *   since we might be able to manipulate other actors in the normal
         *   manner, especially actors small enough that we can pick them up.
         */
        dobjFor(Take) { verifyNotSelfInherit(&takingSelfMsg) }
        dobjFor(Drop) { verifyNotSelfInherit(&droppingSelfMsg) }
        dobjFor(PutOn) { verifyNotSelfInherit(&puttingSelfMsg) }
        dobjFor(PutUnder) { verifyNotSelfInherit(&puttingSelfMsg) }
        dobjFor(Throw) { verifyNotSelfInherit(&throwingSelfMsg) }
        dobjFor(ThrowAt) { verifyNotSelfInherit(&throwingSelfMsg) }
        dobjFor(ThrowDir) { verifyNotSelfInherit(&throwingSelfMsg) }
        dobjFor(ThrowTo) { verifyNotSelfInherit(&throwingSelfMsg) }

        /* customize the message for THROW TO <actor> */
        iobjFor(ThrowTo)
        {
            verify()
            {
                /* by default, we don't want to catch anything */
                illogical(&willNotCatchMsg, self);
            }
        }

        /* treat PUT SELF IN FOO as GET IN FOO */
        dobjFor(PutIn)
        {
            verify()
            {
                /* the target actor is always unsuitable as a default */
                if (gActor == self)
                    nonObvious;
            }

            check()
            {
                /* if I'm putting myself somewhere, treat it as GET IN */
                if (gActor == self)
                    replaceAction(Enter, gIobj);

                /* do the normal work */
                inherited();
            }
        }

        dobjFor(Kiss)
        {
            preCond = [touchObj]
            verify()
            {
                /* cannot kiss oneself */
                verifyNotSelf(&cannotKissSelfMsg);
            }
            action() { mainReport(&cannotKissActorMsg); }
        }

        dobjFor(AskFor)
        {
            preCond = [canTalkToObj]
            verify()
            {
                /* it makes no sense to ask myself for something */
                verifyNotSelf(&cannotAskSelfForMsg);
            }
            action()
            {
                /* note that the issuer is targeting us with conversation */
                gActor.noteConversation(self);

                /* let the state object handle it */
                curState.handleConversation(gActor, gTopic, askForConvType);
            }
        }

        dobjFor(TalkTo)
        {
            preCond = [canTalkToObj]
            verify()
            {
                /* it's generally illogical to talk to oneself */
                verifyNotSelf(&cannotTalkToSelfMsg);
            }
            action()
            {
                /* note that the issuer is targeting us in conversation */
                gActor.noteConversation(self);

                /* handle it as a 'hello' topic */
                curState.handleConversation(gActor, helloTopicObj, helloConvType);
            }
        }

        iobjFor(GiveTo)
        {
            verify()
            {
                /* it makes no sense to give something to myself */
                verifyNotSelf(&cannotGiveToSelfMsg);

                /* it also makes no sense to give something to itself */
                if (gDobj == gIobj)
                    illogicalSelf(&cannotGiveToItselfMsg);
            }
            action()
            {
                /* take note that I've seen the direct object */
                noteObjectShown(gDobj);

                /* note that the issuer is targeting us with conversation */
                gActor.noteConversation(self);

                /* let the state object handle it */
                curState.handleConversation(gActor, gDobj, giveConvType);
            }
        }

        iobjFor(ShowTo)
        {
            verify()
            {
                /* it makes no sense to show something to myself */
                verifyNotSelf(&cannotShowToSelfMsg);

                /* it also makes no sense to show something to itself */
                if (gDobj == gIobj)
                    illogicalSelf(&cannotShowToItselfMsg);
            }
            action()
            {
                /* take note that I've seen the direct object */
                noteObjectShown(gDobj);

                /* note that the issuer is targeting us with conversation */
                gActor.noteConversation(self);

                /* let the actor state object handle it */
                curState.handleConversation(gActor, gDobj, showConvType);
            }
        }

        /*
         *   Note that the given object has been explicitly shown to me.  By
         *   default, we'll mark the object and its visible contents as having
         *   been seen by me.  This is called whenever we're the target of a
         *   SHOW TO or GIVE TO, since presumably such an explicit act of
         *   calling our attention to an object would make us consider the
         *   object as having been seen in the future.
         */
        noteObjectShown(obj)
        {
            local info;

            /* get the table of things we can see */
            info = visibleInfoTable();

            /* if the object is in the table, mark it as seen */
            if (info[obj] != nil)
                setHasSeen(obj);

            /* also mark the visible contents of the object as having been seen */
            obj.setContentsSeenBy(info, self);
        }

        dobjFor(AskAbout)
        {
            preCond = [canTalkToObj]
            verify()
            {
                /* it makes no sense to ask oneself about something */
                verifyNotSelf(&cannotAskSelfMsg);
            }
            action()
            {
                /* note that the issuer is targeting us with conversation */
                gActor.noteConversation(self);

                /* let our state object handle it */
                curState.handleConversation(gActor, gTopic, askAboutConvType);
            }
        }

        dobjFor(TellAbout)
        {
            preCond = [canTalkToObj]
            verify()
            {
                /* it makes no sense to tell oneself about something */
                verifyNotSelf(&cannotTellSelfMsg);
            }
            check()
            {
                /*
                 *   If the direct object is the issuing actor, rephrase this
                 *   as "issuer, ask actor about iobj".
                 *
                 *   Note that we do this in 'check' rather than 'action',
                 *   because this will ensure that we'll rephrase the command
                 *   properly even if the subclass overrides with its own
                 *   check, AS LONG AS the overriding method inherits this base
                 *   definition first.  If we did the rephrasing in the
                 *   'action', then an overriding 'check' might incorrectly
                 *   disqualify the operation on the assumption that it's an
                 *   ordinary TELL ABOUT rather than what it really is, which
                 *   is a rephrased ASK ABOUT.
                 */
                if (gDobj == gIssuingActor)
                    replaceActorAction(gIssuingActor, AskAbout, gActor, gTopic);

            }
            action()
            {
                /* note that the issuer is targeting us with conversation */
                gActor.noteConversation(self);

                /* let the state object handle it */
                curState.handleConversation(gActor, gTopic, tellAboutConvType);
            }
        }

        /*
         *   Handle a conversational command.  All of the conversational
         *   actions (HELLO, GOODBYE, YES, NO, ASK ABOUT, ASK FOR, TELL ABOUT,
         *   SHOW TO, GIVE TO) are routed here when we're the target of the
         *   action (for example, we're BOB in ASK BOB ABOUT TOPIC) AND the
         *   ActorState doesn't want to handle the action.
         */
        handleConversation(actor, topic, convType)
        {
            /* try handling the topic from our topic database */
            if (!handleTopic(actor, topic, convType, nil))
            {
                /* the topic database didn't handle it; use a default response */
                defaultConvResponse(actor, topic, convType);
            }
        }

        /*
         *   Show a default response to a conversational action.  By default,
         *   we'll show the default response for our conversation type.
         */
        defaultConvResponse(actor, topic, convType)
        {
            /* call the appropriate default response for the ConvType */
            convType.defaultResponse(self, actor, topic);
        }

        /*
         *   Show our default greeting message - this is used when the given
         *   another actor greets us with HELLO or TALK TO, and we don't
         *   otherwise handle it (such as via a topic database entry).
         *
         *   By default, we'll just show "there's no response" as a default
         *   message.  We'll show this in default mode, so that if the caller
         *   is going to show a list of suggested conversation topics (which
         *   the 'hello' and 'talk to' commands will normally try to do), the
         *   topic list will override the "there's no response" default.  In
         *   other words, we'll have one of these two types of exchanges:
         *
         *.  >talk to bob
         *.  There's no response
         *
         *.  >talk to bill
         *.  You could ask him about the candle, the book, or the bell, or
         *.  tell him about the crypt.
         */
        defaultGreetingResponse(actor)
            { defaultReport(&noResponseFromMsg, self); }

        /* show our default goodbye message */
        defaultGoodbyeResponse(actor)
            { mainReport(&noResponseFromMsg, self); }

        /*
         *   Show the default answer to a question - this is called when we're
         *   the actor in ASK <actor> ABOUT <topic>, and we can't find a more
         *   specific response for the given topic.
         *
         *   By default, we'll show the basic "there's no response" message.
         *   This isn't a very good message in most cases, because it makes an
         *   actor pretty frustratingly un-interactive, which gives the actor
         *   the appearance of a cardboard cut-out.  But there's not much
         *   better that the library can do; the potential range of actors
         *   makes a more specific default response impossible.  If the default
         *   response were "I don't know about that," it wouldn't work very
         *   well if the actor is someone who only speaks Italian.  So, the
         *   best we can do is this generally rather poor default.  But that
         *   doesn't mean that authors should resign themselves to a poor
         *   default answer; instead, it means that actors should take care to
         *   override this when defining an actor, because it's usually
         *   possible to find a much better default for a *specific* actor.
         *
         *   The *usual* way of providing a default response is to define a
         *   DefaultAskTopic (or a DefaultAskTellTopic) and put it in the
         *   actor's topic database.
         */
        defaultAskResponse(fromActor, topic)
            { mainReport(&noResponseFromMsg, self); }

        /*
         *   Show the default response to being told of a topic - this is
         *   called when we're the actor in TELL <actor> ABOUT <topic>, and we
         *   can't find a more specific response for the topic.
         *
         *   As with defaultAskResponse, this should almost always be
         *   overridden by each actor, since the default response ("there's no
         *   response") doesn't make the actor seem very dynamic.
         *
         *   The usual way of providing a default response is to define a
         *   DefaultTellTopic (or a DefaultAskTellTopic) and put it in the
         *   actor's topic database.
         */
        defaultTellResponse(fromActor, topic)
            { mainReport(&noResponseFromMsg, self); }

        /* the default response for SHOW TO */
        defaultShowResponse(byActor, topic)
            { mainReport(&notInterestedMsg, self); }

        /* the default response for GIVE TO */
        defaultGiveResponse(byActor, topic)
            { mainReport(&notInterestedMsg, self); }

        /* the default response for ASK FOR */
        defaultAskForResponse(byActor, obj)
            { mainReport(&noResponseFromMsg, self); }

        /* default response to being told YES */
        defaultYesResponse(fromActor)
            { mainReport(&noResponseFromMsg, self); }

        /* default response to being told NO */
        defaultNoResponse(fromActor)
            { mainReport(&noResponseFromMsg, self); }

        /* default refusal of a command */
        defaultCommandResponse(fromActor, topic)
            { mainReport(&refuseCommand, self, fromActor); }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An UntakeableActor is one that can't be picked up and moved.
     */
    class UntakeableActor: Actor, Immovable
        /* use customized messages for some 'Immovable' methods */
        cannotTakeMsg = &cannotTakeActorMsg
        cannotMoveMsg = &cannotMoveActorMsg
        cannotPutMsg = &cannotPutActorMsg

        /* TASTE tends to be a bit rude */
        dobjFor(Taste)
        {
            action() { mainReport(&cannotTasteActorMsg); }
        }

        /*
         *   even though we act like an Immovable, we don't count as an
         *   Immovable for listing purposes
         */
        contentsInFixedIn(loc) { return nil; }
    ;

    /*
     *   A Person is an actor that represents a human character.  This is just
     *   an UntakeableActor with some custom versions of the messages for
     *   taking and moving the actor.
     */
    class Person: UntakeableActor
        /* customize the messages for trying to take or move me */
        cannotTakeMsg = &cannotTakePersonMsg
        cannotMoveMsg = &cannotMovePersonMsg
        cannotPutMsg = &cannotPutPersonMsg
        cannotTasteActorMsg = &cannotTastePersonMsg

        /*
         *   use a fairly large default bulk, since people are usually fairly
         *   large compared with the sorts of items that one carries around
         */
        bulk = 10
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Pending response information structure
     */
    class PendingResponseInfo: object
        construct(issuer, prop, args)
        {
            issuer_ = issuer;
            prop_ = prop;
            args_ = args;
        }

        /* the issuer of the command (and target of the response) */
        issuer_ = nil

        /* the message property and argument list for the message */
        prop_ = nil
        args_ = []
    ;

    /*
     *   Pending Command Information structure.  This is an abstract base class
     *   that we subclass for particular ways of representing the command to be
     *   executed.
     */
    class PendingCommandInfo: object
        construct(issuer) { issuer_ = issuer; }

        /*
         *   Check to see if this pending command item has a command to
         *   perform.  This returns true if we have a command, nil if we're
         *   just a queue placeholder without any actual command to execute.
         */
        hasCommand = true

        /* execute the command */
        executePending(targetActor) { }

        /* the issuer of the command */
        issuer_ = nil

        /* we're at the start of a "sentence" */
        startOfSentence_ = nil
    ;

    /* a pending command based on a list of tokens from an input string */
    class PendingCommandToks: PendingCommandInfo
        construct(startOfSentence, issuer, toks)
        {
            inherited(issuer);

            startOfSentence_ = startOfSentence;
            tokens_ = toks;
        }

        /*
         *   Execute the command.  We'll parse our tokens and execute the
         *   parsed results.
         */
        executePending(targetActor)
        {
            /* parse and execute the tokens */
            executeCommand(targetActor, issuer_, tokens_, startOfSentence_);
        }

        /* the token list for the command */
        tokens_ = nil
    ;

    /* a pending command based on a pre-resolved Action and its objects */
    class PendingCommandAction: PendingCommandInfo
        construct(startOfSentence, issuer, action, [objs])
        {
            inherited(issuer);

            startOfSentence_ = startOfSentence;
            action_ = action;
            objs_ = objs;
        }

        /* execute the pending command */
        executePending(targetActor)
        {
            /* invoke the action's main execution method */
            try
            {
                /* run the action */
                newActionObj(CommandTranscript, issuer_,
                             targetActor, action_, objs_...);
            }
            catch (TerminateCommandException tcExc)
            {
                /*
                 *   the command cannot proceed; simply abandon the command
                 *   action here
                 */
            }
        }

        /* the resolved Action to perform */
        action_ = nil

        /* the resolved objects for the action */
        objs_ = nil
    ;

    /*
     *   A pending command marker.  This is not an actual pending command;
     *   rather, it's just a queue marker.  We sometimes want to synchronize
     *   some other activity with an actor's progress through its command
     *   queue; for example, we might want one actor to wait until another
     *   actor has executed a particular pending action.  These markers can be
     *   used for this kind of synchronization; they move through the queue
     *   like ordinary pending commands, so we can tell if an actor has reached
     *   a particular command by observing the marker's progress through the
     *   queue.
     */
    class PendingCommandMarker: PendingCommandInfo
        /* I have no command to execute */
        hasCommand = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Set the current player character
     */
    setPlayer(actor)
    {
        /* remember the new player character */
        libGlobal.playerChar = actor;

        /* set the root global point of view to this actor */
        setRootPOV(actor, actor);
    }

TADS 3 Library Manual
Generated on 8/21/2012 from TADS version 3.1.2