# action.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library: Actions.
     *
     *   This module defines the Action classes.  An Action is an abstract
     *   object representing a command to be performed.
     */

    #include "adv3.h"
    #include "tok.h"

    /* ------------------------------------------------------------------------ */
    /*
     *   Object associations lists.  We use this object to store some lookup
     *   tables that we build during preinitialization to relate object usages
     *   (DirectObject, IndirectObject) to certain properties.
     */
    objectRelations: PreinitObject
        /* preinitialization - build the lookup tables */
        execute()
        {
            /* build the default pre-condition properties table */
            preCondDefaultProps[DirectObject] = &preCondDobjDefault;
            preCondDefaultProps[IndirectObject] = &preCondIobjDefault;

            /* build the catch-all pre-conditions properties table */
            preCondAllProps[DirectObject] = &preCondDobjAll;
            preCondAllProps[IndirectObject] = &preCondIobjAll;

            /* build the default verification properties table */
            verifyDefaultProps[DirectObject] = &verifyDobjDefault;
            verifyDefaultProps[IndirectObject] = &verifyIobjDefault;

            /* build the catch-all verification properties table */
            verifyAllProps[DirectObject] = &verifyDobjAll;
            verifyAllProps[IndirectObject] = &verifyIobjAll;

            /* build the default check properties table */
            checkDefaultProps[DirectObject] = &checkDobjDefault;
            checkDefaultProps[IndirectObject] = &checkIobjDefault;

            /* build the catch-all check properties table */
            checkAllProps[DirectObject] = &checkDobjAll;
            checkAllProps[IndirectObject] = &checkIobjAll;

            /* build the default action properties table */
            actionDefaultProps[DirectObject] = &actionDobjDefault;
            actionDefaultProps[IndirectObject] = &actionIobjDefault;

            /* build the catch-all check properties table */
            actionAllProps[DirectObject] = &actionDobjAll;
            actionAllProps[IndirectObject] = &actionIobjAll;
        }

        /* lookup table for default precondition properties */
        preCondDefaultProps = static new LookupTable()

        /* lookup table for catch-all precondition properties */
        preCondAllProps = static new LookupTable()

        /* lookup table for default verification properties */
        verifyDefaultProps = static new LookupTable()

        /* lookup table for catch-all verification properties */
        verifyAllProps = static new LookupTable()

        /* lookup table for default check properties */
        checkDefaultProps = static new LookupTable()

        /* lookup table for catch-all check properties */
        checkAllProps = static new LookupTable()

        /* lookup table for default action properties */
        actionDefaultProps = static new LookupTable()

        /* lookup table for catch-all action properties */
        actionAllProps = static new LookupTable()
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Invoke the given function with the given values for the parser global
     *   variables gActor and gAction.
     */
    withParserGlobals(issuer, actor, action, func)
    {
        local oldIssuer;
        local oldActor;
        local oldAction;

        /* remember the old action and actor, so we can restore them later */
        oldIssuer = gIssuingActor;
        oldActor = gActor;
        oldAction = gAction;

        /* establish our actor and action as the global settings */
        gIssuingActor = issuer;
        gActor = actor;
        gAction = action;

        /* make sure we restore globals on the way out */
        try
        {
            /* invoke the callback and return the result */
            return (func)();
        }
        finally
        {
            /* restore the globals we changed */
            gActor = oldActor;
            gIssuingActor = oldIssuer;
            gAction = oldAction;
        }
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Pre-condition Descriptor.  This object encapsulates a precondition
     *   object and the argument we want to pass to its condition check method.
     */
    class PreCondDesc: object
        construct(cond, arg)
        {
            /* remember the condition and the check argument */
            cond_ = cond;
            arg_ = arg;
        }

        /* check the precondition */
        checkPreCondition(allowImplicit)
        {
            /* call the precondition's check method with the argument we stored */
            return cond_.checkPreCondition(arg_, allowImplicit);
        }

        /* the precondition object */
        cond_ = nil

        /* the check argument */
        arg_ = nil

        /* our list sorting index */
        index_ = 0
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Basic Action class.  An Action is the language-independent definition
     *   of the abstract action of a command.
     */
    class Action: BasicProd
        /*
         *   Are we the given kind of action?  By default, this simply returns
         *   true if we're of the given action class.
         */
        actionOfKind(cls) { return ofKind(cls); }

        /*
         *   Reset the action in preparation for re-execution.  This should
         *   discard any scoped context from any past execution of the
         *   command, such as cached scope information.
         */
        resetAction()
        {
            /* forget any past successful verification passes */
            verifiedOkay = [];
        }

        /*
         *   Repeat the action, for an AGAIN command.
         */
        repeatAction(lastTargetActor, lastTargetActorPhrase,
                     lastIssuingActor, countsAsIssuerTurn)
        {
            /* execute the command */
            executeAction(lastTargetActor, lastTargetActorPhrase,
                          lastIssuingActor, countsAsIssuerTurn, self);
        }

        /*
         *   Cancel iteration of the action.  This can be called during the
         *   'check' or 'action' phases of executing this action.  It tells
         *   the action that we want to stop executing the action when we're
         *   finished with the current object.
         *
         *   Note that this doesn't cause a jump out of the current code, so
         *   it's not like 'exit' or the other termination signals.  Instead,
         *   this simply tells the action to proceed normally for the
         *   remainder of the processing for the current object, and then act
         *   as though there were no more objects to iterate over, ending the
         *   command normally.  If you want to cut off the remainder of the
         *   execution cycle for the current object, you can use 'exit' (for
         *   example) immediately after calling this method.
         */
        cancelIteration() { iterationCanceled = true; }

        /* internal flag: object iteration has been canceled */
        iterationCanceled = nil

        /*
         *   Create an instance of this action, for use by a recursive or
         *   programmatically-generated command.
         *
         *   The generic actions defined in the library are always subclassed
         *   by language-specific library modules, because the language
         *   modules have to define the grammar rules for the verbs - we can't
         *   define the grammar rules generically because the verbs wouldn't
         *   be reusable for non-English translations if we did.  As a result,
         *   library code referring to one of the library verbs by name, say
         *   TakeAction, doesn't get a language-specific subclass of the verb,
         *   but just gets the language-independent base class.
         *
         *   However, to make full use of an Action object in a recursive
         *   command, we do need a final language-specific subclass - without
         *   this, we won't be able to generate text describing the command,
         *   for example.  This method bridges this gap by finding a suitable
         *   language-specific subclass of the given action, then creating an
         *   instance of that subclass rather than an instance of the base
         *   class.
         *
         *   By default, we'll take any subclass of this action that is itself
         *   a class.  However, if any subclass has the property
         *   defaultForRecursion set to true, we'll use that class
         *   specifically - this lets the language module designate a
         *   particular subclass to use as the default for recursive commands,
         *   which might be desirable in cases where the language module
         *   defines more than one subclass of an action.
         */
        createActionInstance()
        {
            local found;

            /*
             *   Iterate over our subclasses.  Initialize 'found' to this base
             *   class, so that if we fail to find any subclasses, we'll at
             *   least be able to create an instance of the generic base
             *   class.
             */
            for (local cur = firstObj(self, ObjClasses), found = self ;
                 cur != nil ; cur = nextObj(cur, self, ObjClasses))
            {
                /*
                 *   if this one is marked as a default for recursion, and the
                 *   last one we found isn't, choose this one over the last
                 *   one we found
                 */
                if (cur.defaultForRecursion && !found.defaultForRecursion)
                    found = cur;

                /*
                 *   If this one is a subclass of the last one we found, pick
                 *   it instead of the last one.  We always want a final
                 *   subclass here, never an intermediate class, so if we find
                 *   a subclass of the one we've tentatively picked, we know
                 *   that the tentative selection isn't final after all.
                 */
                if (cur.ofKind(found))
                    found = cur;
            }

            /* return a new instance of what we found */
            return found.createInstance();
        }

        /*
         *   Create an instance of this action based on another action.  We'll
         *   copy the basic properties of the original action.
         */
        createActionFrom(orig)
        {
            local action;

            /* create a new instance of this action */
            action = createActionInstance();

            /* copy the token list information to the new action */
            action.tokenList = orig.tokenList;
            action.firstTokenIndex = orig.firstTokenIndex;
            action.lastTokenIndex = orig.lastTokenIndex;

            /* the new action is implicit if the original was */
            if (orig.isImplicit)
                action.setImplicit(orig.implicitMsg);

            /* return the new action */
            return action;
        }

        /*
         *   Mark the command as implicit.  'msgProp' is the property (of
         *   gLibMessages) to use to announce the implicit command.
         */
        setImplicit(msgProp)
        {
            /* mark ourselves as implicit */
            isImplicit = true;

            /* do not show default reports for implicit commands */
            showDefaultReports = nil;

            /* all implicit commands are nested */
            setNested();

            /*
             *   do not include implicit commands in undo - since an implicit
             *   command is only a subpart of an explicit command, an implicit
             *   command is not undone individually but only as part of the
             *   enclosing explicit command
             */
            includeInUndo = nil;

            /* remember the implicit command announcement message property */
            implicitMsg = msgProp;
        }

        /*
         *   Mark the command as nested, noting the parent action (which we
         *   take as the global current action).
         */
        setNested()
        {
            /* remember the parent action */
            parentAction = gAction;
        }

        /*
         *   Determine if I'm nested within the given action.  Returns true if
         *   the given action is my parent action, or if my parent action is
         *   nested within the given action.
         */
        isNestedIn(action)
        {
            /* if my parent action is the given action, I'm nested in it */
            if (parentAction == action)
                return true;

            /*
             *   if I have a parent action, and it's nested in the given
             *   action, then I'm nested in the given action because I'm
             *   nested in anything my parent is nested in
             */
            if (parentAction != nil && parentAction.isNestedIn(action))
                return true;

            /* we're not nested in the given action */
            return nil;
        }

        /*
         *   Set the "original" action.  An action with an original action is
         *   effectively part of the original action for the purposes of its
         *   reported results.
         *
         *   An action has an original action if it's a nested or replacement
         *   action for an action.
         */
        setOriginalAction(action)
        {
            /* remember my original action */
            originalAction = action;
        }

        /*
         *   Get the "original" action.  If I'm a replacement or nested action,
         *   this returns the original main action of which I'm a part, for
         *   reporting pruposes.
         *
         *   It's important to note that an implied action does NOT count as a
         *   nested or replacement action for the purposes of this method.
         *   That is, if a command A triggers an implied action B, which
         *   triggers a nested action C, and then after that command A itself
         *   triggers a nested action D, then
         *
         *   A.getOriginalAction -> A
         *.  B.getOriginalAction -> B (B is implied, not nested)
         *.  C.getOriginalAction -> C (C is nested within B)
         *.  D.getOriginalAction -> A (D is nested within A)
         *
         *   The purpose of the original action is to tell us, mainly for
         *   reporting purposes, what we're really trying to do with the
         *   action.  This allows reports to hide the internal details of how
         *   the action is carried out, and instead say what the action was
         *   meant to do from the player's perspective.
         */
        getOriginalAction()
        {
            /* if I have no other original action, I'm the original action */
            if (originalAction == nil)
                return self;

            /* return my original action's original action */
            return originalAction.getOriginalAction();
        }

        /*
         *   Determine if this action is "part of" the given action.  I'm part
         *   of the given action if I am the given action, or the given action
         *   is my "original" action, or my original action is part of the
         *   given action.
         */
        isPartOf(action)
        {
            /* if I'm the same as the given action, I'm obviously part of it */
            if (action == self)
                return true;

            /* if my original action is part of the action, I'm part of it */
            if (originalAction != nil && originalAction.isPartOf(action))
                return true;

            /* I'm not part of the given action */
            return nil;
        }

        /*
         *   Mark the action as "remapped."  This indicates that the action
         *   was explicitly remapped to a different action during the remap()
         *   phase.
         */
        setRemapped(orig) { remappedFrom = gAction; }

        /* determine if I'm remapped, and get the original action if so */
        isRemapped() { return remappedFrom != nil; }
        getRemappedFrom() { return remappedFrom; }

        /*
         *   Get the "simple synonym" remapping for one of our objects, if
         *   any.  'obj' is the resolved object to remap, and 'role' is the
         *   object role identifier (DirectObject, IndirectObject, etc).
         *   'remapProp' is the remapping property for the role; this is
         *   simply the result of our getRemapPropForRole(role), but we ask
         *   the caller to pass this in so that it can be pre-computed in
         *   cases where we'll called in a loop.
         *
         *   A simple synonym remapping is a remapTo that applies the same
         *   verb to a new object in the same role.  For example, if we remap
         *   OPEN DESK to OPEN DRAWER, then the drawer is the simple synonym
         *   remapping for the desk in an OPEN command.  A remapping is
         *   considered a simple synonym remapping only if we're remapping to
         *   the same action, AND the new object is in the same action role as
         *   the original object was.
         *
         *   If there's no simple synonym remapping, we'll return nil.
         */
        getSimpleSynonymRemap(obj, role, remapProp)
        {
            local mapping;
            local remapIdx;

            /* if the object isn't remapped at all, there's no remapping */
            if ((mapping = obj.(remapProp)) == nil)
                return nil;

            /* if the mapping isn't to the same action, it's not a synonym */
            if (!ofKind(mapping[1]))
                return nil;

            /*
             *   Find the specific (non-role) object in the remap vector -
             *   this is the entry that's actually an object rather than a
             *   role identifier.  (The remapping vector is required to have
             *   exactly one such entry.  Look from the right end of the list,
             *   since the first entry is always the new action, which is
             *   itself an object, but not the object we're looking for.)
             */
            remapIdx = mapping.lastIndexWhich({x: dataType(x) == TypeObject});
            if (remapIdx == nil)
                return nil;

            /*
             *   Determine if the object plays the same role in the new action
             *   as the original object did in the original action.  It does
             *   if it's at the index in the mapping vector of our object role
             *   in the action.  Note that the mapping vector has slot 1
             *   filled with the action, so its objects are actually one slot
             *   higher than they are in the action itself.
             *
             *   If the new object isn't in the same role, then this isn't a
             *   simple synonym remapping.
             */
            if (getRoleFromIndex(remapIdx - 1) != role)
                return nil;

            /*
             *   We have the same action applied to a new object in the same
             *   role as the original object, so this is a simple synonym
             *   remapping.  Return the new object we're mapping to.
             */
            return mapping[remapIdx];
        }

        /*
         *   the defaultForRecursion flag must be explicitly set in subclasses
         *   when desired - by default we'll use any language-specific
         *   subclass of an Action for recursive commands
         */
        defaultForRecursion = nil

        /*
         *   Flag: the command is implicit.  An implicit command is one that
         *   is performed as an implied enabling step of another command - for
         *   example, if an actor wants to throw something, the actor must be
         *   holding the object, so will implicitly try to take the object.
         */
        isImplicit = nil

        /*
         *   The parent action.  If the command is performed programmatically
         *   in the course of executing another command, this is set to the
         *   enclosing action.
         *
         *   Note that while all implicit commands are nested, not all nested
         *   commands are implicit.  A nested command may simply be a
         *   component of another command, or another command may be handled
         *   entirely by running a different command as a nested command.  In
         *   any case, a nested but non-implicit command does not appear to
         *   the player as a separate command; it is simply part of the
         *   original command.
         */
        parentAction = nil

        /*
         *   The original action we were remapped from.  This is valid when
         *   the action was explicitly remapped during the remap() phase to a
         *   different action.
         */
        remappedFrom = nil

        /*
         *   the original action - we are effectively part of the original
         *   action for reporting purposes
         */
        originalAction = nil

        /* the libMessage property, if any, to announce the implicit command */
        implicitMsg = nil

        /*
         *   Flag: we are to show default reports for this action.  In most
         *   cases we will do so, but for some types of commands (such as
         *   implicit commands), we suppress default reports.
         */
        showDefaultReports = true

        /*
         *   Get a message parameter object for the action.  Each action
         *   subclass defines this to return its objects according to its own
         *   classifications.  The default action has no objects, but
         *   recognizes 'actor' as the current command's actor.
         */
        getMessageParam(objName)
        {
            switch(objName)
            {
            case 'pc':
                /* return the player character */
                return gPlayerChar;

            case 'actor':
                /* return the current actor */
                return gActor;

            default:
                /*
                 *   if we have an extra message parameters table, look up the
                 *   parameter name in the table
                 */
                if (extraMessageParams != nil)
                    return extraMessageParams[objName];

                /* we don't recognize other names */
                return nil;
            }
        }

        /*
         *   Define an extra message-specific parameter.  Message processors
         *   can use this to add their own special parameters, so that they
         *   can refer to parameters that aren't involved directly in the
         *   command.  For example, a message for "take <dobj>" might want to
         *   refer to the object containing the direct object.
         */
        setMessageParam(objName, obj)
        {
            /*
             *   if we don't yet have an extra message parameters table,
             *   create a small lookup table for it
             */
            if (extraMessageParams == nil)
                extraMessageParams = new LookupTable(8, 8);

            /* add the parameter to the table, indexing by the parameter name */
            extraMessageParams[objName.toLower()] = obj;
        }

        /*
         *   For convenience, this method allows setting any number of
         *   name/value pairs for message parameters.
         */
        setMessageParams([lst])
        {
            /* set each pair from the argument list */
            for (local i = 1, local len = lst.length() ; i+1 <= len ; i += 2)
                setMessageParam(lst[i], lst[i+1]);
        }

        /*
         *   Synthesize a global message parameter name for the given object.
         *   We'll store the association and return the synthesized name.
         */
        synthMessageParam(obj)
        {
            local nm;

            /* synthesize a name */
            nm = 'synth' + toString(synthParamID++);

            /* store the association */
            setMessageParam(nm, obj);

            /* return the synthesized name */
            return nm;
        }

        /* synthesized message object parameter serial number */
        synthParamID = 1

        /*
         *   Extra message parameters.  If a message processor wants to add
         *   special message parameters of its own, we'll create a lookup
         *   table for the extra parameters.  Message processors might want to
         *   add their own special parameters to allow referring to objects
         *   other than the main objects of the command.
         */
        extraMessageParams = nil

        /*
         *   Flag: this command is repeatable with 'again'.  Before executing
         *   a command, we'll save it for use by the 'again' command if this
         *   flag is true.
         */
        isRepeatable = true

        /*
         *   Flag: this command should be included in the undo records.  This
         *   is true for almost every command, but a few special commands
         *   (undo, save) are not subject to undo.
         */
        includeInUndo = true

        /*
         *   Flag: this is a "conversational" command, as opposed to an order.
         *   When this type of command is addressed to another character, it
         *   doesn't ask the other character to do anything, but rather engages
         *   the other character in conversation.  Examples:
         *
         *.  Bob, hello
         *.  Bob, goodbye
         *.  Bob, tell me about the planet
         *.  Bob, yes
         *.  Bob, no
         *
         *   ("Tell me about..." is a little different from the others.  We
         *   treat it as conversational because it means the same thing as "ask
         *   Bob about...", which we consider conversational because it would
         *   be rendered in real life as a question.  In other words, the
         *   action involves the issuing actor stating the question, which
         *   means that issuing actor is the one doing the physical work of the
         *   action.  "Tell me about..." could be seen as an order, but it
         *   seems more appropriate to view it as simply an alternative way of
         *   phrasing a question.)
         *
         *   The issuing actor is passed as a parameter because some actions
         *   are conversational only in some cases; "tell me about the planet"
         *   is conversational, but "tell Bill about the planet" isn't, since
         *   the latter doesn't ask Bob a question but orders Bob to talk to
         *   Bill.
         *
         *   When the issuing actor and target actor are the same, this is
         *   irrelevant.  The purpose of this is to distinguish orders given to
         *   another character from conversational overtures directed to the
         *   other character, so if the command is coming from the player and
         *   bound for the player character, there's obviously no conversation
         *   going on.
         *
         *   Note also that, contrary to what one might think at first glance,
         *   a command like ASK ABOUT is NOT conversational; it's a command to
         *   ask someone about something, and isn't itself a conversational
         *   overture.  The act of asking is itself a conversational overture,
         *   but the asking is the *result* of the command, not the command
         *   itself.  An action is only conversational if the action itself is
         *   a conversational overture.  So, "BOB, HELLO" is conversational;
         *   "BOB, ASK BILL ABOUT COMPUTER" is not, because it orders Bob to do
         *   something.
         */
        isConversational(issuingActor) { return nil; }

        /*
         *   Get the actual verb phrase the player typed in to generate this
         *   Action, expressed in a canonical format.  The canonical format
         *   consists of the lower-case version of all of the actual text the
         *   player typed, but with each noun phrase replaced by a standard
         *   placeholder token describing the slot.  The placeholder tokens are
         *   '(dobj)' for the direct object, '(iobj)' for the indirect object,
         *   '(literal)' for a literal text phrase, and '(topic)' for a topic
         *   phrase.
         *
         *   For example, if the player typed PUT BOOK AND PENCIL IN BOX, the
         *   canonical phrasing we return would be "put (dobj) in (iobj)".
         */
        getEnteredVerbPhrase()
        {
            local orig;
            local txt;

            /*
             *   If there's an original action, let the original action do the
             *   work.  If we've been remapped, or if this is an implied
             *   action, we won't necessarily have been constructed from the
             *   actual player input, so we need to go back to the original
             *   action for this information.
             */
            if (getOriginalAction() != self)
                return getOriginalAction.getEnteredVerbPhrase();

            /* start with the original token list for the predicate */
            orig = getPredicate().getOrigTokenList();

            /* add each token to the result text */
            for (txt = '', local i = 1, local len = orig.length() ;
                 i <= len ; ++i)
            {
                local foundNp;

                /* add a space if this isn't the first element */
                if (i > 1)
                    txt += ' ';

                /*
                 *   Check to see if we're entering one of the noun-phrase
                 *   slots.  We are if we've reached the first token of one of
                 *   the predicate noun phrases.
                 */
                foundNp = nil;
                foreach (local npProp in predicateNounPhrases)
                {
                    local match;

                    /* check to see if we're at this noun phrase */
                    if ((match = self.(npProp)) != nil
                        && i == match.firstTokenIndex)
                    {
                        /*
                         *   we're entering this noun phrase - add the generic
                         *   placeholder token for the noun phrase
                         */
                        txt += (npProp == &dobjMatch ? '(dobj)' :
                                npProp == &iobjMatch ? '(iobj)' :
                                npProp == &topicMatch ? '(topic)' :
                                npProp == &literalMatch ? '(literal)' :
                                '(other)');

                        /* skip the entire run of tokens for the noun phrase */
                        i = match.lastTokenIndex;

                        /* note that we found a noun phrase */
                        foundNp = true;

                        /* stop looking for a noun phrase */
                        break;
                    }
                }

                /*
                 *   if we didn't find a noun phrase, this token is a literal
                 *   part of the predicate grammar, so add it as-is
                 */
                if (!foundNp)
                    txt += getTokVal(orig[i]);
            }

            /* return the phrase, with everything converted to lower-case */
            return txt.toLower();
        }

        /*
         *   Get the grammar match tree object for the predicate that was used
         *   to enter this command.  By default, if we have an original action,
         *   we return the original action; otherwise we just return 'self'.
         *
         *   Language libraries must override this to return the original match
         *   tree object if Actions are separate from predicate match trees.
         *
         *   (The 'predicate' is simply the grammar match tree object for the
         *   entire verb phrase from the player's actual command entry text
         *   that matched this Action.  For example, in "BOB, TAKE BOX", the
         *   predicate is the match tree for the "TAKE BOX" part.  In "BOB,
         *   TAKE BOX AND GO NORTH", the predicate for the Take action is still
         *   the "TAKE BOX" part.  For "BOB, TAKE BOX AND BOOK AND GO NORTH",
         *   the predicate for the Take action is "TAKE BOX AND BOOK" - the
         *   full verb phrase includes any multiple-object lists in the
         *   original command.)
         */
        getPredicate()
        {
            /*
             *   By default, we just return the original Action - we assume
             *   that the language library defines Action subclasses as the
             *   actual match tree objects for predicate grammars.  Language
             *   modules must override this if they use separate object types
             *   for the predicate match tree objects.
             */
            return getOriginalAction();
        }

        /*
         *   Get the noun-phrase information for my predicate grammar.  This
         *   returns a list of the match-tree properties for the noun-phrase
         *   sub-productions in our predicate grammar.  The properties
         *   generally include &dobjMatch, &iobjMatch, &literalMatch, and
         *   &topicMatch.  The order of the slots isn't important; they simply
         *   tell us which ones we should find in our predicate grammar match.
         *
         *   The base Action is intransitive, so it doesn't have any
         *   noun-phrase slots, hence this is an empty list.
         */
        predicateNounPhrases = []

        /*
         *   Get the object "role" identifier for the given index.  This
         *   returns the identifier (DirectObject, IndirectObject, etc.) for
         *   the object at the given slot index, as used in
         *   setResolvedObjects().  The first object is at index 1.
         */
        getRoleFromIndex(idx) { return nil; }

        /*
         *   Get the "other object" role - this is the complement of the given
         *   object role for a two-object action, and is used to determine the
         *   real role of the special OtherObject placeholder in a remapTo().
         *   This is only meaningful with actions of exactly two objects.
         */
        getOtherObjectRole(role) { return nil; }

        /* get the resolved object in a given role */
        getObjectForRole(role) { return nil; }

        /* get the match tree for the noun phrase in the given role */
        getMatchForRole(role) { return nil; }

        /* get the 'verify' property for a given object role */
        getVerifyPropForRole(role)
        {
            return nil;
        }

        /* get the 'preCond' property for a given object role */
        getPreCondPropForRole(role)
        {
            return nil;
        }

        /* get the 'remap' property for a given object role */
        getRemapPropForRole(role)
        {
            return nil;
        }

        /*
         *   Get the ResolveInfo for the given object in the current command.
         *   Since we don't have any objects at all, we'll simply return a new
         *   ResolveInfo wrapping the given object.  'cur' is the object we're
         *   looking for, and 'oldRole' is the role the object previously
         *   played in the action.
         */
        getResolveInfo(obj, oldRole) { return new ResolveInfo(obj, 0, nil); }

        /*
         *   Explicitly set the resolved objects.  This should be overridden
         *   in each subclass for the number of objects specific to the action
         *   (a simple transitive action takes one argument, an action with
         *   both a direct and indirect object takes two arguments, and so
         *   on).  The base action doesn't have any objects at all, so this
         *   takes no arguments.
         *
         *   This method is used to set up an action to be performed
         *   programmatically, rather than based on parsed input.  Since
         *   there's no parsed input in such cases, the objects are specified
         *   directly by the programmatic caller.
         */
        setResolvedObjects() { }

        /*
         *   Explicitly set the object match trees.  This sets the pre-resolved
         *   production match trees.  The arguments are given in the order of
         *   their roles in the action, using the same order that
         *   setResolvedObjects() uses.
         *
         *   The arguments to this routine can either be match tree objects,
         *   which we'll plug into our grammar tree in the respective roles
         *   exactly as given; or they can be ResolveInfo objects giving the
         *   desired resolutions, in which case we'll build the appropriate
         *   kind of PreResolvedProd for each one.  The types can be freely
         *   mixed.
         */
        setObjectMatches() { }

        /*
         *   Check that the resolved objects are all in scope.  Returns true if
         *   so, nil if not.
         *
         *   This routine is meant for use only for actions built
         *   programmatically using setResolvedObjects().  In particular, we
         *   assume that we have only one object in each slot.  For normal
         *   parser-built actions, this check isn't necessary, because the
         *   parser only resolves objects that are in scope in the first place.
         */
        resolvedObjectsInScope()
        {
            /* we have no objects at all, so there is nothing out of scope */
            return true;
        }

        /*
         *   Get the list of bindings for an anaphoric pronoun - this is a
         *   pronoun that refers back to an earlier noun phrase in the same
         *   sentence, such as HIMSELF in ASK BOB ABOUT HIMSELF.  These
         *   obviously make no sense for verbs that take one (or zero)
         *   objects, since there's no earlier phrase to refer back to; these
         *   should return nil to indicate that an anaphoric pronoun is simply
         *   nonsensical in such a context.  Actions of two or more objects
         *   should return a list of objects for the preceding noun phrase.
         *
         *   Actions of two or more objects should set this if possible to the
         *   resolved object list for the previous noun phrase, as a
         *   ResolveInfo list.
         *
         *   The tricky part is that some actions will resolve noun phrases in
         *   a different order than they appear in the actual command grammar;
         *   similarly, it's also possible that some non-English languages use
         *   cataphoric pronouns (i.e., pronouns that refer to noun phrases
         *   that appear later in the sentence).  To allow for these cases,
         *   return an empty list here if a binding is grammatically possible
         *   but not yet available because of the resolution order, and note
         *   internally that the parser asked us for an anaphoric binding.
         *   Afterwards, the action's resolver method must go back and perform
         *   *another* resolve pass on the noun phrase production that
         *   requested the anaphor binding.
         *
         *   'typ' is the PronounType specifier for the corresponding ordinary
         *   pronoun.  For 'himself', for example, typ will be PronounHim.
         */
        getAnaphoricBinding(typ) { return nil; }

        /*
         *   Set a special pronoun override.  This creates a temporary pronoun
         *   definition, which lasts as long as this action (and any nested
         *   actions).  The temporary definition overrides the normal meaning
         *   of the pronoun.
         *
         *   One example of where this is useful is in global action remapping
         *   cases where the target actor changes in the course of the
         *   remapping.  For example, if we remap BOB, GIVE ME YOUR BOOK to ASK
         *   BOB FOR YOUR BOOK, the YOUR qualifier should still refer to Bob
         *   even though the command is no longer addressing Bob directly.
         *   This routine can be used in this case to override the meaning of
         *   'you' so that it refers to Bob.
         */
        setPronounOverride(typ, val)
        {
            /* if we don't have an override table yet, create one */
            if (pronounOverride == nil)
                pronounOverride = new LookupTable(5, 10);

            /* add it to the table */
            pronounOverride[typ] = val;
        }

        /*
         *   Get any special pronoun override in effect for the action, as set
         *   via setPronounOverride().  This looks in our own override table
         *   for a definition; then, if we have no override of our own, we ask
         *   our parent action if we have one, then our original action.
         */
        getPronounOverride(typ)
        {
            local pro;

            /* check our own table */
            if (pronounOverride != nil
                && (pro = pronounOverride[typ]) != nil)
                return pro;

            /* we don't have anything in our own table; check our parent */
            if (parentAction != nil
                && (pro = parentAction.getPronounOverride(typ)) != nil)
                return pro;

            /* if still nothing, check with the original action */
            if (originalAction != nil
                && originalAction != parentAction
                && (pro = originalAction.getPronounOverride(typ)) != nil)
                return pro;

            /* we didn't find an override */
            return nil;
        }

        /*
         *   the pronoun override table - this is nil by default, which means
         *   that no overrides have been defined yet; we create a LookupTable
         *   upon adding the first entry to the table
         */
        pronounOverride = nil

        /* wrap an object with a ResolveInfo */
        makeResolveInfo(val)
        {
            /*
             *   if it's already a ResolveInfo object, return it as-is;
             *   otherwise, create a new ResolveInfo wrapper for it
             */
            if (dataType(val) == TypeObject && val.ofKind(ResolveInfo))
                return val;
            else
                return new ResolveInfo(val, 0, nil);
        }

        /*
         *   Convert an object or list of objects to a ResolveInfo list
         */
        makeResolveInfoList(val)
        {
            /* if we have a non-list collection, make it a list */
            if (dataType(val) == TypeObject && val.ofKind(Collection))
                val = val.toList();

            /* if it's nil or an empty list, return an empty list */
            if (val == nil || val == [])
                return [];

            /* see what we have */
            if (dataType(val) == TypeList)
            {
                /* it's a list - make a ResolveInfo for each item */
                return val.mapAll({x: makeResolveInfo(x)});
            }
            else
            {
                /* it's not a list - return a one-element ResolveInfo list */
                return [makeResolveInfo(val)];
            }
        }

        /*
         *   If the command is repeatable, save it for use by 'again'.
         */
        saveActionForAgain(issuingActor, countsAsIssuerTurn,
                           targetActor, targetActorPhrase)
        {
            /*
             *   Check to see if the command is repeatable.  It's repeatable if
             *   the base action is marked as repeatable, AND it's not nested,
             *   AND it's issued by the player character, AND it's either a PC
             *   command or it counts as an issuer turn.
             *
             *   Nested commands are never repeatable with 'again', since no
             *   one ever typed them in.
             *
             *   "Again" is strictly for the player's use, so it's repeatable
             *   only if this is the player's turn, as opposed to a scripted
             *   action by an NPC.  This is the player's turn only if the
             *   command was issued by the player character (which means it
             *   came from the player), and either it's directed to the player
             *   character OR it counts as a turn for the player character.
             */
            if (isRepeatable
                && parentAction == nil
                && (issuingActor.isPlayerChar()
                    && (targetActor.isPlayerChar() || countsAsIssuerTurn)))
                AgainAction.saveForAgain(issuingActor, targetActor,
                                         targetActorPhrase, self);
        }

        /*
         *   Perform this action.  Throughout execution of the action, the
         *   global parser variables that specify the current actor and action
         *   are valid.
         */
        doAction(issuingActor, targetActor, targetActorPhrase,
                 countsAsIssuerTurn)
        {
            local oldActor;
            local oldIssuer;
            local oldAction;
            local oldResults;

            /*
             *   save the current parser globals, for restoration when we
             *   finish this command - if this command is nested within
             *   another, this will let us ensure that everything is restored
             *   properly when we finish with this command
             */
            oldActor = gActor;
            oldIssuer = gIssuingActor;
            oldAction = gAction;
            oldResults = gVerifyResults;

            /*
             *   set the new globals (note that there are no verification
             *   results or command reports objects yet - these are valid only
             *   while we're running the corresponding command phases)
             */
            gActor = targetActor;
            gIssuingActor = issuingActor;
            gAction = self;
            gVerifyResults = nil;

            /* make sure we restore globals on our way out */
            try
            {
                local pc;

                /* if applicable, save the command for AGAIN */
                saveActionForAgain(issuingActor, countsAsIssuerTurn,
                                   targetActor, targetActorPhrase);

                /* start a new command visually if this isn't a nested action */
                if (parentAction == nil)
                    gTranscript.addCommandSep();

                /* have the player character note initial conditions */
                pc = gPlayerChar;
                pc.noteConditionsBefore();

                /* run the before routine for the entire action */
                beforeActionMain();

                /* run the subclass-specific processing */
                doActionMain();

                /* run the after routine for the entire action */
                afterActionMain();

                /*
                 *   If this is a top-level action, show the command results.
                 *   Don't show results for a nested action, since we want to
                 *   wait and let the top-level action show the results after
                 *   it has the full set of results.
                 */
                if (parentAction == nil)
                {
                    /*
                     *   If the player character didn't change, ask the player
                     *   character to note any condition changes.  If the
                     *   player character did change to a new actor,
                     *   presumably the command will have displayed a specific
                     *   message, since this would be an unusual development
                     *   for which we can generate no generic message.
                     */
                    if (gPlayerChar == pc)
                        pc.noteConditionsAfter();
                }
            }
            finally
            {
                /* restore the parser globals to how we found them */
                gActor = oldActor;
                gIssuingActor = oldIssuer;
                gAction = oldAction;
                gVerifyResults = oldResults;
            }
        }

        /*
         *   Perform processing before running the action.  This is called
         *   just once per action, even if the action will be iterated for a
         *   list of objects.
         */
        beforeActionMain()
        {
        }

        /*
         *   Perform processing after running the entire action.  This is
         *   called just once per action, even if the action was iterated for
         *   a list of objects.
         */
        afterActionMain()
        {
            /* call each registered after-action handler */
            if (afterActionMainList != nil)
            {
                foreach (local cur in afterActionMainList)
                    cur.afterActionMain();
            }

            /*
             *   Mark ourselves as busy for the amount of time this action
             *   takes.  Don't count the time taken for implied actions,
             *   though, since these are meant to be zero-time sub-actions
             *   performed as part of the main action and thus don't have a
             *   separate time cost.
             *
             *   Note that we add our busy time in the main after-action
             *   processing because we only want to count our time cost once
             *   for the whole command, even if we're performing the command on
             *   multiple objects.
             */
            if (!isImplicit)
            {
                local actor;

                /*
                 *   If the command is conversational, the turn counts as an
                 *   issuer turn; otherwise, it counts as a turn for the target
                 *   actor.  Conversational commands are effectively carried
                 *   out by the issuer, even though in form they're directed to
                 *   another actor (as in "BOB, HELLO"), so we need to count
                 *   the time they take as the issuer's time.
                 */
                actor = (isConversational(gIssuingActor)
                         ? gIssuingActor : gActor);

                /* add the busy time to the actor */
                actor.addBusyTime(self, actionTime);
            }

            /*
             *   If the command failed, and this is a top-level (not nested)
             *   action, check to see if the game wants to cancel remaining
             *   commands on the line.
             */
            if (gTranscript.isFailure
                && parentAction == nil
                && gameMain.cancelCmdLineOnFailure)
            {
                /*
                 *   the command failed, and they want to cancel remaining
                 *   commands on failure - throw a 'cancel command line'
                 *   exception to cancel any remaining tokens
                 */
                throw new CancelCommandLineException();
            }
        }

        /*
         *   Register an object for afterActionMain invocation.  After we've
         *   finished with the entire action - including all iterated objects
         *   involved in the action - we'll invoke each registered object's
         *   afterActionMain() method.  This registration is only meaningful
         *   for the current action instance, and can only be set up before
         *   the action has been finished (i.e., before the current gAction
         *   invokes its own afterActionMain() method).
         *
         *   Each object is only registered once.  If a caller attempts to
         *   register the same object repeatedly, we'll simply ignore the
         *   repeated requests.
         *
         *   This is a convenient way to implement a collective follow-up to
         *   the parts of an iterated action.  Since repeated registrations
         *   are ignored, each handler for an iterated object (such as a
         *   "dobjFor" action() handler) can register its follow-up handler
         *   without worrying about redundant registration.  Then, when the
         *   overall action is completed for each iterated object involved,
         *   the follow-up handler will be invoked, and it can do any final
         *   work for the overall action.  For example, the follow-up handler
         *   could display a message summarizing the iterated parts of the
         *   action; or, it could even scan the transcript for particular
         *   messages and replace them with a summary.
         */
        callAfterActionMain(obj)
        {
            /* if we don't have an after-action list yet, create one */
            if (afterActionMainList == nil)
                afterActionMainList = new Vector(8);

            /* if this item isn't already in the list, add it */
            if (afterActionMainList.indexOf(obj) == nil)
                afterActionMainList.append(obj);
        }

        /* list of methods to invoke after we've completed the action */
        afterActionMainList = nil

        /*
         *   the amount of time on the game clock that the action consumes -
         *   by default, each action consumes one unit, but actions can
         *   override this to consume more or less game time
         */
        actionTime = 1

        /*
         *   Zero the action time in this action and any parent actions.  This
         *   should be used when a nested replacement action is to completely
         *   take over the time-keeping responsibility for the entire turn; all
         *   containing actions in this case are to take zero time, since the
         *   nested action is the only part of the turn that will count for
         *   timing.
         */
        zeroActionTime()
        {
            /* clear my action time */
            actionTime = 0;

            /* if we have a parent action, zero it and its parents */
            if (parentAction != nil)
                parentAction.zeroActionTime();
        }

        /*
         *   Execute the action for a single set of objects.  This runs the
         *   full execution sequence for the current set of objects.
         *
         *   Subclasses generally won't override this method, but will instead
         *   override the methods that implement the individual steps in the
         *   execution sequence.
         */
        doActionOnce()
        {
            /*
             *   Perform the sequence of operations to execute the action.  If
             *   an ExitSignal is thrown during the sequence, skip to the
             *   end-of-turn processing.
             */
            try
            {
                local result;
                local impReport;

                /*
                 *   Before doing any actual execution, check the command for
                 *   remapping.  If we end up doing any remapping, the
                 *   remapping routine will simply replace the current command,
                 *   so we the remapping call will terminate the current action
                 *   with 'exit' and thus never return here.
                 */
                checkRemapping();

                /*
                 *   If this is an implicit action, check for danger: we never
                 *   try a command implicitly when the command is obviously
                 *   dangerous.
                 */
                if (isImplicit)
                {
                    /*
                     *   verify the action for an implicit command, checking
                     *   for actions that aren't allowe implicitly - we never
                     *   try a command implicitly when the command is (or
                     *   should be) obviously dangerous or is simply
                     *   non-obvious
                     */
                    result = verifyAction();

                    /*
                     *   If the action can be performed, but can't be performed
                     *   implicitly, abort.  Note that we only silently ignore
                     *   the action if it is allowed normally but not
                     *   implicitly: if it's not even allowed normally, we'll
                     *   simply fail later with the normal failure message,
                     *   since there's no harm in trying.
                     */
                    if (result != nil
                        && result.allowAction
                        && !result.allowImplicit)
                        abortImplicit;
                }

                /*
                 *   If this is an implicit command, display a message
                 *   indicating that we're performing the command.
                 */
                impReport = maybeAnnounceImplicit();

                /*
                 *   Make one or two passes through verifications and
                 *   preconditions.  If any precondition performs an implicit
                 *   command, we must run everything a second time to ensure
                 *   that the implicit command or commands did not invalidate
                 *   any earlier precondition or a verification.
                 *
                 *   Run verifications before preconditions, because there
                 *   would be no point in applying implicit commands from
                 *   preconditions if the command verifies as illogical in the
                 *   first place.
                 */
                for (local iter = 1 ; iter <= 2 ; ++iter)
                {
                    /* verify the action */
                    result = verifyAction();

                    /*
                     *   if verification doesn't allow the command to proceed,
                     *   show the reason and end the command
                     */
                    if (result != nil && !result.allowAction)
                    {
                        /* show the result message */
                        result.showMessage();

                        /* mark the command as a failure */
                        gTranscript.noteFailure();

                        /*
                         *   if we have an implicit report, mark it as a mere
                         *   attempt, since the action can't be completed
                         */
                        if (impReport != nil)
                            impReport.noteJustTrying();

                        /* terminate the command */
                        exit;
                    }

                    /*
                     *   Check preconditions of the action.  If we don't invoke
                     *   any implicit commands, we can stop here: nothing in
                     *   the game state will have changed, so there is no need
                     *   to re-verify or re-check preconditions.
                     *
                     *   Only allow implicit actions on the first pass.  Do not
                     *   perform implicit actions on the second pass, because
                     *   if we did so we could get into an infinite loop of
                     *   conflicting preconditions, where each pass would
                     *   reverse the state from the last pass.
                     */
                    if (!checkPreConditions(iter == 1))
                        break;
                }

                /*
                 *   Disable sense caching once we start the action phase -
                 *   once we start making changes to game state, it's too much
                 *   work to figure out when to invalidate the cache, so simply
                 *   turn off caching entirely.
                 *
                 *   Note that the sense cache will already be disabled if we
                 *   executed any implied commands, because the first implied
                 *   command will have disabled the cache as soon as it reached
                 *   its execution phase, and no one will have turned caching
                 *   back on.  It does no harm to disable it again here.
                 */
                libGlobal.disableSenseCache();

                /* if desired, run the "before" notifications before "check" */
                if (gameMain.beforeRunsBeforeCheck)
                    runBeforeNotifiers();

                /*
                 *   Invoke the action's execution method.  Catch any "exit
                 *   action" exceptions - these indicate that the action is
                 *   finished but that the rest of the command processing is to
                 *   proceed as normal.
                 */
                try
                {
                    /* notify the actor of what we're about to do */
                    gActor.actorAction();

                    /* check the action */
                    checkAction();

                    /* if desired, run the "before" notifications after "check" */
                    if (!gameMain.beforeRunsBeforeCheck)
                        runBeforeNotifiers();

                    /* execute the action */
                    execAction();
                }
                catch (ExitActionSignal eaSig)
                {
                    /*
                     *   an exit action signal was thrown - since we've now
                     *   skipped past any remaining action processing, simply
                     *   continue with the rest of the command processing as
                     *   normal
                     */
                }

                /* call afterAction for each object in the notify list */
                notifyBeforeAfter(&afterAction);

                /* notify the actor's containers of the completed action */
                gActor.forEachContainer(callRoomAfterAction);

                /* run the after-action processing */
                afterAction();
            }
            catch (ExitSignal exc)
            {
                /* the execution sequence is finished - simply stop here */
            }
        }

        /*
         *   Run the "before" notifiers: this calls beforeAction on everything
         *   in scope, and calls roomBeforeAction on the actor's containers.
         */
        runBeforeNotifiers()
        {
            /* run the before-action processing */
            beforeAction();

            /*
             *   notify the actor's containers that an action is about
             *   to take place within them
             */
            gActor.forEachContainer(callRoomBeforeAction);

            /* call beforeAction for each object in the notify list */
            notifyBeforeAfter(&beforeAction);
        }

        /*
         *   Reset the message generation context for a sense change.  This
         *   can be called when something substantial happens in the midst of
         *   a command, and we might need different message generation rules
         *   before and after the change.  For example, this is used when a
         *   non-player character moves from one location to another, because
         *   the NPC might want to generate leaving and arriving messages
         *   differently in the two locations.
         */
        recalcSenseContext()
        {
            /* tell the sense context capturer to recalculate the context */
            senseContext.recalcSenseContext();
        }

        /*
         *   Maybe announce the action as an implied action.
         */
        maybeAnnounceImplicit()
        {
            /*
             *   if we're a remapped action, we'll actually want to announce
             *   the original, not the remapping
             */
            if (remappedFrom != nil)
                return remappedFrom.maybeAnnounceImplicit();

            /*
             *   if we're implicit, and we have an implicit announcement
             *   message, announce the implicit action
             */
            if (isImplicit && implicitMsg != nil)
                return gTranscript.announceImplicit(self, implicitMsg);

            /* we don't need to announce the implied action */
            return nil;
        }

        /*
         *   Announce the object of an action.  This should be used for each
         *   iteration of a command that takes objects to announce the objects
         *   on this iteration.
         *
         *   We announce an object under several circumstances:
         *
         *   - If we are iterating through multiple objects, we'll show the
         *   current object to show the player the individual step in the
         *   command being performed.
         *
         *   - If 'all' was used to specify the object, we'll announce it even
         *   if only one object is involved, to make it clear to the player
         *   exactly what we chose as a match.
         *
         *   - If we are executing the command on a single object, and the
         *   object was chosen through disambiguation of a set of ambiguous
         *   choices, and some of the discarded possibilities were logical but
         *   less so than the chosen object, we'll show the assumption we
         *   made.  In such cases, our assumption is not necessarily correct,
         *   so we'll tell the user about our choice explicitly by way of
         *   confirmation - this gives the user a better chance of noticing
         *   quickly if our assumption was incorrect.
         *
         *   - If we supplied a default for a missing noun phrase in the
         *   player's command, we'll show what we chose.  Since the player
         *   didn't say what they meant, we'll make it plain that we're
         *   providing an assumption about what we thought they must have
         *   meant.
         *
         *   'info' is the ResolveInfo object describing this resolved object,
         *   and 'numberInList' is the total number of objects we're iterating
         *   over for this object function (direct object, indirect object,
         *   etc).  'whichObj' is one of the object function constants
         *   (DirectObject, IndirectObject, etc) describing which object we're
         *   mentioning; the language-specific message generator might use
         *   this in conjunction with the action to include a preposition with
         *   the displayed phrase, for example, or choose an appropriate
         *   inflection.
         */
        announceActionObject(info, numberInList, whichObj)
        {
            /*
             *   Show prefix announcements only if the player character is
             *   performing the action.  For NPC's, we don't use the prefix
             *   format, because it doesn't work as well for NPC result
             *   reports; instead, the NPC versions of the library messages
             *   tend to use sufficiently detailed reports that the prefix
             *   isn't required (for example, "Bob takes the iron key" rather
             *   than just "Taken").
             */
            if (gActor.isPlayerChar)
            {
                /* check for the various announcements */
                if (maybeAnnounceMultiObject(info, numberInList, whichObj))
                {
                    /* we've done a multi announcement, so we're now done */
                }
                else if ((info.flags_ & UnclearDisambig) != 0
                         || ((info.flags_ & ClearDisambig) != 0
                             && gameMain.ambigAnnounceMode == AnnounceClear))
                {
                    /* show the object, since we're not certain it's right */
                    gTranscript.announceAmbigActionObject(info.obj_, whichObj);
                }
                else if ((info.flags_ & DefaultObject) != 0
                         && !(info.flags_ & AnnouncedDefaultObject))
                {
                    /*
                     *   Show the object, since we supplied it as a default.
                     *   At this stage in the command, we have resolved
                     *   everything.
                     */
                    gTranscript.announceDefaultObject(
                        info.obj_, whichObj, self, true);

                    /* note that we've announced this object */
                    info.flags_ |= AnnouncedDefaultObject;
                }
            }
        }

        /* announce a multi-action object, if appropriate */
        maybeAnnounceMultiObject(info, numberInList, whichObj)
        {
            /*
             *   announce if we have more than one object, or we're set to
             *   announce this object in any case
             */
            if (numberInList > 1 || (info.flags_ & AlwaysAnnounce) != 0)
            {
                /* show the current object of a multi-object action */
                gTranscript.announceMultiActionObject(
                    info.multiAnnounce, info.obj_, whichObj);

                /* tell the caller we made an announcement */
                return true;
            }

            /* tell the caller we didn't make an announcement */
            return nil;
        }

        /*
         *   Pre-calculate the multi-object announcement text for each object.
         *   This is important because these announcements might choose a form
         *   for the name that distinguishes it from the other objects in the
         *   iteration, and the basis for distinction might be state-dependent
         *   (such as the object's current owner or location), and the relevant
         *   state might change as we iterate over the objects.  From the
         *   user's perspective, they're referring to the objects based on the
         *   state at the start of the command, so the user will expect to see
         *   names based on the that state.
         */
        cacheMultiObjectAnnouncements(lst, whichObj)
        {
            /* run through the list and cache each object's announcement */
            foreach (local cur in lst)
            {
                /* calculate and cache this object's multi-object announcement */
                cur.multiAnnounce = libMessages.announceMultiActionObject(
                    cur.obj_, whichObj, self);
            }
        }

        /* get the list of resolved objects in the given role */
        getResolvedObjList(which)
        {
            /*
             *   the base action doesn't have any objects in any roles, so just
             *   return nil; subclasses need to override this
             */
            return nil;
        }

        /*
         *   Announce a defaulted object list, if appropriate.  We'll announce
         *   the object if we have a single object in the given resolution
         *   list, it was defaulted, and it hasn't yet been announced.
         */
        maybeAnnounceDefaultObject(lst, which, allResolved)
        {
            /*
             *   if the list has exactly one element, and it's marked as a
             *   defaulted object, and it hasn't yet been announced, announce
             *   it
             */
            if (lst != nil
                && lst.length() == 1
                && (lst[1].flags_ & DefaultObject) != 0
                && !(lst[1].flags_ & AnnouncedDefaultObject))
            {
                /* announce the object */
                gTranscript.announceDefaultObject(
                    lst[1].obj_, which, self, allResolved);

                /*
                 *   we've now announced the object; mark it as announced so
                 *   we don't show the same announcement again
                 */
                lst[1].flags_ |= AnnouncedDefaultObject;
            }
        }

        /*
         *   "Pre-announce" a common object for a command that might involve
         *   iteration over other objects.  For example, in "put all in box",
         *   the box is common to all iterations of the command, so we would
         *   want to preannounce it, if it needs to be announced at all,
         *   before the iterations of the command.
         *
         *   We'll announce the object only if it's marked as defaulted or
         *   unclearly disambiguated, and then only if the other list will be
         *   announcing its objects as multi-action objects.  However, we do
         *   not pre-announce anything for a remapped action, because we'll
         *   show the full action description for each individually announced
         *   object, so we don't need or want a separate announcement for the
         *   group.
         *
         *   Returns true if we did any pre-announcing, nil otherwise.  If we
         *   return true, the caller should not re-announce this object during
         *   the iteration, since our pre-announcement is common to all
         *   iterations.
         */
        preAnnounceActionObject(info, mainList, whichObj)
        {
            /* do not pre-announce anything for a remapped action */
            if (isRemapped())
                return nil;

            /*
             *   determine if the main list will be announcing its objects -
             *   it will if it has more than one object, or if its one object
             *   is marked as "always announce"
             */
            if (mainList.length() > 1
                || (mainList[1].flags_ & AlwaysAnnounce) != 0)
            {
                /*
                 *   we will be announcing the main list object or objects, so
                 *   definitely pre-announce this object if appropriate
                 */
                announceActionObject(info, 1, whichObj);

                /* tell the caller we pre-announced the object */
                return true;
            }

            /* we didn't pre-announce the object */
            return nil;
        }

        /*
         *   Run our action-specific pre-processing.  By default, we do
         *   nothing here.
         */
        beforeAction()
        {
        }

        /*
         *   Check the action.  This runs the 'check' phase, and must be
         *   overridden for each subclass.
         *
         *   Intransitive actions don't generally have to do anything in the
         *   'check' phase, since they can simply do any necessary checks in
         *   the 'execute' phase that runs immediately after 'check'.  This
         *   phase is separated out from 'execute' mostly for the benefit of
         *   transitive actions, where the 'check' phase gives the involved
         *   objects a chance to object to the action.
         */
        checkAction() { }

        /*
         *   Execute the action.  This must be overridden by each subclass.
         *
         *   Intransitive actions must do all of their work in this routine.
         *   In most cases, transitive actions will delegate processing to one
         *   or more of the objects involved in the command - for example,
         *   most single-object commands will call a method in the direct
         *   object to carry out the command.
         */
        execAction()
        {
            /* by default, just show the 'no can do' message */
            mainReport(&cannotDoThatMsg);
        }

        /*
         *   Run our action-specific post-processing.  By default, we do
         *   nothing here.
         */
        afterAction()
        {
        }

        /*
         *   Verify the action.  Action subclasses with one or more objects
         *   should call object verification routines here.  Returns a
         *   VerifyResultList with the results, or nil if there are no
         *   verification results at all.  A nil return should be taken as
         *   success, not failure, because it means that we found no objection
         *   to the command.
         */
        verifyAction()
        {
            /*
             *   there are no objects in the default action, but we might have
             *   pre-condition verifiers
             */
            return callVerifyPreCond(nil);
        }

        /*
         *   Initialize tentative resolutions for other noun phrases besides
         *   the one indicated.
         */
        initTentative(issuingActor, targetActor, whichObj)
        {
            /* by default, we have no noun phrases to tentatively resolve */
        }

        /*
         *   Check for remapping the action.  This should check with each
         *   resolved object involved in the command to see if the object wants
         *   to remap the action to a new action; if it does, the object must
         *   replace the current action (using replaceAction or equivalent).
         *   Note that replacing the action must use 'exit' to terminate the
         *   original action, so this will never return if remapping actually
         *   does occur.
         */
        checkRemapping()
        {
            /* by default, we have no objects, so we do nothing here */
        }

        /*
         *   Invoke a callback with a verify results list in gVerifyResults,
         *   using the existing results list or creating a new one if there is
         *   no existing one.  Returns the results list used.
         */
        withVerifyResults(resultsSoFar, obj, func)
        {
            local oldResults;

            /* if we don't already have a result list, create one */
            if (resultsSoFar == nil)
                resultsSoFar = new VerifyResultList();

            /* remember the old global results list */
            oldResults = gVerifyResults;

            /* install the new one */
            gVerifyResults = resultsSoFar;

            /* make sure we restore the old result list on the way out */
            try
            {
                /* invoke the callback */
                (func)();
            }
            finally
            {
                /* restore the old result list */
                gVerifyResults = oldResults;
            }

            /* return the result list */
            return resultsSoFar;
        }

        /*
         *   Verify the non-object-related pre-conditions.  This runs
         *   verification on each of the pre-condition objects defined for the
         *   action.
         */
        callVerifyPreCond(resultSoFar)
        {
            /* look at each of our preconditions */
            foreach (local cond in preCond)
            {
                /*
                 *   If this precondition defines a verifier, call it.  Check
                 *   to see if we have a verifier defined first so that we can
                 *   avoid creating a result list if we won't have any use for
                 *   it.
                 */
                if (cond.propDefined(&verifyPreCondition))
                {
                    /*
                     *   invoke the pre-condition verifier - this is an
                     *   action-level verifier, so there's no object involved
                     */
                    resultSoFar = withVerifyResults(resultSoFar, nil,
                        {: cond.verifyPreCondition(nil) });
                }
            }

            /* return the latest result list */
            return resultSoFar;
        }

        /*
         *   Call a catch-all property on the given object.
         *
         *   actionProp is the custom per-object/per-action property that we
         *   normally invoke to process the action.  For example, if we're
         *   processing verification for the direct object of Take, this would
         *   be &verifyDobjTake.
         *
         *   defProp is the default property that corresponds to actionProp.
         *   This is the per-object/default-action property that we invoke
         *   when the object doesn't provide a "more specialized" version of
         *   actionProp - that is, if the object doesn't define or inherit
         *   actionProp at a point in its class hierarchy that is more
         *   specialized than the point at which it defines defProp, we'll
         *   call defProp.  If there is a more specialized definition of
         *   actionProp for the object, it effectively overrides the default
         *   handler, so we do not invoke the default handler.
         *
         *   allProp is the catch-all property corresponding to actionProp.
         *   We invoke this property in all cases.
         *
         *   Returns true if there is indeed a Default property that overrides
         *   the action, nil if not.
         */
        callCatchAllProp(obj, actionProp, defProp, allProp)
        {
            /* always invoke the catch-all property first */
            obj.(allProp)();

            /*
             *   invoke the default property only if the object doesn't have a
             *   "more specialized" version of actionProp
             */
            if (!obj.propHidesProp(actionProp, defProp))
            {
                /* the Default overrides the action-specific method */
                obj.(defProp)();

                /* tell the caller the Default routine handled it */
                return true;
            }
            else
            {
                /* tell the caller to call the action-specific property */
                return nil;
            }
        }

        /*
         *   Call a verification routine.  This creates a results object and
         *   makes it active, then invokes the given verification routine on
         *   the given object.
         *
         *   We call verification directly on the object, and we also call
         *   verification on the object's preconditions.
         *
         *   If resultSoFar is non-nil, it is a VerifyResultList that has the
         *   results so far - this can be used for multi-object verifications
         *   to gather all of the verification results for all of the objects
         *   into a single result list.  If resultSoFar is nil, we'll create a
         *   new result list.
         */
        callVerifyProp(obj, verProp, preCondProp, remapProp,
                       resultSoFar, whichObj)
        {
            local remapInfo;

            /* check for remapping */
            if ((remapInfo = obj.(remapProp)()) != nil)
            {
                /* call the remapped verify */
                resultSoFar = remapVerify(whichObj, resultSoFar, remapInfo);

                /*
                 *   If the original object has a verify that effectively
                 *   "overrides" the remap - i.e., defined by an object that
                 *   inherits from the object where the remap is defined - then
                 *   run it by the overriding verify as well.
                 */
                local remapSrc = obj.propDefined(remapProp, PropDefGetClass);
                if (obj.propDefined(verProp)
                    && overrides(obj, remapSrc, verProp))
                {
                    resultSoFar = withVerifyResults(
                        resultSoFar, obj, function() { obj.(verProp)(); });
                }

                /* return the results */
                return resultSoFar;
            }

            /* initialize tentative resolutions for other noun phrases */
            initTentative(gIssuingActor, gActor, whichObj);

            /*
             *   run the verifiers in the presence of a results list, and
             *   return the result list
             */
            return withVerifyResults(resultSoFar, obj, function()
            {
                local lst;

                /*
                 *   check the object for a default or catch-all verifier, and
                 *   call it if present; if there isn't a default that
                 *   overrides the action-specific verifier, continue to the
                 *   action-specific verifer
                 */
                if (!callCatchAllProp(
                    obj, verProp, objectRelations.verifyDefaultProps[whichObj],
                    objectRelations.verifyAllProps[whichObj]))
                {
                    /*
                     *   invoke the action-specific verifier method - this
                     *   will update the global verification results object
                     *   with the appropriate status for this action being
                     *   performed on this object
                     */
                    obj.(verProp)();
                }

                /*
                 *   Check the pre-conditions defined for this action on this
                 *   object.  For each one that has a verifier, invoke the
                 *   verifier.
                 */
                lst = getObjPreConditions(obj, preCondProp, whichObj);
                if (lst != nil)
                    foreach (local cur in lst)
                        cur.verifyPreCondition(obj);
            });
        }

        /*
         *   Get the precondition list for an object.  whichObj is the object
         *   role of the object whose preconditions we're retrieving; this is
         *   nil if we're looking for action-level preconditions.
         */
        getObjPreConditions(obj, preCondProp, whichObj)
        {
            local allPreProp;
            local defPreProp;
            local pre;

            /*
             *   if we're looking for action preconditions, there are no
             *   default or catch-all properties, so simply get the
             *   preconditions from the action
             */
            if (whichObj == nil)
                return obj.(preCondProp);

            /* get the default-action and catch-all precondition properties */
            defPreProp = objectRelations.preCondDefaultProps[whichObj];
            allPreProp = objectRelations.preCondAllProps[whichObj];

            /*
             *   Check for an "overriding" default-action handler.  If we have
             *   a default-action handler that hides the specific handler for
             *   this action, use the default handler's precondition list
             *   instead.  Otherwise, use the specific action preconditions.
             */
            if (obj.propHidesProp(defPreProp, preCondProp))
                pre = obj.(defPreProp);
            else
                pre = obj.(preCondProp);

            /* if we have catch-all preconditions, add them to the list */
            if (obj.propDefined(allPreProp))
            {
                /* get the catch-all preconditions */
                local allPre = obj.(allPreProp);

                /* add them to the list so far (if any) */
                pre = (pre == nil ? allPre : pre + allPre);
            }

            /* return the precondition list */
            return pre;
        }

        /*
         *   Verify that some sort of handling for this action is defined on
         *   at least one of the given objects.  If we have no handlers at all
         *   definfed, we'll add an "illogical" status to the verification
         *   results to indicate that the action is not defined on this
         *   object.  This check provides a last resort for verbs with no
         *   handling at all defined on the involved objects, to ensure that
         *   the verb won't go completely unprocessed.
         *
         *   Each entry in objList is an object involved in the action.  For
         *   each entry in objList, there must be *THREE* entries in propList:
         *   a verify property, a check property, and an action property.  If
         *   any of these three properties is defined on the corresponding
         *   object, we'll allow the command to proceed.  If we can find none
         *   of the given handler properties on any of our objects, we'll add
         *   an "illogical" verify result.
         */
        verifyHandlersExist(objList, propList, result)
        {
            /*
             *   check each object to see if any define their corresponding
             *   action property
             */
            for (local i = 1, local j = 1, local len = objList.length() ;
                 i <= len ; ++i, j += 3)
            {
                /* check each of the associated handler properties */
                for (local k = 0 ; k < 3 ; ++k)
                {
                    /* check this handler property */
                    if (objList[i].propDefined(propList[j + k]))
                    {
                        /* we've found a handler, so we can proceed */
                        return result;
                    }
                }
            }

            /*
             *   None of the objects defines an appropriate action method, so
             *   this verifies as illogical.  If there's no result list so
             *   far, create one.
             */
            if (result == nil)
                result = new VerifyResultList();

            /* add an "illogical" status to the results */
            result.addResult(new IllogicalVerifyResult(&cannotDoThatMsg));

            /* return the result */
            return result;
        }

        /*
         *   Call the beforeAction or afterAction method for each object in
         *   the notification list.
         */
        notifyBeforeAfter(prop)
        {
            local lst;

            /* get a table of potential notification receivers */
            lst = getNotifyTable();

            /* go through the table and notify each object */
            lst.forEachAssoc({obj, val: obj.(prop)()});
        }

        /*
         *   Get the list of objects to notify before or after the action has
         *   been performed.
         */
        getNotifyTable()
        {
            local tab;
            local curObjs;
            local actor;

            /* stash the current actor in a local for faster reference */
            actor = gActor;

            /* start with everything connected by containment to the actor */
            tab = actor.connectionTable();

            /* add the actor's explicitly registered notification list */
            foreach (local cur in actor.getActorNotifyList())
                tab[cur] = true;

            /* add the items explicitly registered in the actor's location(s) */
            actor.forEachContainer(
                {loc: loc.getRoomNotifyList().forEach(
                    {obj: tab[obj] = true})
                });

            /* get the list of objects directly involved in the command */
            curObjs = getCurrentObjects();

            /*
             *   add any objects explicitly registered with the objects
             *   directly involved in the command
             */
            foreach (local cur in curObjs)
            {
                /* add each item in the object's notify list */
                foreach (local ncur in cur.getObjectNotifyList())
                    tab[ncur] = true;
            }

            /*
             *   Remove from the list all of the actor's containers.  These
             *   will be notified via the more specific room notification
             *   methods, so we don't want to send them generic notifiers as
             *   well.
             */
            tab.forEachAssoc(function(obj, val)
            {
                if (actor.isIn(obj))
                    tab.removeElement(obj);
            });

            /* if we have any explicitly registered objects, add them */
            if (beforeAfterObjs != nil)
            {
                /* add each object in the list of explicitly registered objects */
                foreach (local cur in beforeAfterObjs)
                    tab[cur] = true;
            }

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

        /*
         *   Register an object for explicit inclusion in beforeAction and
         *   afterAction notifications.  This can be used to register objects
         *   that might not be connected by containment or otherwise
         *   notifiable by normal means.  If this is called after the
         *   beforeAction notification loop has already started, then the
         *   object will only be sent an afterAction notification.
         */
        addBeforeAfterObj(obj)
        {
            /* if we don't yet have a before/after list, create one */
            if (beforeAfterObjs == nil)
                beforeAfterObjs = new Vector(16);

            /* add the object to the list */
            beforeAfterObjs.append(obj);
        }

        /* vector of objects requiring explicit before/after notification */
        beforeAfterObjs = nil

        /*
         *   Get the list of all of the objects (direct object, indirect
         *   object, and any additional objects for actions with three or more
         *   object roles) involved in the current execution.  This is valid
         *   only during a call to doActionOnce(), since that's the only time
         *   a particular set of objects are selected for the action.
         *
         *   By default, an action has no objects roles at all, so we'll just
         *   return an empty list.
         */
        getCurrentObjects()
        {
            return [];
        }

        /*
         *   Set the current objects.  This takes a list of the same form
         *   returned by getCurrentObjects().
         */
        setCurrentObjects(lst) { }

        /*
         *   Check any pre-conditions for the action.
         *
         *   This should check all of the conditions that must be met for the
         *   action to proceed.  If any pre-condition can be met by running an
         *   implicit command first, that implicit command should be executed
         *   here.  If any pre-condition cannot be met, this routine should
         *   notify the actor and throw an ExitSignal.
         *
         *   Returns true if any implicit commands are executed, nil if not.
         *   Implicit commands can only be attempted if allowImplicit is true;
         *   if this is nil, a precondition must simply fail (by displaying an
         *   appropriate failure report and using 'exit') without attempting
         *   an implicit command if its assertion does not hold.
         */
        checkPreConditions(allowImplicit)
        {
            /* check each condition in our action preconditions */
            return callPreConditions(getPreCondDescList(), allowImplicit);
        }

        /*
         *   Get the precondition descriptor list for the action.  For the base
         *   intransitive action type, this simply returns the list of
         *   conditions for the action itself.
         */
        getPreCondDescList()
        {
            /* get the action-level preconditions */
            return getObjPreCondDescList(self, &preCond, nil, nil);
        }

        /*
         *   Get a precondition descriptor list for the given object.  This
         *   returns a list of PreCondDesc objects that wrap the preconditions
         *   for the given object in the given role for this action.
         */
        getObjPreCondDescList(obj, preCondProp, checkArg, whichObj)
        {
            local lst;

            /* get the list of preconditions for the given object in its role */
            lst = getObjPreConditions(obj, preCondProp, whichObj);

            /* if there are no preconditions, return an empty list */
            if (lst == nil)
                return [];

            /*
             *   wrap the precondition objects in descriptors, so that we can
             *   keep track of the check argument that goes with these
             *   preconditions
             */
            return lst.mapAll({x: new PreCondDesc(x, checkArg)});
        }

        /*
         *   Call a method on all of the precondition objects in the
         *   precondition list obtained from the given property of the given
         *   object.
         */
        callPreConditions(lst, allowImplicit)
        {
            local ret;
            local i;

            /* presume we won't call any implicit commands */
            ret = nil;

            /*
             *   note the original descriptor list order, so that we can retain
             *   the current ordering when the execution order doesn't require
             *   changes
             */
            i = 0;
            lst.forEach({x: x.index_ = i++});

            /* sort the precondition list by execution order */
            lst = lst.sort(SortAsc, function(a, b) {
                local delta;

                /* if the execution orders differ, sort by execution order */
                delta = a.cond_.preCondOrder - b.cond_.preCondOrder;
                if (delta != 0)
                    return delta;

                /* otherwise, retain the original list order */
                return a.index_ - b.index_;
            });

            /* catch any 'exit' signals within the preconditions */
            try
            {
                /* invoke the check method for each condition in the list */
                foreach (local cur in lst)
                {
                    /*
                     *   call this precondition; if it runs an implicit
                     *   command, note that we have run an implicit command
                     */
                    if (cur.checkPreCondition(allowImplicit))
                        ret = true;
                }
            }
            catch (ExitSignal es)
            {
                /*
                 *   any 'exit' that occurs within a precondition is a failure
                 *   for the enclosing action
                 */
                gTranscript.noteFailure();

                /* re-throw the 'exit' to cancel the enclosing action */
                throw es;
            }

            /* return true if any implicit commands were executed */
            return ret;
        }

        /*
         *   Our list of action-level pre-condition objects.  These are the
         *   conditions that apply to the overall action, not to the
         *   individual objects involved.  (Object-level pre-conditions are
         *   attached to the objects, not to the action.)
         */
        preCond = []

        /*
         *   Finish the result list for a resolved noun phrase.  This is used
         *   just before disambiguation.  We'll give each object in the list a
         *   chance to filter the list with filterResolveList, and we'll note
         *   the noun phrase we matched in each resolved object.
         */
        finishResolveList(lst, whichObj, np, requiredNum)
        {
            /* give each object a chance to filter the list */
            foreach (local cur in lst)
                lst = cur.obj_.filterResolveList(
                    lst, self, whichObj, np, requiredNum);

            /* stash the noun phrase in each object in the list */
            foreach (local cur in lst)
                cur.np_ = np;

            /* return the list */
            return lst;
        }

        /*
         *   Get a list of verification results for the given ResolveInfo
         *   objects, sorted from best to worst.  Each entry in the returned
         *   list will be a VerifyResultList object whose obj_ property is set
         *   to the ResolveInfo object for which it was generated.
         */
        getSortedVerifyResults(lst, verProp, preCondProp, remapProp,
                               whichObj, np, requiredNum)
        {
            local results;
            local idx;

            /* if there's nothing in the list, we're done */
            if (lst == [])
                return lst;

            /*
             *   Before we run verification, give each object in the list a
             *   chance to do its own filtering on the entire list.  This form
             *   of filtering allows an object to act globally on the list, so
             *   that it can take special action according to the presence or
             *   absence of other objects, and can affect the presence of other
             *   objects.
             */
            lst = finishResolveList(lst, whichObj, np, requiredNum);

            /* create a vector to hold the verification results */
            results = new Vector(lst.length());

            /*
             *   Call the given verifier method on each object, noting each
             *   result.
             */
            idx = 0;
            foreach (local cur in lst)
            {
                local curResult;

                /* call the verifier method and note the current result */
                curResult = callVerifyProp(cur.obj_, verProp, preCondProp,
                                           remapProp, nil, whichObj);

                /*
                 *   save the ResolveInfo in the verify result list object, so
                 *   that we can figure out later (after sorting the results)
                 *   which original ResolveInfo this verification result
                 *   applies to
                 */
                curResult.obj_ = cur;

                /* remember the original list order */
                curResult.origOrder = idx++;

                /* add this verify result to our result vector */
                results.append(curResult);
            }

            /*
             *   Sort the results in descending order of logicalness, and
             *   return the sorted list.  When results are equivalently
             *   logical, keep the results in their existing order.
             */
            return results.toList().sort(SortDesc, function(x, y)
            {
                /* compare the logicalness */
                local c = x.compareTo(y);

                /*
                 *   if it's the same, keep in ascending pluralOrder - note
                 *   that we must reverse the sense of this comparison, since
                 *   we're sorting the overall list in descending order
                 */
                if (c == 0)
                    c = (y.obj_.obj_.pluralOrder - x.obj_.obj_.pluralOrder);

                /* if they're otherwise the same, preserve the original order */
                if (c == 0)
                    c = (y.origOrder - x.origOrder);

                /* return the result */
                return c;
            });
        }

        /*
         *   Combine any remapped verify results in the given verify result
         *   list.  We will remove any result that was remapped to a different
         *   object if and only if the target of the remapping appears in the
         *   list and has the same results as the remapped original.  When
         *   objects are remapped in this fashion, they become effectively
         *   equivalent for the purposes of this command, so we don't have to
         *   distinguish between them for disambiguation or defaulting
         *   purposes.
         */
        combineRemappedVerifyResults(lst, role)
        {
            /* scan each element in the list */
            for (local i = 1, local len = lst.length() ; i <= len ; ++i)
            {
                local cur = lst[i];

                /* if this element has been remapped, consider it further */
                if (cur.remapTarget_ != nil)
                {
                    local other;

                    /*
                     *   scan for another entry in the list that matches us
                     *   for remapping purposes
                     */
                    other = lst.indexWhich(
                        {x: x != cur
                            && cur.matchForCombineRemapped(x, self, role)});

                    /*
                     *   if we found another entry, delete the other entry,
                     *   since it is indistinguishable from this entry
                     */
                    if (other != nil)
                    {
                        /* remove this element from the list */
                        lst -= cur;

                        /* adjust our list counters for the deletion */
                        --i;
                        --len;
                    }
                }
            }

            /* return the updated list */
            return lst;
        }

        /*
         *   Filter an ambiguous object list using the given verification
         *   method.  We call the given verification method on each object,
         *   noting the result, then find the best (most logical) result in
         *   the list.  We reduce the set to the objects that all have the
         *   same best value - everything else in the list is less logical, so
         *   we discard it.  This gives us a set of objects that are all of
         *   equivalent likelihood and all of the best likelihood of all the
         *   objects.
         *
         *   This is the typical way that we disambiguate a list of objects,
         *   but this is merely a service routine, so individual actions can
         *   choose to use this or other mechanisms as appropriate.
         */
        filterAmbiguousWithVerify(lst, requiredNum, verProp,
                                  preCondProp, remapProp, whichObj, np)
        {
            local results;
            local discards;
            local keepers;
            local bestResult;
            local uniqueKeepers;

            /* if there's nothing in the list, there's nothing to do */
            if (lst == [])
                return lst;

            /* first, filter out redundant facets */
            lst = filterFacets(lst);

            /*
             *   Call the verifier method on each object, and sort the results
             *   from best to worst.
             */
            results = getSortedVerifyResults(lst, verProp, preCondProp,
                                             remapProp, whichObj,
                                             np, requiredNum);

            /* note the best result value */
            bestResult = results[1];

            /*
             *   ask the noun phrase to filter this list down to the ones it
             *   wants to keep
             */
            keepers = np.getVerifyKeepers(results);

            /*
             *   Count the number of unique keepers - this is the number of
             *   items in the keepers list that don't have any equivalents in
             *   the keepers list.
             *
             *   To calculate this number, start with the total number of
             *   items in the list, and reduce it by one for each item with an
             *   earlier equivalent in the list.  Note that we only ignore
             *   items with unique equivalents *earlier* in the list so that
             *   we keep exactly one of each equivalent - if we ignored every
             *   element with a unique equivalent elsewhere in the list, we'd
             *   ignore every equivalent item, so we'd only count the items
             *   with no equivalents at all.
             */
            uniqueKeepers = keepers.length();
            for (local i = 1, local cnt = keepers.length() ; i <= cnt ; ++i)
            {
                local eqIdx;

                /* check for a unique equivalent earlier in the list */
                eqIdx = keepers.indexWhich(
                    {x: x.obj_.obj_.isVocabEquivalent(keepers[i].obj_.obj_)});
                if (eqIdx != nil && eqIdx < i)
                {
                    /*
                     *   this one has an earlier equivalent, so don't include
                     *   it in the unique item count
                     */
                    --uniqueKeepers;
                }
            }

            /*
             *   If we found more items to keep than were required by the
             *   caller, we were not able to reduce the set to an unambiguous
             *   subset.  In this case, keep *all* of the items that are
             *   logical.
             */
            if (uniqueKeepers > requiredNum)
            {
                local allAllowed;

                /* filter so that we keep all of the logical results */
                allAllowed = results.subset({x: x.allowAction});

                /* if that list is non-empty, use it as the keepers */
                if (allAllowed.length() != 0)
                    keepers = allAllowed;
            }

            /*
             *   Get the list of discards - this is the balance of the
             *   original result list after removing the best ones.
             */
            discards = results - keepers;

            /*
             *   We now have the set of objects we want, but the entries in
             *   the list are all VerifyResultList instances, and we just want
             *   the objects - pull out a list of just the ResolveInfo, and
             *   return that.
             */
            keepers = keepers.mapAll({x: x.obj_});

            /*
             *   Check to see if we should set flags in the results.  If we
             *   eliminated any objects, flag the remaining objects as having
             *   been selected through disambiguation.  If the best of the
             *   discarded objects were logical, flag the survivors as
             *   "unclear," because we only selected them as better than the
             *   discards, and not because they were the only possible
             *   choices.
             */
            if (discards != [])
            {
                local addedFlags;

                /*
                 *   We have reduced the set.  If the best of the discards was
                 *   ranked as highly as the survivors at the coarsest level,
                 *   flag the survivors as having been "unclearly"
                 *   disambiguated; otherwise, mark them as "clearly"
                 *   disambiguated.
                 */
                if (keepers.indexOf(bestResult.obj_) != nil
                    && (discards[1].getEffectiveResult().resultRank ==
                        bestResult.getEffectiveResult().resultRank))
                {
                    /*
                     *   we had to make a choice that discarded possibilities
                     *   that were valid, though not as good as the one we
                     *   chose - mark the objects as being not perfectly clear
                     */
                    addedFlags = UnclearDisambig;

                    /*
                     *   if the keepers and the rejects are all basic
                     *   equivalents, don't bother flagging this as unclear,
                     *   since there's no point in mentioning that we chose
                     *   one basic equivalent over another, as they all have
                     *   the same name
                     */
                    if (BasicResolveResults.filterWithDistinguisher(
                        keepers + discards.mapAll({x: x.obj_}),
                        basicDistinguisher).length() == 1)
                    {
                        /* they're all basic equivalents - mark as clear */
                        addedFlags = ClearDisambig;
                    }
                }
                else
                {
                    /* the choice is clear */
                    addedFlags = ClearDisambig;
                }

                /* add the flags to each survivor */
                foreach (local cur in keepers)
                    cur.flags_ |= addedFlags;
            }

            /* return the results */
            return keepers;
        }

        /*
         *   Filter a plural list with a verification method.  We'll reduce
         *   the list to the subset of objects that verify as logical, if
         *   there are any.  If there are no logical objects in the list,
         *   we'll simply return the entire original list.
         */
        filterPluralWithVerify(lst, verProp, preCondProp, remapProp, whichObj, np)
        {
            local results;

            /* if there's nothing in the list, there's nothing to do */
            if (lst == [])
                return lst;

            /* first, filter out redundant facets */
            lst = filterFacets(lst);

            /*
             *   Call the verifier method on each object, and sort the results
             *   from best to worst.
             */
            results = getSortedVerifyResults(lst, verProp, preCondProp,
                                             remapProp, whichObj, np, nil);

            /*
             *   If the best (and thus first) result allows the action, filter
             *   the list to keep only the "keepers," as determined by the noun
             *   phrase.  Otherwise, there are no allowed results, so return
             *   the original list.
             */
            if (results[1].allowAction)
                results = np.getVerifyKeepers(results);

            /* return the resolve results objects from the list */
            return results.mapAll({x: x.obj_});
        }

        /*
         *   Filter out redundant facets of the same object.  The various
         *   facets of an object are equivalent to the parser.  An object that
         *   has multiple facets is meant to appear to be one game world
         *   object from the perspective of a character - the multiple facet
         *   objects are an internal implementation detail.
         */
        filterFacets(lst)
        {
            local result;
            local actor = gActor;

            /*
             *   create a vector for the result list, presuming we'll keep all
             *   of the original list elements
             */
            result = new Vector(lst.length(), lst);

            /* check for facets */
            foreach (local cur in lst)
            {
                local allFacets;

                /* if this item has any facets, check for them in our results */
                allFacets = cur.obj_.getFacets() + cur.obj_;
                if (allFacets.length() != 0)
                {
                    local inScopeFacets;
                    local best;

                    /* make a new list of the facets that appear in the results */
                    inScopeFacets = allFacets.subset(
                        {f: result.indexWhich({r: r.obj_ == f}) != nil});

                    /* pick the best of those facets */
                    best = findBestFacet(actor, inScopeFacets);

                    /*
                     *   remove all of the facets besides the best one from the
                     *   result list - this will ensure that we have only the
                     *   single best facet in the final results
                     */
                    foreach (local r in result)
                    {
                        /*
                         *   if this result list item is in the facet list, and
                         *   it's not the best facet, delete it from the result
                         *   list
                         */
                        if (r.obj_ != best
                            && inScopeFacets.indexOf(r.obj_) != nil)
                            result.removeElement(r);
                    }

                }
            }

            /* return the result list */
            return result.toList();
        }

        /*
         *   Get a default object using the given verification method.  We'll
         *   start with the 'all' list, then use the verification method to
         *   reduce the list to the most likely candidates.  If we find a
         *   unique most likely candidate, we'll return a ResolveInfo list
         *   with that result; otherwise, we'll return nothing, since there is
         *   no suitable default.
         */
        getDefaultWithVerify(resolver, verProp, preCondProp, remapProp,
                             whichObj, np)
        {
            local lst;
            local results;
            local bestResult;

            /*
             *   Start with the 'all' list for this noun phrase.  This is the
             *   set of every object that we consider obviously suitable for
             *   the command, so it's a good starting point to guess at a
             *   default object.
             */
            lst = resolver.getAllDefaults();

            /* eliminate objects that can't be defaults for this action */
            lst = lst.subset({x: !x.obj_.hideFromDefault(self)});

            /*
             *   reduce equivalent items to a single instance of each
             *   equivalent - if we have several equivalent items that are
             *   equally good as defaults, we can pick just one
             */
            lst = resolver.filterAmbiguousEquivalents(lst, np);

            /* if we have no entries in the list, there is no default */
            if (lst == [])
                return nil;

            /*
             *   get the verification results for these objects, sorted from
             *   best to worst
             */
            results = getSortedVerifyResults(lst, verProp, preCondProp,
                                             remapProp, whichObj, np, nil);

            /* eliminate redundant remapped objects */
            results = combineRemappedVerifyResults(results, whichObj);

            /* note the best result */
            bestResult = results[1];

            /*
             *   if the best item is not allowed as an implied object, there
             *   is no default
             */
            if (!bestResult.allowImplicit)
                return nil;

            /*
             *   The best item must be uniquely logical in order to be a
             *   default - if more than one item is equally good, it makes no
             *   sense to assume anything about what the user meant.  So, if
             *   we have more than one item, and the second item is equally as
             *   logical as the first item, we cannot supply a default.  (The
             *   second item cannot be better than the first item, because of
             *   the sorting - at most, it can be equally good.)
             */
            if (results.length() != 1 && bestResult.compareTo(results[2]) == 0)
                return nil;

            /*
             *   We have a uniquely logical item, so we can assume the user
             *   must have been referring to this item.  Return the
             *   ResolveInfo for this item (which the verify result sorter
             *   stashed in the obj_ property of the verify result object).
             *
             *   Before returning the list, clear the 'all' flag in the result
             *   (getAll() set that flag), and replace it with the 'default'
             *   flag, since the object is an implied default.
             */
            bestResult.obj_.flags_ &= ~MatchedAll;
            bestResult.obj_.flags_ |= DefaultObject;

            /* return the result list */
            return [bestResult.obj_];
        }

        /*
         *   A synthesized Action (one that's generated by something other than
         *   parsing a command line, such as an event action or nested action)
         *   won't have a parser token list attached to it.  If we're asked to
         *   get the token list, we need to check for this possibility.  If we
         *   don't have a token list, but we do have a parent action, we'll
         *   defer to the parent action.  Otherwise, we'll simply return nil.
         */
        getOrigTokenList()
        {
            /* if we don't have a token list, look elsewhere */
            if (tokenList == nil)
            {
                /* if we have a parent action, defer to it */
                if (parentAction != nil)
                    return parentAction.getOrigTokenList();

                /* we have nowhere else to look, so return an empty list */
                return [];
            }

            /* inherit the standard handling from BasicProd */
            return inherited();
        }

        /*
         *   Create a topic qualifier resolver.  This type of resolver is used
         *   for qualifier phrases (e.g., possessives) within topic phrases
         *   within objects of this verb.  Topics *usually* only apply to
         *   TopicActionBase subclasses, but not exclusively: action remappings
         *   can sometimes require a topic phrase from one action to be
         *   resolved in the context of another action that wouldn't normally
         *   involve a topic phrase.  That's why this is needed on the base
         *   Action class.
         */
        createTopicQualifierResolver(issuingActor, targetActor)
        {
            /* create and return a topic qualifier object resolver */
            return new TopicQualifierResolver(self, issuingActor, targetActor);
        }

        /*
         *   List of objects that verified okay on a prior pass.  This is a
         *   scratch-pad for use by verifier routines, to keep track of work
         *   they've already done.  A few verifiers use this as a way to
         *   detect when an implicit action actually finished the entire job,
         *   which would in many cases result in a verify failure if not
         *   checked (because a command that effects conditions that already
         *   hold is normally illogical); by tracking that the verification
         *   previously succeeded, the verifier can know that the action
         *   should be allowed to proceed and do nothing.
         */
        verifiedOkay = []

        /*
         *   Get the missing object response production for a given resolver
         *   role.  (The base action doesn't have any objects, so there's no
         *   such thing as a missing object query.)
         */
        getObjResponseProd(resolver) { return nil; }
    ;

    /*
     *   Call the roomBeforeAction method on a given room's containing rooms,
     *   then on the room itself.
     */
    callRoomBeforeAction(room)
    {
        /* first, call roomBeforeAction on the room's containers */
        room.forEachContainer(callRoomBeforeAction);

        /* call roomBeforeAction on this room */
        room.roomBeforeAction();
    }

    /*
     *   Call the roomAfterAction method on a given room, then on the room's
     *   containing rooms.
     */
    callRoomAfterAction(room)
    {
        /* first, call roomAfterAction on this room */
        room.roomAfterAction();

        /* next, call roomAfterAction on the room's containers */
        room.forEachContainer(callRoomAfterAction);
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Intransitive Action class - this is an action that takes no objects.
     *   In general, each subclass should implement its action handling in its
     *   execAction() method.
     */
    class IAction: Action
        /*
         *   resolve my noun phrases to objects
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            /*
             *   We have no objects to resolve.  The only thing we have to do
             *   is note in the results our number of structural noun slots
             *   for the verb, which is zero, since we have no objects at all.
             */
            results.noteNounSlots(0);
        }

        /*
         *   Execute the action.
         */
        doActionMain()
        {
            /*
             *   we have no objects to iterate, so simply run through the
             *   execution sequence once
             */
            doActionOnce();
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   "All" and "Default" are a pseudo-actions used for dobjFor(All),
     *   iobjFor(Default), and similar catch-all handlers.  These are never
     *   executed as actual actions, but we define them so that dobjFor(All)
     *   and the like won't generate any warnings for undefined actions.
     */
    class AllAction: object
    ;
    class DefaultAction: object
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Transitive Action class - this is an action that takes a direct
     *   object.
     *
     *   For simplicity, this object is its own object resolver - we really
     *   don't need a separate resolver object because we have only one object
     *   list for this verb.  (In contrast, an action with both a direct and
     *   indirect object might need separate resolution rules for the two
     *   objects, and hence would need separate resolver objects for the two.)
     *
     *   The advantage of implementing the Resolver behavior in this object,
     *   rather than using a separate object, is that it's less trouble to
     *   override object resolution rules - simply override the resolver
     *   methods in the subclass where you define the grammar rule for the
     *   action.
     */
    class TAction: Action, Resolver
        construct()
        {
            /* inherit only the Action constructor */
            inherited Action.construct();
        }

        resetAction()
        {
            /* inherit default handling */
            inherited();

            /* discard our cached resolver */
            dobjResolver_ = nil;
        }

        /*
         *   Create an action for retrying an original action with changes.
         */
        createForRetry(orig)
        {
            local action;

            /* create the new action based on the original action */
            action = createActionFrom(orig);

            /*
             *   do not include the new command in undo, since it's merely
             *   part of the enclosing explicit command
             */
            action.includeInUndo = nil;

            /* mark the action as nested */
            action.setNested();

            /*
             *   Even if the original action is implicit, don't announce this
             *   action as implicit, because it's good enough to have
             *   announced the original implicit action.  Now, this new
             *   command actually still is implicit if the original was - we
             *   simply don't want to announce it as such.  To suppress the
             *   extra announcement while still retaining the rest of the
             *   desired implicitness, simply set the implicitMsg property of
             *   the new action to nil; when there's no implicitMsg, there's
             *   no announcement.
             */
            action.implicitMsg = nil;

            /* return the new action */
            return action;
        }

        /*
         *   Retry an intransitive action as a single-object action.  We'll
         *   obtain a indirect object using the normal means (first looking
         *   for a default, then prompting the player if we can't find a
         *   suitable default).  'orig' is the original zero-object action.
         *
         *   This routine terminates with 'exit' if it doesn't throw some
         *   other error.
         */
        retryWithMissingDobj(orig, asker)
        {
            /* resolve and execute the replacement action */
            resolveAndReplaceAction(createForMissingDobj(orig, asker));
        }

        /*
         *   Retry an action as a single-object action with an ambiguous
         *   direct object.  We'll ask which of the given possible objects is
         *   intended.
         */
        retryWithAmbiguousDobj(orig, objs, asker, objPhrase)
        {
            local action;
            local resolver;

            /* create a missing-direct-object replacement action */
            action = createForMissingDobj(orig, asker);

            /* reduce the object list to the objects in scope */
            resolver = action.getDobjResolver(gIssuingActor, gActor, true);
            objs = objs.subset({x: resolver.objInScope(x)});

            /* plug in the ambiguous direct object list */
            action.dobjMatch = new PreResolvedAmbigProd(objs, asker, objPhrase);

            /* resolve and execute the replacement action */
            resolveAndReplaceAction(action);
        }

        /*
         *   Test to see if askForDobj() would find a default direct object.
         *   Returns true if there's a default, nil if not.  If this returns
         *   true, then askForDobj() will simply take the default and proceed;
         *   otherwise, it will have to actually ask the user for the missing
         *   information.
         */
        testRetryDefaultDobj(orig)
        {
            /* create the new action for checking for a direct object */
            local action = createForMissingDobj(orig, ResolveAsker);

            /* get the default dobj */
            local def = action.getDefaultDobj(
                action.dobjMatch,
                action.getDobjResolver(gIssuingActor, gActor, nil));

            /* if there's exactly one result, then we have a default */
            return (def != nil && def.length() == 1);
        }

        /*
         *   Create an instance of this action for retrying a given
         *   single-object action with a missing direct object.
         */
        createForMissingDobj(orig, asker)
        {
            /* create the action for a retry */
            local action = createForRetry(orig);

            /* use an empty noun phrase for the new action's direct object */
            action.dobjMatch = new EmptyNounPhraseProd();

            /* set our custom response production and ResolveAsker */
            action.dobjMatch.setPrompt(action.askDobjResponseProd, asker);

            /* initialize the new action with any pre-resolved parts */
            action.initForMissingDobj(orig);

            /* return the new action */
            return action;
        }

        /*
         *   Initialize this action in preparation for retrying with a missing
         *   direct object.  This routine must copy any phrases from the
         *   original action that have already been resolved.  This base
         *   TAction implementation obviously can't have anything pre-resolved
         *   in the original, since the original must simply be an IAction.
         *   Subclasses must override as appropriate for the kinds of base
         *   actions from which they can be retried.
         */
        initForMissingDobj(orig) { }

        /*
         *   The root production to use to parse missing direct object
         *   responses.  By default, this is nounList, but individual actions
         *   can override this as appropriate.
         *
         *   Note that language modules might want to override this to allow
         *   for special responses.  For example, in English, some verbs might
         *   want to override this with a specialized production that allows
         *   the appropriate preposition in the response.
         */
        askDobjResponseProd = nounList

        /* get the missing object response production for a given role */
        getObjResponseProd(resolver)
        {
            /* if it's the direct object, return the dobj response prod */
            if (resolver.whichObject == DirectObject)
                return askDobjResponseProd;

            /* otherwise use the default handling */
            return inherited(resolver);
        }

        /*
         *   Can the direct object potentially resolve to the given simulation
         *   object?  This only determines if the object is a *syntactic*
         *   match, meaning that it can match at a vocabulary and grammar
         *   level.  This doesn't test it for logicalness or check that it's an
         *   otherwise valid resolution.
         */
        canDobjResolveTo(obj)
        {
            /* check our dobj match tree to see if it can resolve to 'obj' */
            return dobjMatch.canResolveTo(
                obj, self, issuer_, actor_, DirectObject);
        }

        /*
         *   Resolve objects.  This is called at the start of command
         *   execution to resolve noun phrases in the command to specific
         *   objects.
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            /* note that we have a single noun slot (the direct object) */
            results.noteNounSlots(1);

            /*
             *   Ask the direct object noun phrase list to resolve itself, and
             *   store the resulting object list.  Since we're starting a
             *   resolution pass through our objects, reset the resolver if
             *   we're reusing it.
             */
            dobjList_ = dobjMatch.resolveNouns(
                getDobjResolver(issuingActor, targetActor, true), results);
        }

        /* a transitive action has one noun phrase: the direct object */
        predicateNounPhrases = [&dobjMatch]

        /* get the role of an object */
        getRoleFromIndex(idx)
        {
            /* we only take a single object role - the direct object */
            return (idx == 1 ? DirectObject : inherited(idx));
        }

        /* get the resolved object in a given role */
        getObjectForRole(role)
        {
            /* return the direct object if requested */
            return (role == DirectObject ? getDobj() : inherited(role));
        }

        /* get the match tree for the noun phrase in the given role */
        getMatchForRole(role)
        {
            /* return the direct object match tree if requested */
            return (role == DirectObject ? dobjMatch : inherited(role));
        }

        /* get the 'verify' property for a given object role */
        getVerifyPropForRole(role)
        {
            return (role == DirectObject ? verDobjProp : inherited(role));
        }

        /* get the 'preCond' property for a given object role */
        getPreCondPropForRole(role)
        {
            return (role == DirectObject ? preCondDobjProp : inherited(role));
        }

        /* get the 'remap' property for a given object role */
        getRemapPropForRole(role)
        {
            return (role == DirectObject ? remapDobjProp : inherited(role));
        }

        /* get the ResolveInfo for the given object */
        getResolveInfo(obj, oldRole)
        {
            local info = nil;

            /* scan our resolved direct object list for the given object */
            if (dobjList_ != nil)
                info = dobjList_.valWhich({x: x.obj_ == obj});

            /* if we didn't find one, create one from scratch */
            if (info == nil)
            {
                /*
                 *   if there's anything in the old dobj role, copy the flags
                 *   and noun phrase to the new role
                 */
                if (dobjList_.length() > 0)
                {
                    /* get the flags and noun phrase from the old role */
                    info = new ResolveInfo(
                        obj, dobjList_[1].flags_, dobjList_[1].np_);
                }
                else
                {
                    /* there's no old role, so start from scratch */
                    info = new ResolveInfo(obj, 0, nil);
                }
            }

            /* return what we found (or created) */
            return info;
        }

        /* get the list of resolved objects in the given role */
        getResolvedObjList(which)
        {
            return (which == DirectObject ? getResolvedDobjList()
                    : inherited(which));
        }

        /* get the list of resolved direct objects */
        getResolvedDobjList()
        {
            /*
             *   if we have a direct object list, return the objects from it;
             *   otherwise, return an empty list
             */
            return (dobjList_ == nil ? nil : dobjList_.mapAll({x: x.obj_}));
        }

        /* manually set the resolved objects - we'll set the direct object */
        setResolvedObjects(dobj)
        {
            /* set the resolved direct object */
            setResolvedDobj(dobj);
        }

        /* set the resolved direct object */
        setResolvedDobj(dobj)
        {
            /*
             *   set the direct object tree to a fake grammar tree that
             *   resolves to our single direct object, in case we're asked to
             *   resolve explicitly
             */
            dobjMatch = new PreResolvedProd(dobj);

            /*
             *   Set the resolved direct object list to the single object or
             *   to the list of objects, depending on what we received.
             */
            dobjList_ = makeResolveInfoList(dobj);

            /* set the current object as well */
            dobjCur_ = (dobjList_.length() > 0 ? dobjList_[1].obj_ : nil);
            dobjInfoCur_ = (dobjList_.length() > 0 ? dobjList_[1] : nil);
        }

        /* manually set the unresolved object noun phrase match trees */
        setObjectMatches(dobj)
        {
            /*
             *   if it's a ResolveInfo, handle it as a resolved object;
             *   otherwise handle it as a match tree to be resolved
             */
            if (dobj.ofKind(ResolveInfo))
                setResolvedDobj(dobj);
            else
                dobjMatch = dobj;
        }

        /* check that the resolved objects are in scope */
        resolvedObjectsInScope()
        {
            /* check the direct object */
            return getDobjResolver(gIssuingActor, gActor, true)
                .objInScope(dobjList_[1].obj_);
        }

        /*
         *   Get a message parameter object for the action.  We define 'dobj'
         *   as the direct object, in addition to any inherited targets.
         */
        getMessageParam(objName)
        {
            switch(objName)
            {
            case 'dobj':
                /* return the current direct object */
                return dobjCur_;

            default:
                /* inherit default handling */
                return inherited(objName);
            }
        }

        /*
         *   Execute the action.  We'll run through the execution sequence
         *   once for each resolved direct object.
         */
        doActionMain()
        {
            /*
             *   Set the direct object list as the antecedent, using the
             *   language-specific pronoun setter.  Don't set pronouns for a
             *   nested command, because the player didn't necessarily refer to
             *   the objects in a nested command.
             */
            if (parentAction == nil)
                gActor.setPronoun(dobjList_);

            /* we haven't yet canceled the iteration */
            iterationCanceled = nil;

            /* pre-calculate the multi-object announcement text for each dobj */
            cacheMultiObjectAnnouncements(dobjList_, DirectObject);

            /* run through the sequence once for each direct object */
            for (local i = 1, local len = dobjList_.length() ;
                 i <= len && !iterationCanceled ; ++i)
            {
                /* make this object our current direct object */
                dobjCur_ = dobjList_[i].obj_;
                dobjInfoCur_ = dobjList_[i];

                /* announce the object if appropriate */
                announceActionObject(dobjList_[i], len, whichMessageObject);

                /* run the execution sequence for the current direct object */
                doActionOnce();

                /* if we're top-level, count the iteration in the transcript */
                if (parentAction == nil)
                    gTranscript.newIter();
            }
        }

        /* get the precondition descriptor list for the action */
        getPreCondDescList()
        {
            /*
             *   return the inherited preconditions plus the conditions that
             *   apply to the direct object
             */
            return inherited()
                + getObjPreCondDescList(dobjCur_, preCondDobjProp, dobjCur_,
                                        DirectObject);
        }

        /*
         *   Get the list of active objects.  We have only a direct object, so
         *   we'll return a list with the current direct object.
         */
        getCurrentObjects()
        {
            return [dobjCur_];
        }

        /* set the current objects */
        setCurrentObjects(lst)
        {
            dobjCur_ = lst[1];
            dobjInfoCur_ = nil;
        }

        /*
         *   Verify the action.
         */
        verifyAction()
        {
            local result;

            /* invoke any general (non-object) pre-condition verifiers */
            result = callVerifyPreCond(nil);

            /*
             *   invoke the verifier routine ("verXxx") on the current direct
             *   object and return the result
             */
            result = callVerifyProp(dobjCur_, verDobjProp, preCondDobjProp,
                                    remapDobjProp, result, DirectObject);

            /* verify that the action routine ("doXxx") exists on the object */
            return verifyHandlersExist(
                [dobjCur_], [verDobjProp, actionDobjProp, checkDobjProp],
                result);
        }

        /* initialize tentative resolutions for other noun phrases */
        initTentative(issuingActor, targetActor, whichObj)
        {
            /*
             *   we have only one noun phrase, so there's nothing else to
             *   tentatively resolve
             */
        }

        /*
         *   Check for remapping
         */
        checkRemapping()
        {
            local remapInfo;

            /* check for remapping in the direct object */
            if ((remapInfo = dobjCur_.(remapDobjProp)) != nil)
            {
                /*
                 *   we have a remapping, so apply it - note that this won't
                 *   return, since this will completely replace the command
                 *   and thus terminate the old command with 'exit'
                 */
                remapAction(nil, DirectObject, remapInfo);
            }
        }

        /*
         *   Check the command.
         *
         *   For a single-object transitive action, this runs the catch-all
         *   'check' properties (the dobjFor(Default) and dobjFor(All) 'check'
         *   methods) on the direct object, then calls the individual 'check'
         *   routine for this specific action.
         */
        checkAction()
        {
            try
            {
                /* call the catch-all 'check' properties */
                if (!callCatchAllProp(dobjCur_, checkDobjProp,
                                      &checkDobjDefault, &checkDobjAll))
                {
                    /*
                     *   the action-specific check routine overrides any
                     *   Default catch-all, so call the direct object's check
                     *   routine (its "checkXxx") method
                     */
                    dobjCur_.(checkDobjProp)();
                }
            }
            catch (ExitSignal es)
            {
                /* mark the action as a failure in the transcript */
                gTranscript.noteFailure();

                /* re-throw the signal */
                throw es;
            }
        }

        /*
         *   Execute the command.
         */
        execAction()
        {
            /* call the catch-all 'action' properties */
            if (!callCatchAllProp(dobjCur_, actionDobjProp,
                                  &actionDobjDefault, &actionDobjAll))
            {
                /*
                 *   the verb-specific 'action' handler overrides any Default
                 *   action handler, so call the verb-specific 'action'
                 *   routine in the direct object (the "doXxx" method)
                 */
                dobjCur_.(actionDobjProp)();
            }
        }

        /*
         *   The direct object preconditions, verifier, remapper, check, and
         *   action methods for this action.  Each concrete action must define
         *   these appropriately.  By convention, the methods are named like
         *   so:
         *
         *.  preconditions: preCondDobjAction
         *.  verifier: verDobjAction
         *.  remap: remapDobjAction
         *.  check: checkDobjAction
         *.  action: actionDobjAction
         *
         *   where the 'Action' suffix is replaced by the name of the action.
         *   The DefineTAction macro applies this convention, so in most cases
         *   library and game authors will never have to create all of those
         *   property names manually.
         */
        verDobjProp = nil
        preCondDobjProp = nil
        remapDobjProp = nil
        checkDobjProp = nil
        actionDobjProp = nil

        /*
         *   Get my direct object resolver.  If I don't already have one,
         *   create one and cache it; if I've already cached one, return it.
         *   Note that we cache the resolver because it can sometimes take a
         *   bit of work to set one up (the scope list can in some cases be
         *   complicated to calculate).  We use the resolver only during the
         *   object resolution phase; since game state can't change during
         *   this phase, it's safe to keep a cached copy.
         */
        getDobjResolver(issuingActor, targetActor, reset)
        {
            /* create a new resolver if we don't already have one cached */
            if (dobjResolver_ == nil)
                dobjResolver_ = createDobjResolver(issuingActor, targetActor);

            /* reset the resolver if desired */
            if (reset)
                dobjResolver_.resetResolver();

            /* return it */
            return dobjResolver_;
        }

        /*
         *   Create a resolver for the direct object.  By default, we are our
         *   own resolver.  Some actions might want to override this to create
         *   and return a specialized resolver instance if special resolution
         *   rules are needed.
         */
        createDobjResolver(issuingActor, targetActor)
        {
            /* initialize myself as a resolver */
            initResolver(issuingActor, targetActor);

            /* return myself */
            return self;
        }

        /*
         *   Does this action allow "all" to be used in noun phrases?  By
         *   default, we allow it or not according to a gameMain property.
         *
         *   Note that the inventory management verbs (TAKE, TAKE FROM, DROP,
         *   PUT IN, PUT ON) override this to allow "all" to be used, so
         *   disallowing "all" here (or via gameMain) won't disable "all" for
         *   those verbs.
         */
        actionAllowsAll = (gameMain.allVerbsAllowAll)

        /*
         *   Resolve 'all' for the direct object, given a list of everything
         *   in scope.  By default, we'll simply return everything in scope;
         *   some actions might want to override this to return a more
         *   specific list of objects suitable for 'all'.
         */
        getAllDobj(actor, scopeList)
        {
            return scopeList;
        }

        /* filter an ambiguous direct object noun phrase */
        filterAmbiguousDobj(lst, requiredNum, np)
        {
            /* filter using the direct object verifier method */
            return filterAmbiguousWithVerify(lst, requiredNum, verDobjProp,
                                             preCondDobjProp, remapDobjProp,
                                             DirectObject, np);
        }

        /* filter a plural phrase */
        filterPluralDobj(lst, np)
        {
            /* filter using the direct object verifier method */
            return filterPluralWithVerify(lst, verDobjProp, preCondDobjProp,
                                          remapDobjProp, DirectObject, np);
        }

        /* get the default direct object */
        getDefaultDobj(np, resolver)
        {
            /* get a default direct object using the verify method */
            return getDefaultWithVerify(resolver, verDobjProp, preCondDobjProp,
                                        remapDobjProp, DirectObject, np);
        }

        /* get the current direct object of the command */
        getDobj() { return dobjCur_; }

        /* get the full ResolveInfo associated with the current direct object */
        getDobjInfo() { return dobjInfoCur_; }

        /* get the object resolution flags for the direct object */
        getDobjFlags() { return dobjInfoCur_ != nil ? dobjInfoCur_.flags_ : 0; }

        /* get the number of direct objects */
        getDobjCount() { return dobjList_ != nil ? dobjList_.length() : 0; }

        /* get the original token list of the current direct object phrase */
        getDobjTokens()
        {
            return dobjInfoCur_ != nil && dobjInfoCur_.np_ != nil
                ? dobjInfoCur_.np_.getOrigTokenList() : nil;
        }

        /* get the original words (as a list of strings) of the current dobj */
        getDobjWords()
        {
            local l = getDobjTokens();
            return l != nil ? l.mapAll({x: getTokOrig(x)}) : nil;
        }

        /* the predicate must assign the direct object production tree here */
        dobjMatch = nil

        /* my resolved list of direct objects */
        dobjList_ = []

        /*
         *   The resolved direct object on which we're currently executing the
         *   command.  To execute the command, we iterate through the direct
         *   object list, calling the execution sequence for each object in
         *   the list.  We set this to the current object in each iteration.
         */
        dobjCur_ = nil

        /* the full ResolveInfo associated with dobjCur_ */
        dobjInfoCur_ = nil

        /* my cached direct object resolver */
        dobjResolver_ = nil

        /* -------------------------------------------------------------------- */
        /*
         *   Resolver interface implementation - for the moment, we don't need
         *   any special definitions here, since the basic Resolver
         *   implementation (which we inherit) is suitable for a single-object
         *   action.
         */

        /* -------------------------------------------------------------------- */
        /*
         *   private Resolver implementation details
         */

        /*
         *   Initialize me as a resolver.
         */
        initResolver(issuingActor, targetActor)
        {
            /* remember the actors */
            issuer_ = issuingActor;
            actor_ = targetActor;

            /* I'm the action as well as the resolver */
            action_ = self;

            /* cache the actor's default scope list */
            cacheScopeList();
        }

        /* issuing actor */
        issuer_ = nil

        /* target actor */
        actor_ = nil

        /*
         *   By default, our direct object plays the direct object role in
         *   generated messages.  Subclasses can override this if the resolved
         *   object is to play a different role.  Note that this only affects
         *   generated messages; for parsing purposes, our object is always in
         *   the DirectObject role.
         */
        whichMessageObject = DirectObject
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   "Tentative" noun resolver results gather.  This type of results
     *   gatherer is used to perform a tentative pre-resolution of an object
     *   of a multi-object action.
     *
     *   Consider what happens when we resolve a two-object action, such as
     *   "put <dobj> in <iobj>".  Since we have two objects, we obviously must
     *   resolve one object or the other first; but this means that we must
     *   resolve one object with no knowledge of the resolution of the other
     *   object.  This often makes it very difficult to resolve that first
     *   object intelligently, because we'd really like to know something
     *   about the other object.  For example, if we first resolve the iobj of
     *   "put <dobj> in <iobj>", it would be nice to know which dobj we're
     *   talking about, since we could reduce the likelihood that the iobj is
     *   the dobj's present container.
     *
     *   Tentative resolution addresses this need by giving us some
     *   information about a later-resolved object while resolving an
     *   earlier-resolved object, even though we obviously can't have fully
     *   resolved the later-resolved object.  In tentative resolution, we
     *   perform the resolution of the later-resolved object, completely in
     *   the dark about the earlier-resolved object(s), and come up with as
     *   much information as we can.  The important thing about this stage of
     *   resolution is that we don't ask any interactive questions and we
     *   don't count anything for ranking purposes - we simply do the best we
     *   can and note the results, leaving any ranking or interaction for the
     *   true resolution phase that we'll perform later.
     */
    class TentativeResolveResults: ResolveResults
        construct(target, issuer) { setActors(target, issuer); }

        /*
         *   ignore most resolution problems, since this is only a tentative
         *   resolution pass
         */
        noMatch(action, txt) { }
        noMatchPoss(action, txt) { }
        noVocabMatch(action, txt) { }
        noMatchForAll() { }
        noteEmptyBut() { }
        noMatchForAllBut() { }
        noMatchForListBut() { }
        noMatchForPronoun(typ, txt) { }
        reflexiveNotAllowed(typ, txt) { }
        wrongReflexive(typ, txt) { }
        noMatchForPossessive(owner, txt) { }
        noMatchForLocation(loc, txt) { }
        noteBadPrep() { }
        nothingInLocation(loc) { }
        unknownNounPhrase(match, resolver) { return []; }
        noteLiteral(txt) { }
        emptyNounPhrase(resolver) { return []; }
        zeroQuantity(txt) { }
        insufficientQuantity(txt, matchList, requiredNum) { }
        uniqueObjectRequired(txt, matchList) { }
        noteAdjEnding() { }
        noteIndefinite() { }
        noteMiscWordList(txt) { }
        notePronoun() { }
        noteMatches(matchList) { }
        incCommandCount() { }
        noteActorSpecified() { }
        noteNounSlots(cnt) { }
        noteWeakPhrasing(level) { }
        allowActionRemapping = nil

        /*
         *   during the tentative phase, keep all equivalents - we don't want
         *   to make any arbitrary choices among equivalents during this
         *   phase, because doing so could improperly force a choice among
         *   otherwise ambiguous resolutions to the other phrase
         */
        allowEquivalentFiltering = nil

        /*
         *   for ambiguous results, don't attempt to narrow things down - just
         *   keep the entire list
         */
        ambiguousNounPhrase(keeper, asker, txt,
                            matchList, fullMatchList, scopeList,
                            requiredNum, resolver)
        {
            return matchList;
        }

        /*
         *   no interaction is allowed, so return nothing if we need to ask
         *   for a missing object
         */
        askMissingObject(asker, resolver, responseProd)
        {
            /* note that we have a missing noun phrase */
            npMissing = true;

            /* return nothing */
            return nil;
        }

        /*
         *   no interaction is allowed, so return no tokens if we need to ask
         *   for a literal
         */
        askMissingLiteral(action, which) { return []; }

        /* no interaction is allowed during tentative resolution */
        canResolveInteractively() { return nil; }

        /*
         *   flag: the noun phrase we're resolving is a missing noun phrase,
         *   which means that we'll ask for it to be filled in when we get
         *   around to resolving it for real
         */
        npMissing = nil
    ;

    /*
     *   A dummy object that we use for the *tentative* resolution of a noun
     *   phrase when the noun phrase doesn't match anything.  This lets us
     *   distinguish cases where we have a noun phrase that has an error from a
     *   noun phrase that's simply missing.
     */
    dummyTentativeObject: object
    ;

    dummyTentativeInfo: ResolveInfo
        obj_ = dummyTentativeObject
        flags_ = 0
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Transitive-with-indirect Action class - this is an action that takes
     *   both a direct and indirect object.  We subclass the basic one-object
     *   action to add the indirect object handling.
     */
    class TIAction: TAction
        resetAction()
        {
            /* inherit defaulting handling */
            inherited();

            /* discard our cached iobj resolver */
            iobjResolver_ = nil;
        }

        /*
         *   Retry a single-object action as a two-object action.  We'll treat
         *   the original action's direct object list as our direct object
         *   list, and obtain an indirect object using the normal means (first
         *   looking for a default, then prompting the player if we can't find
         *   a suitable default).  'orig' is the original single-object action.
         *
         *   This routine terminates with 'exit' if it doesn't throw some
         *   other error.
         */
        retryWithMissingIobj(orig, asker)
        {
            /* resolve and execute the replacement action */
            resolveAndReplaceAction(createForMissingIobj(orig, asker));
        }

        /*
         *   Retry an action as a two-object action with an ambiguous indirect
         *   object.  We'll ask which of the given possible objects is
         *   intended.
         */
        retryWithAmbiguousIobj(orig, objs, asker, objPhrase)
        {
            /* create a missing-indirect-object replacement action */
            local action = createForMissingIobj(orig, asker);

            /* reduce the object list to the objects in scope */
            local resolver = action.getIobjResolver(gIssuingActor, gActor, true);
            objs = objs.subset({x: resolver.objInScope(x)});

            /* plug in the ambiguous indirect object list */
            action.iobjMatch = new PreResolvedAmbigProd(objs, asker, objPhrase);

            /* resolve and execute the replacement action */
            resolveAndReplaceAction(action);
        }

        /*
         *   Test to see if askForIobj() would find a default indirect object.
         *   Returns true if there's a default, nil if not.  If this returns
         *   true, then askForIobj() will simply take the default and proceed;
         *   otherwise, it will have to actually ask the user for the missing
         *   information.
         */
        testRetryDefaultIobj(orig)
        {
            local action;
            local def;

            /* create the new action for checking for an indirect object */
            action = createForMissingIobj(orig, ResolveAsker);

            /* get the default iobj */
            def = action.getDefaultIobj(
                action.iobjMatch,
                action.getIobjResolver(gIssuingActor, gActor, nil));

            /* if there's exactly one result, then we have a default */
            return (def != nil && def.length() == 1);
        }

        /*
         *   Create an instance of this action for retrying a given
         *   single-object action with a missing indirect object.
         */
        createForMissingIobj(orig, asker)
        {
            /* create the new action based on the original action */
            local action = createForRetry(orig);

            /* use an empty noun phrase for the new action's indirect object */
            action.iobjMatch = new EmptyNounPhraseProd();

            /* set our custom response production and ResolveAsker */
            action.iobjMatch.setPrompt(action.askIobjResponseProd, asker);

            /* copy what we've resolved so far */
            action.initForMissingIobj(orig);

            /* return the action */
            return action;
        }

        /*
         *   Initialize the action for retrying with a missing direct object.
         *
         *   If we're trying a TIAction, we can only be coming from a TAction
         *   (since that's the only kind of original action that can turn into
         *   a two-object, at least in the base library).  That means the
         *   original action already has a direct object.  Now, since we're
         *   asking for a MISSING direct object, the only possibility is that
         *   the original action's direct object is our INDIRECT object.  For
         *   example: SWEEP WITH BROOM is turning into SWEEP <what> WITH
         *   BROOM.
         */
        initForMissingDobj(orig)
        {
            local origDobj = orig.getDobj();

            /*
             *   Set the indirect object in the new action to the direct object
             *   from the original action.  If there's no individual direct
             *   object yet, we must be retrying the overall command, before we
             *   started iterating through the individual dobjs, so copy the
             *   entire dobj list from the original.
             */
            iobjMatch = new PreResolvedProd(origDobj != nil
                                            ? origDobj : orig.dobjList_ );
        }

        /*
         *   Initialize the action for retrying with a missing indirect object.
         *
         *   We can only be coming from a TAction, so the TAction will have a
         *   direct object already.  Simply copy that as our own direct
         *   object.  For example: UNLOCK DOOR is turning into UNLOCK DOOR
         *   WITH <what>.
         */
        initForMissingIobj(orig)
        {
            local origDobj = orig.getDobj();

            /*
             *   Copy the direct object from the original.  If there's no
             *   individual direct object yet, we must be retrying the overall
             *   command, before we started iterating through the individual
             *   dobjs, so copy the entire dobj list.
             */
            dobjMatch = new PreResolvedProd(origDobj != nil
                                            ? origDobj : orig.dobjList_);
        }

        /*
         *   The root production to use to parse missing indirect object
         *   responses.  By default, this is singleNoun, but individual
         *   actions can override this as appropriate.
         *
         *   Note that language modules might want to override this to allow
         *   for special responses.  For example, in English, most verbs will
         *   want to override this with a specialized production that allows
         *   the appropriate preposition in the response.
         */
        askIobjResponseProd = singleNoun

        /* get the missing object response production for a given role */
        getObjResponseProd(resolver)
        {
            /* if it's the indirect object, return the iobj response prod */
            if (resolver.whichObject == IndirectObject)
                return askIobjResponseProd;

            /* otherwise use the default handling */
            return inherited(resolver);
        }

        /*
         *   Resolution order - returns DirectObject or IndirectObject to
         *   indicate which noun phrase to resolve first in resolveNouns().
         *   By default, we'll resolve the indirect object first, but
         *   individual actions can override this to resolve in a non-default
         *   order.
         */
        resolveFirst = IndirectObject

        /*
         *   Empty phrase resolution order.  This is similar to the standard
         *   resolution order (resolveFirst), but is used only when both the
         *   direct and indirect objects are empty phrases.
         *
         *   When both phrases are empty, we will either use a default or
         *   prompt interactively for the missing phrase.  In most cases, it
         *   is desirable to prompt interactively for a missing direct object
         *   first, regardless of the usual resolution order.
         */
        resolveFirstEmpty = DirectObject

        /*
         *   Determine which object to call first for action processing.  By
         *   default, we execute in the same order as we resolve, but this can
         *   be overridden if necessary.
         */
        execFirst = (resolveFirst)

        /*
         *   Can the indirect object potentially resolve to the given
         *   simulation object?  This only determines if the object is a
         *   *syntactic* match, meaning that it can match at a vocabulary and
         *   grammar level.  This doesn't test it for logicalness or check that
         *   it's an otherwise valid resolution.
         */
        canIobjResolveTo(obj)
        {
            /* check our iobj match tree to see if it can resolve to 'obj' */
            return iobjMatch.canResolveTo(
                obj, self, issuer_, actor_, IndirectObject);
        }

        /*
         *   resolve our noun phrases to objects
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            local first;
            local objMatch1, objMatch2;
            local objList1, objList2;
            local getResolver1, getResolver2;
            local objCur1;
            local remapProp;
            local reResolveFirst;

            /*
             *   note in the results that we have two noun slots (the direct
             *   and indirect objects)
             */
            results.noteNounSlots(2);

            /* we have no current known direct or indirect object yet */
            dobjCur_ = nil;
            iobjCur_ = nil;

            /*
             *   presume we won't have to re-resolve the first noun phrase,
             *   and clear out our record of anaphor bindings
             */
            reResolveFirst = nil;
            needAnaphoricBinding_ = nil;
            lastObjList_ = nil;

            /*
             *   Determine which object we want to resolve first.  If both
             *   phrases are empty, use the special all-empty ordering;
             *   otherwise, use the standard ordering for this verb.
             */
            if (dobjMatch.isEmptyPhrase && iobjMatch.isEmptyPhrase)
            {
                /* both phrases are empty - use the all-empty ordering */
                first = resolveFirstEmpty;
            }
            else
            {
                /*
                 *   we have at least one non-empty phrase, so use our
                 *   standard ordering
                 */
                first = resolveFirst;
            }

            /*
             *   The resolution process is symmetrical for the two possible
             *   resolution orders (direct object first or indirect object
             *   first); all we need to do is to set up the parameters we'll
             *   need according to which order we're using.
             *
             *   This parameterized approach makes the code further below a
             *   little mind-boggling, because it's using so much indirection.
             *   But the alternative is worse: the alternative is to
             *   essentially make two copies of the code below (one for the
             *   dobj-first case and one for the iobj-first case), which is
             *   prone to maintenance problems because of the need to keep the
             *   two copies synchronized when making any future changes.
             */
            if (first == DirectObject)
            {
                objMatch1 = dobjMatch;
                objMatch2 = iobjMatch;
                objList1 = &dobjList_;
                objList2 = &iobjList_;
                getResolver1 = &getDobjResolver;
                getResolver2 = &getIobjResolver;
                objCur1 = &dobjCur_;
                remapProp = remapDobjProp;
            }
            else
            {
                objMatch1 = iobjMatch;
                objMatch2 = dobjMatch;
                objList1 = &iobjList_;
                objList2 = &dobjList_;
                getResolver1 = &getIobjResolver;
                getResolver2 = &getDobjResolver;
                objCur1 = &iobjCur_;
                remapProp = remapIobjProp;
            }

            /*
             *   Get the unfiltered second-resolved object list - this will
             *   give the first-resolved object resolver access to some
             *   minimal information about the possible second-resolved object
             *   or objects.
             *
             *   Note that we're not *really* resolving the second object here
             *   - it is the second-resolved object, after all.  What we're
             *   doing is figuring out the *potential* set of objects that
             *   could resolve to the second phrase, with minimal
             *   disambiguation, so that the first object resolver is not
             *   totally in the dark about the second object's potential
             *   resolution.
             */
            initTentative(issuingActor, targetActor, first);

            /* resolve the first-resolved noun phrase */
            self.(objList1) = objMatch1.resolveNouns(
                self.(getResolver1)(issuingActor, targetActor, true), results);

            /*
             *   If the first-resolved noun phrase asked for an anaphoric
             *   binding, we'll need to go back and re-resolve that noun
             *   phrase after we resolve our other noun phrase, since the
             *   first-resolved phrase refers to the second-resolved phrase.
             */
            reResolveFirst = needAnaphoricBinding_;

            /*
             *   if the second-resolved phrase uses an anaphoric pronoun, the
             *   anaphor can be taken as referring to the first-resolved
             *   phrase's results
             */
            lastObjList_ = self.(objList1);

            /*
             *   if the first-resolved phrase resolves to just one object, we
             *   can immediately set the current resolved object, so that we
             *   can use it while resolving the second-resolved object list
             */
            if (self.(objList1).length() == 1)
            {
                /* set the current first-resolved object */
                self.(objCur1) = self.(objList1)[1].obj_;

                /* if remapping is allowed at this point, look for a remapping */
                if (results.allowActionRemapping)
                {
                    withParserGlobals(issuingActor, targetActor, self,
                                      function()
                    {
                        local remapInfo;

                        /* check for a remapping */
                        if ((remapInfo = self.(objCur1).(remapProp)) != nil)
                        {
                            /*
                             *   we have a remapping, so apply it - note that
                             *   we're still in the process of resolving noun
                             *   phrases (since we've only resolved one of our
                             *   two noun phrases so far), so pass 'true' for
                             *   the inResolve parameter
                             */
                            remapAction(true, first, remapInfo);
                        }
                    });
                }
            }

            /* resolve the second-resolved object */
            self.(objList2) = objMatch2.resolveNouns(
                self.(getResolver2)(issuingActor, targetActor, true), results);

            /*
             *   if we have to re-resolve the first noun phrase due to an
             *   anaphoric pronoun, go back and do that now
             */
            if (reResolveFirst)
            {
                /*
                 *   use the second-resolved noun phrase as the referent of
                 *   the anaphoric pronoun(s) in the first-resolved phrase
                 */
                lastObjList_ = self.(objList2);

                /* re-resolve the first object list */
                self.(objList1) = objMatch1.resolveNouns(
                    self.(getResolver1)(issuingActor, targetActor, true),
                    results);
            }
        }

        /* we have a direct and indirect object */
        predicateNounPhrases = [&dobjMatch, &iobjMatch]

        /* get an object role */
        getRoleFromIndex(idx)
        {
            /*
             *   the second object is always our indirect object; for other
             *   roles, defer to the inherited behavior
             */
            return (idx == 2 ? IndirectObject : inherited(idx));
        }

        /* get the resolved object in a given role */
        getObjectForRole(role)
        {
            /* return the indirect object if requested; otherwise inherit */
            return (role == IndirectObject ? getIobj() : inherited(role));
        }

        /* get the ResolveInfo for the given resolved object */
        getResolveInfo(obj, oldRole)
        {
            local info;

            /* scan our resolved direct object list for the given object */
            if (dobjList_ != nil)
                info = dobjList_.valWhich({x: x.obj_ == obj});

            /* if we didn't find it there, try the indirect object list */
            if (info == nil && iobjList_ != nil)
                info = iobjList_.valWhich({x: x.obj_ == obj});

            /* if we didn't find one, create one from scratch */
            if (info == nil)
            {
                /* get the list for the old role */
                local lst = (oldRole == DirectObject ? dobjList_ : iobjList_);

                /*
                 *   if there's anything in the old role, copy its flags and
                 *   noun phrase to the new role
                 */
                if (lst.length() > 0)
                {
                    /* copy the old role's attributes */
                    info = new ResolveInfo(obj, lst[1].flags_, lst[1].np_);
                }
                else
                {
                    /* nothing in the old role - start from scratch */
                    info = new ResolveInfo(obj, 0, nil);
                }
            }

            /* return what we found (or created) */
            return info;
        }

        /* get the OtherObject role for the given role */
        getOtherObjectRole(role)
        {
            /* the complementary roles are DirectObject and IndirectObject */
            return (role == IndirectObject ? DirectObject : IndirectObject);
        }

        /* get the match tree for the noun phrase in the given role */
        getMatchForRole(role)
        {
            /* return the indirect object match tree if requested; else inherit */
            return (role == IndirectObject ? iobjMatch : inherited(role));
        }

        /* get the 'verify' property for a given object role */
        getVerifyPropForRole(role)
        {
            return (role == IndirectObject ? verIobjProp : inherited(role));
        }

        /* get the 'preCond' property for a given object role */
        getPreCondPropForRole(role)
        {
            return (role == IndirectObject ? preCondIobjProp : inherited(role));
        }

        /* get the 'remap' property for a given object role */
        getRemapPropForRole(role)
        {
            return (role == IndirectObject ? remapIobjProp : inherited(role));
        }

        /* get the list of resolved objects in the given role */
        getResolvedObjList(which)
        {
            return (which == IndirectObject ? getResolvedIobjList()
                    : inherited(which));
        }

        /* get the list of resolved indirect objects */
        getResolvedIobjList()
        {
            /*
             *   if we have an indirect object list, return the objects from
             *   it; otherwise, return an empty list
             */
            return (iobjList_ == nil ? nil : iobjList_.mapAll({x: x.obj_}));
        }

        /*
         *   Manually set the resolved objects.  We'll set our direct and
         *   indirect objects.
         */
        setResolvedObjects(dobj, iobj)
        {
            /* inherit the base class handling to set the direct object */
            inherited(dobj);

            /* set the resolved iobj */
            setResolvedIobj(iobj);
        }

        /* set a resolved iobj */
        setResolvedIobj(iobj)
        {
            /* build a pre-resolved production for the indirect object */
            iobjMatch = new PreResolvedProd(iobj);

            /* set the resolved indirect object */
            iobjList_ = makeResolveInfoList(iobj);

            /* set the current indirect object as well */
            iobjCur_ = (iobjList_.length() > 0 ? iobjList_[1].obj_ : nil);
            iobjInfoCur_ = (iobjList_.length() > 0 ? iobjList_[1] : nil);
        }

        /* manually set the unresolved object noun phrase match trees */
        setObjectMatches(dobj, iobj)
        {
            /* inherit default handling */
            inherited(dobj);

            /*
             *   if the iobj is a ResolveInfo, set it as a resolved object;
             *   otherwise set it as an unresolved match tree
             */
            if (iobj.ofKind(ResolveInfo))
                setResolvedIobj(iobj);
            else
                iobjMatch = iobj;
        }

        /*
         *   Get the anaphoric binding for the noun phrase we're currently
         *   resolving.
         */
        getAnaphoricBinding(typ)
        {
            /* if we've resolved a prior noun phrase, return it */
            if (lastObjList_ != nil)
                return lastObjList_;

            /*
             *   we don't have a prior noun phrase - make a note that we have
             *   to come back and re-resolve the current noun phrase
             */
            needAnaphoricBinding_ = true;

            /*
             *   return an empty list to indicate that the anaphor is
             *   acceptable in this context but has no binding yet
             */
            return [];
        }

        /*
         *   The last object list we resolved.  We keep track of this so that
         *   we can provide it as the anaphoric binding, if an anaphor binding
         *   is requested.
         */
        lastObjList_ = nil

        /*
         *   Flag: we have been asked for an anaphoric binding, but we don't
         *   have a binding available.  We'll check this after resolving the
         *   first-resolved noun phrase so that we can go back and re-resolve
         *   it again after resolving the other noun phrase.
         */
        needAnaphoricBinding_ = nil

        /* check that the resolved objects are in scope */
        resolvedObjectsInScope()
        {
            /*
             *   check the indirect object, and inherit the base class handling
             *   to check the direct object
             */
            return (inherited()
                    && (getIobjResolver(gIssuingActor, gActor, true)
                        .objInScope(iobjList_[1].obj_)));
        }

        /*
         *   get our indirect object resolver, or create one if we haven't
         *   already cached one
         */
        getIobjResolver(issuingActor, targetActor, reset)
        {
            /* if we don't already have one cached, create a new one */
            if (iobjResolver_ == nil)
                iobjResolver_ = createIobjResolver(issuingActor, targetActor);

            /* reset the resolver if desired */
            if (reset)
                iobjResolver_.resetResolver();

            /* return the cached resolver */
            return iobjResolver_;
        }

        /*
         *   Create our indirect object resolver.  By default, we'll use a
         *   basic indirect object resolver.
         */
        createIobjResolver(issuingActor, targetActor)
        {
            /* create and return a new basic indirect object resolver */
            return new IobjResolver(self, issuingActor, targetActor);
        }

        /*
         *   Resolve 'all' for the indirect object.  By default, we'll return
         *   everything in the scope list.
         */
        getAllIobj(actor, scopeList)
        {
            return scopeList;
        }

        /* filter an ambiguous indirect object noun phrase */
        filterAmbiguousIobj(lst, requiredNum, np)
        {
            /* filter using the indirect object verifier method */
            return filterAmbiguousWithVerify(lst, requiredNum, verIobjProp,
                                             preCondIobjProp, remapIobjProp,
                                             IndirectObject, np);
        }

        /* filter a plural phrase */
        filterPluralIobj(lst, np)
        {
            /* filter using the direct object verifier method */
            return filterPluralWithVerify(lst, verIobjProp, preCondIobjProp,
                                          remapIobjProp, IndirectObject, np);
        }

        /* get the default indirect object */
        getDefaultIobj(np, resolver)
        {
            /* get a default indirect object using the verify method */
            return getDefaultWithVerify(resolver, verIobjProp, preCondIobjProp,
                                        remapIobjProp, IndirectObject, np);
        }

        /*
         *   Execute the action.  We'll run through the execution sequence
         *   once for each resolved object in our direct or indirect object
         *   list, depending on which one is the list and which one is the
         *   singleton.
         */
        doActionMain()
        {
            local lst;
            local preAnnouncedDobj;
            local preAnnouncedIobj;

            /*
             *   Get the list of resolved objects for the multiple object.  If
             *   neither has multiple objects, it doesn't matter which is
             *   iterated, since we'll just do the command once anyway.
             */
            lst = (iobjList_.length() > 1 ? iobjList_ : dobjList_);

            /*
             *   Set the pronoun antecedents, using the game-specific pronoun
             *   setter.  Don't set an antecedent for a nested command.
             */
            if (parentAction == nil)
            {
               /*
                *   Set both direct and indirect objects as potential
                *   antecedents.  Rather than trying to figure out right now
                *   which one we might want to refer to in the future, remember
                *   both - we'll decide which one is the logical antecedent
                *   when we find a pronoun to resolve in a future command.
                */
               gActor.setPronounMulti(dobjList_, iobjList_);

                /*
                 *   If one or the other object phrase was specified in the
                 *   input as a pronoun, keep the meaning of that pronoun the
                 *   same, overriding whatever we just did.  Note that the
                 *   order we use here doesn't matter: if a given pronoun
                 *   appears in only one of the two lists, then the list where
                 *   it's not set has no effect on the pronoun, hence it
                 *   doesn't matter which comes first; if a pronoun appears in
                 *   both lists, it will have the same value in both lists, so
                 *   we'll just do the same thing twice, so, again, order
                 *   doesn't matter.
                 */
                setPronounByInput(dobjList_);
                setPronounByInput(iobjList_);
            }

            /*
             *   pre-announce the non-list object if appropriate - this will
             *   provide a common pre-announcement if we iterate through
             *   several announcements of the main list objects
             */
            if (lst == dobjList_)
            {
                /* pre-announce the single indirect object if needed */
                preAnnouncedIobj = preAnnounceActionObject(
                    iobjList_[1], dobjList_, IndirectObject);

                /* we haven't announced the direct object yet */
                preAnnouncedDobj = nil;

                /* pre-calculate the multi-object announcements */
                cacheMultiObjectAnnouncements(dobjList_, DirectObject);
            }
            else
            {
                /* pre-announce the single direct object if needed */
                preAnnouncedDobj = preAnnounceActionObject(
                    dobjList_[1], iobjList_, DirectObject);

                /* we haven't announced the indirect object yet */
                preAnnouncedIobj = nil;

                /* pre-calculate the multi-object announcements */
                cacheMultiObjectAnnouncements(iobjList_, IndirectObject);
            }

            /* we haven't yet canceled the iteration */
            iterationCanceled = nil;

            /* iterate over the resolved list for the multiple object */
            for (local i = 1, local len = lst.length() ;
                 i <= len && !iterationCanceled ; ++i)
            {
                local dobjInfo;
                local iobjInfo;

                /*
                 *   make the current list item the direct or indirect object,
                 *   as appropriate
                 */
                if (lst == dobjList_)
                {
                    /* the direct object is the multiple object */
                    dobjInfo = dobjInfoCur_ = lst[i];
                    iobjInfo = iobjInfoCur_ = iobjList_[1];
                }
                else
                {
                    /* the indirect object is the multiple object */
                    dobjInfo = dobjInfoCur_ = dobjList_[1];
                    iobjInfo = iobjInfoCur_ = lst[i];
                }

                /* get the current dobj and iobj from the resolve info */
                dobjCur_ = dobjInfo.obj_;
                iobjCur_ = iobjInfo.obj_;

                /*
                 *   if the action was remapped, and we need to announce
                 *   anything, announce the entire action
                 */
                if (isRemapped())
                {
                    /*
                     *   We were remapped.  The entire phrasing of the new
                     *   action might have changed from what the player typed,
                     *   so it might be nonsensical to show the objects as we
                     *   usually would, as sentence fragments that are meant
                     *   to combine with what the player actually typed.  So,
                     *   instead of showing the usual sentence fragments, show
                     *   the entire phrasing of the command.
                     *
                     *   Only show the announcement if we have a reason to: we
                     *   have unclear disambiguation in one of the objects, or
                     *   one of the objects is defaulted.
                     *
                     *   If we don't want to announce the remapped action,
                     *   still consider showing a multi-object announcement,
                     *   if we would normally need to do so.
                     */
                    if (needRemappedAnnouncement(dobjInfo)
                        || needRemappedAnnouncement(iobjInfo))
                    {
                        /* show the remapped announcement */
                        gTranscript.announceRemappedAction();
                    }
                    else
                    {
                        /* announce the multiple dobj if necessary */
                        if (!preAnnouncedDobj)
                            maybeAnnounceMultiObject(
                                dobjInfo, dobjList_.length(), DirectObject);

                        /* announce the multiple iobj if necessary */
                        if (!preAnnouncedIobj)
                            maybeAnnounceMultiObject(
                                iobjInfo, iobjList_.length(), IndirectObject);
                    }
                }
                else
                {
                    /* announce the direct object if appropriate */
                    if (!preAnnouncedDobj)
                        announceActionObject(dobjInfo, dobjList_.length(),
                                             DirectObject);

                    /* announce the indirect object if appropriate */
                    if (!preAnnouncedIobj)
                        announceActionObject(iobjInfo, iobjList_.length(),
                                             IndirectObject);
                }

                /* run the execution sequence for the current direct object */
                doActionOnce();

                /* if we're top-level, count the iteration in the transcript */
                if (parentAction == nil)
                    gTranscript.newIter();
            }
        }

        /*
         *   Set the pronoun according to the pronoun type actually used in
         *   the input.  For example, if we said PUT BOX ON IT, we want IT to
         *   continue referring to whatever IT referred to before this command
         *   - we specifically do NOT want IT to refer to the BOX in this
         *   case.
         */
        setPronounByInput(lst)
        {
            local objs;

            /* get the subset of the list that was specified by pronoun */
            lst = lst.subset({x: x.pronounType_ != nil});

            /*
             *   Get a list of the unique objects in the list.  This will
             *   ensure that we can distinguish THEM from IT AND IT AND IT: if
             *   we have the same pronoun appearing with multiple objects,
             *   we'll know that it's because the pronoun was actually plural
             *   (THEM) rather than a singular pronoun that was repeated (IT
             *   AND IT AND IT).
             */
            objs = lst.mapAll({x: x.obj_}).getUnique();

            /*
             *   Now retain one 'lst' element for each 'objs' element.  This
             *   is a bit tricky: we're mapping each element in 'objs' to pick
             *   out one element of 'lst' where the 'lst' element points to
             *   the 'objs' element.
             */
            lst = objs.mapAll({o: lst.valWhich({l: l.obj_ == o})});

            /*
             *   Now we can set the pronouns.  Go through the list, and set
             *   each different pronoun type that appears.  Set it to the
             *   subset of the list with that matches that pronoun type, and
             *   then remove that subset and keep iterating to pick up the
             *   remaining pronoun types.
             */
            while (lst.length() != 0)
            {
                local cur;
                local typ;

                /* on this iteration, handle the first pronoun type in the list */
                typ = lst[1].pronounType_;

                /* pick out the subset with the current pronoun type */
                cur = lst.subset({x: x.pronounType_ == typ});

                /* set the current pronoun to the current list */
                gActor.setPronounByType(typ, cur);

                /* remove the subset we just handled from the remaining list */
                lst -= cur;
            }
        }

        /*
         *   Determine if we need to announce this action when the action was
         *   remapped, based on the resolution information for one of our
         *   objects.  We need to announce a remapped action when either
         *   object had unclear disambiguation or was defaulted.
         */
        needRemappedAnnouncement(info)
        {
            /*
             *   if it's a defaulted object that hasn't been announced, or it
             *   was unclearly disambiguated, we need an announcement
             */
            return (((info.flags_ & DefaultObject) != 0
                     && (info.flags_ & AnnouncedDefaultObject) == 0)
                    || (info.flags_ & UnclearDisambig) != 0);
        }

        /*
         *   Verify the action.
         */
        verifyAction()
        {
            local result;

            /* invoke any general (non-object) pre-condition verifiers */
            result = callVerifyPreCond(nil);

            /* check the direct object */
            result = callVerifyProp(dobjCur_, verDobjProp, preCondDobjProp,
                                    remapDobjProp, result, DirectObject);

            /*
             *   Check the indirect object, combining the results with the
             *   direct object results.  We combine the results for the two
             *   objects because we're simply looking for any reason that we
             *   can't perform the command.
             */
            result = callVerifyProp(iobjCur_, verIobjProp, preCondIobjProp,
                                    remapIobjProp, result, IndirectObject);

            /*
             *   check that the action method ("doXxx") is defined on at least
             *   one of the objects
             */
            return verifyHandlersExist(
                [dobjCur_, iobjCur_],
                [verDobjProp, checkDobjProp, actionDobjProp,
                verIobjProp, checkIobjProp, actionIobjProp],
                result);
        }

        /* initialize tentative resolutions for other noun phrases */
        initTentative(issuingActor, targetActor, whichObj)
        {
            local tRes;
            local ti, td;

            /*
             *   remember the old tentative direct and indirect objects, then
             *   set them to empty lists - this will ensure that we don't
             *   recursively try to get a tentative resolution for the object
             *   we're working on right now, which would cause infinite
             *   recursion
             */
            td = tentativeDobj_;
            ti = tentativeIobj_;

            /* set them to empty lists to indicate they don't need resolving */
            tentativeDobj_ = [];
            tentativeIobj_ = [];

            /* make sure our built-in dobj resolver is initialized */
            issuer_ = issuingActor;
            actor_ = targetActor;

            /* make sure we set things back when we're done */
            try
            {
                /* initialize the other noun phrase */
                if (whichObj == DirectObject && ti == nil)
                {
                    /* tentatively resolve the indirect object */
                    tRes = new TentativeResolveResults(targetActor, issuingActor);
                    ti = iobjMatch.resolveNouns(
                        getIobjResolver(issuingActor, targetActor, true), tRes);

                    /*
                     *   if the list is empty, and we didn't have a missing
                     *   noun phrase, use a dummy object as the tentative
                     *   resolution - this distinguishes erroneous noun phrases
                     *   from those that are simply missing
                     */
                    if (ti == [] && !tRes.npMissing)
                        ti = [dummyTentativeInfo];
                }
                else if (whichObj == IndirectObject && td == nil)
                {
                    /* tentatively resolve the direct object */
                    tRes = new TentativeResolveResults(targetActor, issuingActor);
                    td = dobjMatch.resolveNouns(
                        getDobjResolver(issuingActor, targetActor, true), tRes);

                    /* use a dummy object if appropriate */
                    if (td == [] && !tRes.npMissing)
                        td = [dummyTentativeInfo];
                }
            }
            finally
            {
                /* set the original (or updated) tentative lists */
                tentativeDobj_ = td;
                tentativeIobj_ = ti;
            }
        }

        /*
         *   Check for remapping
         */
        checkRemapping()
        {
            local remapInfo;
            local role;

            /* presume we'll find remapping for the first-resolved object */
            role = resolveFirst;

            /* check remapping for each object, in the resolution order */
            if (resolveFirst == DirectObject)
            {
                /* the direct object is resolved first, so try it first */
                if ((remapInfo = dobjCur_.(remapDobjProp)) == nil)
                {
                    remapInfo = iobjCur_.(remapIobjProp);
                    role = IndirectObject;
                }
            }
            else
            {
                /* the indirect object is resolved first, so remap it first */
                if ((remapInfo = iobjCur_.(remapIobjProp)) == nil)
                {
                    remapInfo = dobjCur_.(remapDobjProp);
                    role = DirectObject;
                }
            }

            /* if we found a remapping, apply it */
            if (remapInfo != nil)
                remapAction(nil, role, remapInfo);
        }

        /*
         *   Check the command.
         *
         *   For a two-object action, this first calls the catch-all 'check'
         *   methods (the dobjFor(Default) and dobjFor(All) methods) on the two
         *   objects (indirect object first), then calls the 'check' methods
         *   for this specific action (direct object first).
         */
        checkAction()
        {
            try
            {
                /* invoke the catch-all 'check' methods on each object */
                local defIo = callCatchAllProp(
                    iobjCur_, checkIobjProp, &checkIobjDefault, &checkIobjAll);
                local defDo = callCatchAllProp(
                    dobjCur_, checkDobjProp, &checkDobjDefault, &checkDobjAll);

                /*
                 *   invoke the 'check' method on each object, as long as it
                 *   overrides any corresponding Default 'check' handler
                 */
                if (!defDo)
                    dobjCur_.(checkDobjProp)();
                if (!defIo)
                    iobjCur_.(checkIobjProp)();
            }
            catch (ExitSignal es)
            {
                /* mark the action as a failure in the transcript */
                gTranscript.noteFailure();

                /* re-throw the signal */
                throw es;
            }
        }

        /*
         *   Execute the command.
         */
        execAction()
        {
            local defIo, defDo;

            /* invoke the catch-all 'action' method on each object */
            defIo = callCatchAllProp(iobjCur_, actionIobjProp,
                                     &actionIobjDefault, &actionIobjAll);
            defDo = callCatchAllProp(dobjCur_, actionDobjProp,
                                     &actionDobjDefault, &actionDobjAll);

            /*
             *   Invoke the action method on each object, starting with the
             *   non-list object.  Call these only if the corresponding
             *   default 'action' methods didn't override the verb-specific
             *   methods.
             */
            if (execFirst == DirectObject)
            {
                if (!defDo)
                    dobjCur_.(actionDobjProp)();
                if (!defIo)
                    iobjCur_.(actionIobjProp)();
            }
            else
            {
                if (!defIo)
                    iobjCur_.(actionIobjProp)();
                if (!defDo)
                    dobjCur_.(actionDobjProp)();
            }
        }

        /* get the precondition descriptor list for the action */
        getPreCondDescList()
        {
            /*
             *   return the inherited preconditions plus the conditions that
             *   apply to the indirect object
             */
            return inherited()
                + getObjPreCondDescList(iobjCur_, preCondIobjProp, iobjCur_,
                                        IndirectObject);
        }

        /*
         *   Get a message parameter object for the action.  We define 'dobj'
         *   as the direct object and 'iobj' as the indirect object, in
         *   addition to any inherited targets.
         */
        getMessageParam(objName)
        {
            switch(objName)
            {
            case 'iobj':
                /* return the current indirect object */
                return iobjCur_;

            default:
                /* inherit default handling */
                return inherited(objName);
            }
        }

        /* get the current indirect object being executed */
        getIobj() { return iobjCur_; }

        /* get the full ResolveInfo associated with the current indirect object */
        getIobjInfo() { return iobjInfoCur_; }

        /* get the object resolution flags for the indirect object */
        getIobjFlags() { return iobjInfoCur_ != nil ? iobjInfoCur_.flags_ : 0; }

        /* get the number of direct objects */
        getIobjCount() { return iobjList_ != nil ? iobjList_.length() : 0; }

        /* get the original token list of the current indirect object phrase */
        getIobjTokens()
        {
            return iobjInfoCur_ != nil && iobjInfoCur_.np_ != nil
                ? iobjInfoCur_.np_.getOrigTokenList() : nil;
        }

        /* get the original words (as a list of strings) of the current iobj */
        getIobjWords()
        {
            local l = getIobjTokens();
            return l != nil ? l.mapAll({x: getTokOrig(x)}) : nil;
        }

        /*
         *   Get the list of active objects.  We have a direct and indirect
         *   object.
         */
        getCurrentObjects()
        {
            return [dobjCur_, iobjCur_];
        }

        /* set the current objects */
        setCurrentObjects(lst)
        {
            /* remember the current direct and indirect objects */
            dobjCur_ = lst[1];
            iobjCur_ = lst[2];

            /* we don't have ResolveInfo objects for this call */
            dobjInfoCur_ = iobjInfoCur_ = nil;
        }

        /*
         *   Copy one tentative object list to the other.  This is useful when
         *   an object's verifier for one TIAction wants to forward the call
         *   to the other object verifier for a different TIAction.
         */
        copyTentativeObjs()
        {
            /* copy whichever tentative list is populated to the other slot */
            if (tentativeDobj_ != nil)
                tentativeDobj_ = tentativeIobj_;
            else
                tentativeIobj_ = tentativeDobj_;
        }

        /*
         *   Get the tentative direct/indirect object resolution lists.  A
         *   tentative list is available for the later-resolved object while
         *   resolving the earlier-resolved object.
         */
        getTentativeDobj() { return tentativeDobj_; }
        getTentativeIobj() { return tentativeIobj_; }

        /*
         *   the predicate grammar must assign the indirect object production
         *   tree to iobjMatch
         */
        iobjMatch = nil

        /* the indirect object list */
        iobjList_ = []

        /* current indirect object being executed */
        iobjCur_ = nil

        /* the full ResolveInfo associated with iobjCur_ */
        iobjInfoCur_ = nil

        /* my cached indirect object resolver */
        iobjResolver_ = nil

        /*
         *   The tentative direct and indirect object lists.  A tentative list
         *   is available for the later-resolved object while resolving the
         *   earlier-resolved object.
         */
        tentativeDobj_ = nil
        tentativeIobj_ = nil

        /*
         *   Verification and action properties for the indirect object.  By
         *   convention, the verification method for the indirect object of a
         *   two-object action is verIobjXxx; the check method is
         *   checkIobjXxx; and the action method is actionIobjXxx.
         */
        verIobjProp = nil
        preCondIobjProp = nil
        checkIobjProp = nil
        actionIobjProp = nil

        /*
         *   Action-remap properties for the indirect object.  By convention,
         *   the remapper properties are named remapDobjAction and
         *   remapIobjAction, for the direct and indirect objects,
         *   respectively, where Action is replaced by the root name of the
         *   action.
         */
        remapIobjProp = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Common base class for actions involving literal phrases.  This is a
     *   mix-in class that can be combined with Action subclasses to create
     *   specific kinds of literal actions.
     */
    class LiteralActionBase: object
        /*
         *   Get a message parameter.  We define 'literal' as the text of the
         *   literal phrase, in addition to inherited targets.
         */
        getMessageParam(objName)
        {
            switch(objName)
            {
            case 'literal':
                /* return the text of the literal phrase */
                return text_;

            default:
                /* inherit default handling */
                return inherited(objName);
            }
        }

        /* manually set the resolved objects */
        setResolvedObjects(txt)
        {
            /* remember the literal text */
            text_ = txt;
        }

        /* manually set the pre-resolved match trees */
        setObjectMatches(lit)
        {
            /* if it's not already a PreResolvedLiteralProd, wrap it */
            if (!lit.ofKind(PreResolvedLiteralProd))
            {
                /* save the literal text */
                text_ = lit;

                /* wrap it in a match tree */
                lit = new PreResolvedLiteralProd(lit);
            }

            /* note the new literal match tree */
            literalMatch = lit;
        }

        /* get the current literal text */
        getLiteral() { return text_; }

        /* the text of the literal phrase */
        text_ = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An action with a literal phrase as its only object, such as "say <any
     *   text>".  We'll accept anything as the literal phrase - a number, a
     *   quoted string, or arbitrary words - and treat them all simply as text.
     *
     *   The grammar rules that produce these actions must set literalMatch to
     *   the literal phrase's match tree.
     *
     *   Because we don't have any actual resolved objects, we're based on
     *   IAction.  Subclasses that implement particular literal actions should
     *   override execAction() to carry out the action; this method can call
     *   the getLiteral() method of self to get a string giving the literal
     *   text.
     */
    class LiteralAction: LiteralActionBase, IAction
        /*
         *   Resolve objects.  We don't actually have any objects to resolve,
         *   but we do have to get the text for the literal phrase.
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            /* the literal phrase counts as one noun slot */
            results.noteNounSlots(1);

            /*
             *   "Resolve" our literal phrase.  The literal phrase doesn't
             *   resolve to an object list the way a regular noun phrase would,
             *   but rather just resolves to a text string giving the original
             *   literal contents of the phrase.
             */
            literalMatch.resolveLiteral(results);

            /* retrieve the text of the phrase, exactly as the player typed it */
            text_ = literalMatch.getLiteralText(results, self, DirectObject);
        }

        /* we have a literal phrase as our only noun phrase */
        predicateNounPhrases = [&literalMatch]
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An action with a direct object and a literal, such as "turn dial to
     *   <setting>" or "type <string> on keypad".  We'll accept anything as the
     *   literal phrase - a number, a quoted string, or arbitrary words - and
     *   treat them all simply as text.
     *
     *   The grammar rules that produce these actions must set dobjMatch to the
     *   resolvable object of the command, and must set literalMatch to the
     *   literal phrase's match tree.  Note that we use dobjMatch as the
     *   resolvable object even if the object serves grammatically as the
     *   indirect object - this is a simplification, and the true grammatical
     *   purpose of the object isn't important since there's only one true
     *   object in the command.
     *
     *   When referring to objects by role (such as in remapTo), callers should
     *   ALWAYS refer to the resolvable object as DirectObject, and the literal
     *   phrase as IndirectObject.
     *
     *   Each subclass must set the property whichMessageLiteral to the
     *   grammatical role (DirectObject, IndirectObject) the literal phrase
     *   plays for message generation purposes.  This only affects messages; it
     *   doesn't affect anything else; in particular, regardless of the
     *   whichMessageLiteral setting, callers should always refer to the
     *   literal as IndirectObject when calling getObjectForRole() and the
     *   like, and should always call getDobj() to get the resolved version of
     *   the resolvable object phrase.
     */
    class LiteralTAction: LiteralActionBase, TAction
        /*
         *   Resolve objects.
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            /*
             *   the literal phrase counts as one noun slot, and the direct
             *   object as another
             */
            results.noteNounSlots(2);

            /*
             *   If the literal phrase serves as the direct object in generated
             *   messages, ask for its literal text first, so that we ask for
             *   it interactively first.  Otherwise, get the tentative literal
             *   text, in case it's useful to have in resolving the other
             *   object.
             */
            if (whichMessageLiteral == DirectObject)
                text_ = literalMatch.getLiteralText(results, self, DirectObject);
            else
                text_ = literalMatch.getTentativeLiteralText();

            /* resolve the direct object */
            dobjList_ = dobjMatch.resolveNouns(
                getDobjResolver(issuingActor, targetActor, true), results);

            /*
             *   "Resolve" the literal, ignoring the result - we call this so
             *   that the literal can do any scoring it wants to do during
             *   resolution; but since we're treating it literally, it
             *   obviously has no actual resolved value.
             */
            literalMatch.resolveLiteral(results);

            /* the literal phrase resolves to the text only */
            text_ = literalMatch.getLiteralText(results, self,
                                                whichMessageLiteral);
        }

        /* we have a direct object and a literal phrase */
        predicateNounPhrases = [&dobjMatch, &literalMatch]

        /* get an object role */
        getRoleFromIndex(idx)
        {
            /*
             *   index 2 is the literal object, which is always in the indirect
             *   object role; for others, inherit the default handling
             */
            return (idx == 2 ? IndirectObject : inherited(idx));
        }

        /* get the OtherObject role for the given role */
        getOtherObjectRole(role)
        {
            /* the complementary roles are DirectObject and IndirectObject */
            return (role == DirectObject ? IndirectObject : DirectObject);
        }

        /* get the resolved object in a given role */
        getObjectForRole(role)
        {
            /*
             *   the literal is in the IndirectObject role; inherit the default
             *   for others
             */
            return (role == IndirectObject ? text_ : inherited(role));
        }

        /* get the match tree for the given role */
        getMatchForRole(role)
        {
            /*
             *   the literal is in the IndirectObject role; inherit the default
             *   for anything else
             */
            return (role == IndirectObject ? literalMatch : inherited(role));
        }

        /* manually set the resolved objects */
        setResolvedObjects(dobj, txt)
        {
            /* inherit default TAction handling for the direct object */
            inherited TAction(dobj);

            /* inherit the LiteralActionBase handling for the literal text */
            inherited LiteralActionBase(txt);
        }

        /* manually set the pre-resolved match trees */
        setObjectMatches(dobj, lit)
        {
            /* inherit default TAction handling for the direct object */
            inherited TAction(dobj);

            /* inherit the default LiteralActionBase handling for the literal */
            inherited LiteralActionBase(lit);
        }

        /*
         *   Get a list of the current objects.  We include only the direct
         *   object here, since the literal text is not a resolved object but
         *   simply literal text.
         */
        getCurrentObjects() { return [dobjCur_]; }

        /* set the current objects */
        setCurrentObjects(lst)
        {
            dobjCur_ = lst[1];
            dobjInfoCur_ = nil;
        }

        /*
         *   Retry a single-object action as an action taking both an object
         *   and a literal phrase.  We'll treat the original action's direct
         *   object list as our direct object list, and obtain a literal
         *   phrase interactively.
         *
         *   This routine terminates with 'exit' if it doesn't throw some
         *   other error.
         */
        retryWithMissingLiteral(orig)
        {
            local action;

            /* create the new action based on the original action */
            action = createForRetry(orig);

            /* use an empty literal phrase for the new action */
            action.literalMatch = new EmptyLiteralPhraseProd();

            /* initialize for the missing literal phrase */
            action.initForMissingLiteral(orig);

            /* resolve and execute the replacement action */
            resolveAndReplaceAction(action);
        }

        /* initialize with a missing direct object phrase */
        initForMissingDobj(orig)
        {
            /*
             *   Since we have a missing direct objet, we must be coming from
             *   a LiteralAction.  The LiteralAction will already have a
             *   literal phrase, so copy it.  For example: WRITE HELLO ->
             *   WRITE HELLO ON <what>.
             */
            literalMatch = new PreResolvedLiteralProd(orig.getLiteral());
        }

        /* initialize for a missing literal phrase */
        initForMissingLiteral(orig)
        {
            local origDobj = orig.getDobj();

            /*
             *   Since we have a missing literal, we must be coming from a
             *   TAction.  The TAction will already have a direct object,
             *   which we'll want to keep as our own direct object.  For
             *   example: WRITE ON SLATE -> WRITE <what> ON SLATE.
             */
            dobjMatch = new PreResolvedProd(origDobj != nil
                                            ? origDobj : orig.dobjList_);
        }

        /* object role played by the literal phrase */
        whichMessageLiteral = nil

        /* -------------------------------------------------------------------- */
        /*
         *   Direct Object Resolver implementation.  We serve as our own
         *   direct object resolver, so we define any special resolver
         *   behavior here.
         */

        /*
         *   the true grammatical role of the resolved object is always the
         *   direct object
         */
        whichObject = DirectObject

        /*
         *   What we call our direct object might actually be playing the
         *   grammatical role of the indirect object - in order to inherit
         *   easily from TAction, we call our resolved object our direct
         *   object, regardless of which grammatical role it actually plays.
         *   For the most part it doesn't matter which is which; but for the
         *   purposes of our resolver, we actually do care about its real role.
         *   So, override the resolver method whichMessageObject so that it
         *   returns whichever role is NOT served by the topic object.
         */
        whichMessageObject =
            (whichMessageLiteral == DirectObject ? IndirectObject : DirectObject)
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Base class for actions that include a "topic" phrase.  This is a
     *   mix-in class that can be used in different types of topic actions.  In
     *   all cases, the topic phrase must be assigned to the 'topicMatch'
     *   property in grammar rules based on this class.
     */
    class TopicActionBase: object
        /*
         *   Resolve the topic phrase.  This resolves the match tree in out
         *   'topicMatch' property, and stores the result in topicList_.  This
         *   is for use in resolveNouns().
         */
        resolveTopic(issuingActor, targetActor, results)
        {
            /* get the topic resolver */
            local resolver = getTopicResolver(issuingActor, targetActor, true);

            /* resolve the topic match tree */
            topicList_ = topicMatch.resolveNouns(resolver, results);

            /* make sure it's properly packaged as a ResolvedTopic */
            topicList_ = resolver.packageTopicList(topicList_, topicMatch);
        }

        /*
         *   Set the resolved topic to the given object list.  This is for use
         *   in setResolvedObjects().
         */
        setResolvedTopic(topic)
        {
            /* if the topic isn't given as a ResolvedTopic, wrap it in one */
            if (!topic.ofKind(ResolvedTopic))
                topic = ResolvedTopic.wrapObject(topic);

            /* use the match object from the ResolvedTopic */
            topicMatch = topic.topicProd;

            /* finally, make a ResolveInfo list out of the ResolvedTopic */
            topicList_ = makeResolveInfoList(topic);
        }

        /*
         *   Set the topic match tree.  This is for use in setObjectMatches().
         */
        setTopicMatch(topic)
        {
            /*
             *   If it's given as a ResolveInfo, wrap it in a PreResolvedProd.
             *   Otherwise, if it's not a topic match (a TopicProd object), and
             *   it has tokens, re-parse it as a topic match to make sure we
             *   get the grammar optimized for global scope.
             */
            if (topic.ofKind(ResolveInfo))
            {
                /* it's a resolved object, so wrap it in a fake match tree */
                topic = new PreResolvedProd(topic);
            }
            else if (!topic.ofKind(TopicProd))
            {
                /*
                 *   Re-parse it as a topic phrase.  If our dobj resolver knows
                 *   the actors, use those; otherwise default to the player
                 *   character.
                 */
                local issuer = (issuer_ != nil ? issuer_ : gPlayerChar);
                local target = (actor_ != nil ? actor_ : gPlayerChar);
                topic = reparseMatchAsTopic(topic, issuer, target);
            }

            /* save the topic match */
            topicMatch = topic;
        }

        /*
         *   Re-parse a match tree as a topic phrase.  Returns a TopicProd
         *   match tree, if possible.
         */
        reparseMatchAsTopic(topic, issuingActor, targetActor)
        {
            local toks;

            /* we can only proceed if the match tree has a token list */
            if ((toks = topic.getOrigTokenList()) != nil && toks.length() > 0)
            {
                /* parse it as a topic phrase */
                local match = topicPhrase.parseTokens(toks, cmdDict);

                /*
                 *   if we parsed it successfully, and we have more than one
                 *   match, get the best of the bunch
                 */
                if (match != nil && match.length() > 1)
                {
                    /* set up a topic resolver for the match */
                    local resolver = getTopicResolver(
                        issuingActor, targetActor, true);

                    /* sort it and return the best match */
                    return CommandRanking.sortByRanking(match, resolver)[1].match;
                }
                else if (match != nil && match.length() > 0)
                {
                    /* we found exactly one match, so use it */
                    return match[1];
                }
            }

            /*
             *   if we make it this far, we couldn't parse it as a topic, so
             *   just return the original
             */
            return topic;
        }

        /*
         *   get the topic resolver
         */
        getTopicResolver(issuingActor, targetActor, reset)
        {
            /* create one if we don't already have one */
            if (topicResolver_ == nil)
                topicResolver_ = createTopicResolver(issuingActor, targetActor);

            /* reset the resolver if desired */
            if (reset)
                topicResolver_.resetResolver();

            /* return the cached resolver */
            return topicResolver_;
        }

        /*
         *   Create the topic resolver.
         */
        createTopicResolver(issuingActor, targetActor)
        {
            return new TopicResolver(
                self, issuingActor, targetActor, topicMatch, whichMessageTopic,
                getTopicQualifierResolver(issuingActor, targetActor, nil));
        }

        /*
         *   Get a message parameter by name.  We'll return the topic for the
         *   keyword 'topic', and inherit the default handling for anything
         *   else.
         */
        getMessageParam(objName)
        {
            switch(objName)
            {
            case 'topic':
                /* return the topic */
                return getTopic();

            default:
                /* inherit default handling */
                return inherited(objName);
            }
        }

        /* get the current topic */
        getTopic()
        {
            /*
             *   Because the topic list always contains one entry (a
             *   ResolvedTopic object encapsulating the topic information),
             *   our current topic is always simply the first and only element
             *   of the topic list.
             */
            return topicList_[1].obj_;
        }

        /*
         *   Get the resolver for qualifiers we find in the topic phrase
         *   (qualifiers that might need resolution include things like
         *   possessive adjectives and locational phrases).
         */
        getTopicQualifierResolver(issuingActor, targetActor, reset)
        {
            /* if we don't already have a qualifier resolver, create one */
            if (topicQualResolver_ == nil)
                topicQualResolver_ = createTopicQualifierResolver(
                    issuingActor, targetActor);

            /* reset it if desired */
            if (reset)
                topicQualResolver_.resetResolver();

            /* return it */
            return topicQualResolver_;
        }

        /* the topic qualifier resolver */
        topicQualResolver_ = nil

        /* the resolved topic object list */
        topicList_ = nil

        /* my cached topic resolver */
        topicResolver_ = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An action with a topic phrase as its only object, such as "think about
     *   <topic>".
     *
     *   The grammar rules that produce these actions must set topicMatch to
     *   the topic match tree.
     *
     *   Because we don't have any actual resolved objects, we're based on
     *   IAction.  Subclasses that implement particular topic actions should
     *   override execAction() to carry out the action; this method can call
     *   the getTopic() method of self to get a string giving the topic.
     */
    class TopicAction: TopicActionBase, IAction
        /*
         *   Resolve objects.  We don't actually have any normal objects to
         *   resolve, but we do have to get the resolved topic phrase.
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            /* the topic phrase counts as one noun slot */
            results.noteNounSlots(1);

            /* resolve the topic */
            resolveTopic(issuingActor, targetActor, results);
        }

        /* manually set the resolved objects */
        setResolvedObjects(topic)
        {
            /* set the resolved topic */
            setResolvedTopic(topic);
        }

        /* manually set the pre-resolved match trees */
        setObjectMatches(topic)
        {
            /* note the new topic match tree */
            setTopicMatch(topic);
        }

        /* we have a topic noun phrase */
        predicateNounPhrases = [&topicMatch]
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An Action with a direct object and a topic, such as "ask <actor> about
     *   <topic>".  Topics differ from ordinary noun phrases in scope: rather
     *   than resolving to simulation objects based on location, we resolve
     *   these based on the actor's knowledge.
     *
     *   The grammar rules that produce these actions must set dobjMatch to the
     *   resolvable object of the command (the <actor> in "ask <actor> about
     *   <topic>"), and must set topicMatch to the topic match tree object,
     *   which must be a TopicProd object.  Note that, in some cases, the
     *   phrasing might make the dobjMatch the indirect object, grammatically
     *   speaking: "type <topic> on <object>"; even in such cases, use
     *   dobjMatch for the resolvable object.
     *
     *   When we resolve the topic, we will always resolve it to a single
     *   object of class ResolvedTopic.  This contains the literal tokens of
     *   the original command plus a list of simulation objects matching the
     *   topic name, ordered from best to worst.  This is different from the
     *   way most commands work, since we do not resolve the topic to a simple
     *   game world object.  We keep all of this extra information because we
     *   don't want to perform disambiguation in the normal fashion, but
     *   instead resolve as much as we can with what we're given, and then give
     *   the specialized action code as much information as we can to let the
     *   action code figure out how to respond to the topic.
     */
    class TopicTAction: TopicActionBase, TAction
        /*
         *   reset the action
         */
        resetAction()
        {
            /* reset the inherited state */
            inherited();

            /* forget our topic resolver */
            topicResolver_ = nil;
        }

        /*
         *   resolve our noun phrases to objects
         */
        resolveNouns(issuingActor, targetActor, results)
        {
            local dobjRes;

            /*
             *   the topic phrase counts as one noun slot, and the direct
             *   object as another
             */
            results.noteNounSlots(2);

            /* resolve the direct object, if we have one */
            if (dobjMatch != nil)
            {
                /* presume we won't find an unbound anaphor */
                needAnaphoricBinding_ = nil;

                /*
                 *   if the direct object phrase is an empty phrase, resolve
                 *   the topic first
                 */
                if (dobjMatch.isEmptyPhrase)
                    resolveTopic(issuingActor, targetActor, results);

                /* get the direct object resolver */
                dobjRes = getDobjResolver(issuingActor, targetActor, true);

                /* resolve the direct object */
                dobjList_ = dobjMatch.resolveNouns(dobjRes, results);

                /*
                 *   if that turned up an anaphor (LOOK UP BOOK IN ITSELF),
                 *   resolve the topic phrase as though it were the direct
                 *   object
                 */
                if (needAnaphoricBinding_ && topicMatch != nil)
                    dobjList_ = topicMatch.resolveNouns(dobjRes, results);
            }

            /* resolve the topic */
            resolveTopic(issuingActor, targetActor, results);
        }

        /*
         *   Filter the resolved topic.  This is called by our
         *   TActionTopicResolver, which refers the resolution back to us.
         */
        filterTopic(lst, np, resolver)
        {
            /* by default, simply put everything in an undifferentiated list */
            return new ResolvedTopic(lst, [], [], topicMatch);
        }

        /*
         *   Retry a single-object action as an action taking both an object
         *   and a topic phrase.  We'll treat the original action's direct
         *   object list as our direct object list, and we'll obtain a topic
         *   phrase interactively.
         *
         *   This routine terminates with 'exit' if it doesn't throw some other
         *   error.
         */
        retryWithMissingTopic(orig)
        {
            local action;

            /* create the new action based on the original action */
            action = createForRetry(orig);

            /* use an empty topic phrase for the new action */
            action.topicMatch = new EmptyTopicPhraseProd();

            /* initialize for the missing topic */
            action.initForMissingTopic(orig);

            /* resolve and execute the replacement action */
            resolveAndReplaceAction(action);
        }

        /* initialize a new action we're retrying for a missing direct object */
        initForMissingDobj(orig)
        {
            /*
             *   The original action must have been a TopicAction, so we
             *   already have a topic.  Simply copy the original topic to use
             *   as our own topic.  For example: ASK ABOUT BOOK -> ASK <whom>
             *   ABOUT BOOK.
             */
            topicMatch = new PreResolvedProd(orig.getTopic());
        }

        /* initialize for retrying with a missing topic phrase */
        initForMissingTopic(orig)
        {
            local origDobj = orig.getDobj();

            /*
             *   The original action must have been a TAction, so we already
             *   have a direct object.  Simply copy the original direct object
             *   for use as our own.  For example: ASK BOB -> ASK BOB ABOUT
             *   <what>.
             */
            dobjMatch = new PreResolvedProd(origDobj != nil
                                            ? origDobj : orig.dobjList_);
        }

        /* create our TAction topic resolver */
        createTopicResolver(issuingActor, targetActor)
        {
            return new TActionTopicResolver(
                self, issuingActor, targetActor, topicMatch, whichMessageTopic,
                getTopicQualifierResolver(issuingActor, targetActor, nil));
        }

        /*
         *   In the topic phrase, we can use an anaphoric pronoun to refer
         *   back to the direct object.  Since we resolve the direct object
         *   phrase first, we can simply return the direct object list as the
         *   binding.  If the direct object isn't resolved yet, make a note to
         *   come back and re-bind the anaphor.
         */
        getAnaphoricBinding(typ)
        {
            /* if we've already resolved the direct object phrase, return it */
            if (dobjList_ not in (nil, []))
                return dobjList_;

            /* no dobj yet - make a note that we need to re-bind the anaphor */
            needAnaphoricBinding_ = true;

            /*
             *   return an empty list for now to indicate that the anaphor is
             *   acceptable but has no binding yet
             */
            return [];
        }

        /*
         *   Flag: we have been asked for an anaphoric binding, but we don't
         *   have a binding available.  We'll check this after resolving the
         *   first-resolved noun phrase so that we can go back and re-resolve
         *   it again after resolving the other noun phrase.
         */
        needAnaphoricBinding_ = nil

        /* we have a direct object and a topic phrase */
        predicateNounPhrases = [&dobjMatch, &topicMatch]

        /* get an object role */
        getRoleFromIndex(idx)
        {
            /*
             *   index 2 is the topic object, which is always in the indirect
             *   object role; for others, inherit the default handling
             */
            return (idx == 2 ? IndirectObject : inherited(idx));
        }

        /* get the OtherObject role for the given role */
        getOtherObjectRole(role)
        {
            /* the complementary roles are DirectObject and IndirectObject */
            return (role == DirectObject ? IndirectObject : DirectObject);
        }

        /* get the resolved object in a given role */
        getObjectForRole(role)
        {
            /*
             *   the topic is in the IndirectObject role; inherit the default
             *   for others
             */
            return (role == IndirectObject ? getTopic() : inherited(role));
        }

        /* get the match tree for the given role */
        getMatchForRole(role)
        {
            /*
             *   the topic is in the IndirectObject role; inherit the default
             *   for anything else
             */
            return (role == IndirectObject ? topicMatch : inherited(role));
        }

        /*
         *   Manually set the resolved objects.  We'll set our direct and
         *   indirect objects.
         */
        setResolvedObjects(dobj, topic)
        {
            /* inherit default handling for the direct object */
            inherited(dobj);

            /* set the resolved topic */
            setResolvedTopic(topic);
        }

        /* manually set the pre-resolved match trees */
        setObjectMatches(dobj, topic)
        {
            /* inherit default handling for the direct object */
            inherited(dobj);

            /* note the new topic match tree */
            setTopicMatch(topic);
        }

        /*
         *   Get the list of active objects.  We return only our direct
         *   object, since our topic isn't actually a simulation object.
         */
        getCurrentObjects()
        {
            return [dobjCur_];
        }

        /* set the current objects */
        setCurrentObjects(lst)
        {
            dobjCur_ = lst[1];
            dobjInfoCur_ = nil;
        }

        /* the resolved topic object list */
        topicList_ = nil

        /* my cached topic resolver */
        topicResolver_ = nil

        /* grammatical role played by topic phrase in generated messages */
        whichMessageTopic = nil

        /* -------------------------------------------------------------------- */
        /*
         *   Direct Object Resolver implementation.  We serve as our own
         *   direct object resolver, so we define any special resolver
         *   behavior here.
         */

        /* the true role of the resolved object is always as the direct object */
        whichObject = DirectObject

        /*
         *   What we call our direct object might actually be playing the
         *   grammatical role of the indirect object - in order to inherit
         *   easily from TAction, we call our resolved object our direct
         *   object, regardless of which grammatical role it actually plays.
         *   For the most part it doesn't matter which is which; but for the
         *   purposes of our resolver, we actually do care about its real role.
         *   So, override the resolver method whichMessageObject so that it
         *   returns whichever role is NOT served by the topic object.
         */
        whichMessageObject = (whichMessageTopic == DirectObject
                              ? IndirectObject : DirectObject)
    ;

    /*
     *   "Conversation" TopicTAction.  Many TopicTAction verbs involve
     *   conversation with an actor, who's specified as the direct object: ASK
     *   <actor> ABOUT <topic>, TELL <actor> ABOUT <topic>, ASK <actor> FOR
     *   <topic>.  For these common cases, the most likely default direct
     *   object is the last interlocutor of the actor performing the command -
     *   that is, ASK ABOUT BOOK should by default be directed to whomever we
     *   were speaking to last.
     *
     *   This subclass is suitable for such verbs.  When asked for a default
     *   direct object, we'll check for a current interlocutor, and use it as
     *   the default if available.  If no interlocutor is available, we'll
     *   inherit the standard default handling.
     */
    class ConvTopicTAction: TopicTAction
        getDefaultDobj(np, resolver)
        {
            /*
             *   check to see if the actor has a default interlocutor; if so,
             *   use it as the default actor to be addressed here, otherwise
             *   use the default handling
             */
            local obj = resolver.getTargetActor().getCurrentInterlocutor();
            if (obj != nil)
                return [new ResolveInfo(obj, 0, nil)];
            else
                return inherited(np, resolver);
        }

        /*
         *   Create the topic resolver.  Use a conversational topic resolver
         *   for this type of action.
         */
        createTopicResolver(issuingActor, targetActor)
        {
            return new ConvTopicResolver(
                self, issuingActor, targetActor, topicMatch, whichMessageTopic,
                getTopicQualifierResolver(issuingActor, targetActor, nil));
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   A conversational intransitive action.  This class is for actions such
     *   as Hello, Goodbye, Yes, No - conversational actions that don't involve
     *   any topics or other objects, but whose expression is entirely
     *   contained in the verb.
     */
    class ConvIAction: IAction
        /* this action is conversational */
        isConversational(issuer) { return true; }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Resolved Topic object.  The topic of a TopicTAction always resolves to
     *   one of these objects.
     */
    class ResolvedTopic: object
        construct(inScope, likely, others, prod)
        {
            /*
             *   Remember our lists of objects.  We keep three separate lists,
             *   so that the action knows how we've classified the objects
             *   matching our phrase.  We keep a list of objects that are in
             *   scope; a list of objects that aren't in scope but which the
             *   actor thinks are likely topics; and a list of all of the other
             *   matches.
             *
             *   We keep only the simulation objects in these retained lists -
             *   we don't keep the full ResolveInfo data here.
             */
            inScopeList = inScope.mapAll({x: x.obj_});
            likelyList = likely.mapAll({x: x.obj_});
            otherList = others.mapAll({x: x.obj_});

            /* keep the production match tree */
            topicProd = prod;

            /*
             *   But it might still be interesting to have the ResolveInfo
             *   data, particularly for information on the vocabulary match
             *   strength.  Keep that information in a separate lookup table
             *   indexed by simulation object.
             */
            local rt = new LookupTable();
            others.forEach({x: rt[x.obj_] = x});
            likely.forEach({x: rt[x.obj_] = x});
            inScope.forEach({x: rt[x.obj_] = x});

            /* save the ResolvedInfo lookup table */
            resInfoTab = rt;
        }

        /*
         *   Static method: create a ResolvedTopic to represent an object
         *   that's already been resolved to a game object for the current
         *   action.  'role' is the object role to wrap (DirectObject,
         *   IndirectObject, etc).
         */
        wrapActionObject(role)
        {
            local rt;

            /* create a ResolvedTopic with just the match tree */
            rt = new ResolvedTopic([],  [], [], gAction.getMatchForRole(role));

            /*
             *   add the current resolved object in the role as the one
             *   in-scope match
             */
            rt.inScopeList += gAction.getObjectForRole(role);

            /* return the ResolvedTopic wrapper */
            return rt;
        }

        /*
         *   Static method: create a ResolvedTopic to represent the given
         *   object.
         */
        wrapObject(obj)
        {
            local rt;

            /* create a ResolvedTopic with a PreResolvedProd for the object */
            rt = new ResolvedTopic([], [], [], new PreResolvedProd(obj));

            /* add the given object or objects as the in-scope match list */
            rt.inScopeList += obj;

            /* return the ResolvedTopic wrapper */
            return rt;
        }

        /*
         *   Get the best object match to the topic.  This is a default
         *   implementation that can be changed by game authors or library
         *   extensions to implement different topic-matching strategies.  This
         *   implementation simply picks an object arbitrarily from the
         *   "strongest" of the three lists we build: if there's anything in
         *   the inScopeList, we choose an object from that list; otherwise, if
         *   there's anything in the likelyList, we choose an object from that
         *   list; otherwise we choose an object from the otherList.
         */
        getBestMatch()
        {
            /* if there's an in-scope match, pick one */
            if (inScopeList.length() != 0)
                return inScopeList[1];

            /* nothing's in scope, so try to pick a likely match */
            if (likelyList.length() != 0)
                return likelyList[1];

            /*
             *   there's not even anything likely, so pick an "other", if
             *   there's anything in that list
             */
            if (otherList.length() != 0)
                return otherList[1];

            /* there are no matches at all - return nil */
            return nil;
        }

        /*
         *   Is the given object among the possible matches for the topic?
         */
        canMatchObject(obj)
        {
            return inScopeList.indexOf(obj) != nil
                || likelyList.indexOf(obj) != nil
                || otherList.indexOf(obj) != nil;
        }

        /* get the original text of the topic phrase */
        getTopicText() { return topicProd.getOrigText(); }

        /*
         *   Are we allowed to match the topic text literally, for parsing
         *   purposes?  If this is true, it means that we can match the literal
         *   text the player entered against strings, regular expressions,
         *   etc.; for example, we can match a TopicMatchTopic's matchPattern
         *   regular expression.  If this is nil, it means that we can only
         *   interpret the meaning of the resolved topic by looking at the
         *   various topic match lists (inScopeList, likelyList, otherList).
         *
         *   By default, we simply return true.  Note that the base library
         *   never has any reason of its own to disallow literal matching of
         *   topic text; this property is purely for the use of language
         *   modules, to handle language-specific input that parses at a high
         *   level as a topic phrase but which has some idiomatic or
         *   grammatical function that makes it in appropriate to try to
         *   extract the meaning of the resolved topic from the literal text of
         *   the topic phrase in isolation.  This case doesn't seem to arise in
         *   English, but does occur in other languages: Michel Nizette cites
         *   "parlez-en a Bob" as an example in French, where "en" is
         *   essentially a particle modifying the verb, not a full-fledged
         *   phrase that we can interpret separately as a topic.
         */
        canMatchLiterally = true

        /*
         *   get the original tokens of the topic phrase, in canonical
         *   tokenizer format
         */
        getTopicTokens() { return topicProd.getOrigTokenList(); }

        /*
         *   get the original word strings of the topic phrase - this is
         *   simply a list of the original word strings (in their original
         *   case), without any of the extra information of the more
         *   complicated canonical tokenizer format
         */
        getTopicWords()
        {
            /* return just the original text strings from the token list */
            return topicProd.getOrigTokenList().mapAll({x: getTokOrig(x)});
        }

        /*
         *   Get the parser ResolveInfo object for a given matched object.
         *   This recovers the ResolveInfo describing the parsing result for
         *   any object in the resolved object lists (inScopeList, etc).
         */
        getResolveInfo(obj) { return resInfoTab[obj]; }

        /*
         *   Our lists of resolved objects matching the topic phrase,
         *   separated by classification.
         */
        inScopeList = nil
        likelyList = nil
        otherList = nil

        /*
         *   The production match tree object that matched the topic phrase in
         *   the command.  This can be used to obtain the original tokens of
         *   the command or the original text of the phrase.
         */
        topicProd = nil

        /*
         *   ResolveInfo table for the resolved objects.  This is a lookup
         *   table indexed by simulation object.  Each entry in the resolved
         *   object lists (inScopeList, etc) has have a key in this table, with
         *   the ResolveInfo object as the value for the key.  This can be used
         *   to recover the ResolveInfo object describing the parser results
         *   for this object.
         */
        resInfoTab = nil
    ;

    /*
     *   A special topic for "nothing."  It's occasionally useful to be able
     *   to construct a TopicTAction with an empty topic phrase.  For the
     *   topic phrase to be well-formed, we need a valid ResolvedTopic object,
     *   even though it won't refer to anything.
     */
    resolvedTopicNothing: ResolvedTopic
        inScopeList = []
        likelyList = []
        otherList = []

        getTopicText() { return ''; }
        getTopicTokens() { return []; }
        getTopicWords() { return []; }
    ;

    /*
     *   Topic Resolver
     */
    class TopicResolver: Resolver
        construct(action, issuingActor, targetActor, prod, which,
                  qualifierResolver)
        {
            /* inherit the base class constructor */
            inherited(action, issuingActor, targetActor);

            /* the topic phrase doesn't have a true resolved role */
            whichObject = nil;

            /* remember the grammatical role of our object in the command */
            whichMessageObject = which;

            /* remember our topic match tree */
            topicProd = prod;

            /* remember the resolver for qualifier phrases */
            qualifierResolver_ = qualifierResolver;
        }

        resetResolver()
        {
            /* inherit the default handling */
            inherited();

            /* reset our qualifier resolver as well */
            qualifierResolver_.resetResolver();
        }

        /*
         *   package a resolved topic list - if it's not already represented as
         *   a ResolvedTopic object, we'll apply that wrapping
         */
        packageTopicList(lst, match)
        {
            /*
             *   if the topic is other than a single ResolvedTopic object, run
             *   it through the topic resolver's ambiguous noun phrase filter
             *   to get it into that canonical form
             */
            if (lst != nil
                && (lst.length() > 1
                    || (lst.length() == 1 && !lst[1].obj_.ofKind(ResolvedTopic))))
            {
                /* it's not in canonical form, so get it in canonical form now */
                lst = filterAmbiguousNounPhrase(lst, 1, match);
            }

            /* return the result */
            return lst;
        }

        /* our qualifier resolver */
        qualifierResolver_ = nil

        /* get our qualifier resolver */
        getQualifierResolver() { return qualifierResolver_; }
        getPossessiveResolver() { return qualifierResolver_; }

        /*
         *   Determine if the object is in scope.  We consider any vocabulary
         *   match to be in scope for the purposes of a topic phrase, since the
         *   subject matter of topics is mere references to things, not the
         *   things themselves; we can, for example, ASK ABOUT things that
         *   aren't physically present, or even about completely abstract
         *   ideas.
         */
        objInScope(obj) { return true; }

        /*
         *   our scope is global, because we don't limit the scope to the
         *   physical senses
         */
        isGlobalScope = true

        /*
         *   Determine if an object is in physical scope.  We'll accept
         *   anything that's in physical scope, and we'll also accept any topic
         *   object that the actor knows about.
         *
         *   Note that this isn't part of the basic Resolver interface.  It's
         *   instead provided as a service routine for our subclasses, so that
         *   they can easily determine the physical scope of an object if
         *   needed.
         */
        objInPhysicalScope(obj)
        {
            /*
             *   it's in physical scope, or if the actor knows about it as a
             *   topic, it's in scope
             */
            return (scope_.indexOf(obj) != nil);
        }

        /*
         *   Filter an ambiguous noun phrase list using the strength of
         *   possessive qualification, if any.  For a topic phrase, we want to
         *   keep all of the possibilities.
         */
        filterPossRank(lst, num)
        {
            return lst;
        }

        /*
         *   Filter an ambiguous noun list.
         *
         *   It is almost always undesirable from a user interface perspective
         *   to ask for help disambiguating a topic phrase.  In the first
         *   place, since all topics tend to be in scope all the time, we
         *   might reveal too much about the inner model of the story if we
         *   were to enumerate all of the topic matches to a phrase.  In the
         *   second place, topics are used in conversational contexts, so it
         *   almost never make sense for the parser to ask for clarification -
         *   the other member of the conversation might ask, but not the
         *   parser.  So, we'll always filter the list to the required number,
         *   even if it means we choose arbitrarily.
         *
         *   As a first cut, we prefer objects that are physically in scope to
         *   those not in scope: if the player is standing next to a control
         *   panel and types "ask bob about control panel," it makes little
         *   sense to consider any other control panels in the simulation.
         *
         *   As a second cut, we'll ask the actor to filter the list.  Games
         *   that keep track of the actor's knowledge can use this to filter
         *   according to topics the actor is likely to know about.
         */
        filterAmbiguousNounPhrase(lst, requiredNum, np)
        {
            /* ask the action to create the ResolvedTopic */
            local rt = resolveTopic(lst, requiredNum, np);

            /* wrap the ResolvedTopic in the usual ResolveInfo list */
            return [new ResolveInfo(rt, 0, np)];
        }

        /*
         *   Resolve the topic phrase.  This returns a ResolvedTopic object
         *   encapsulating the resolution of the phrase.
         *
         *   This default base class implementation simply creates a resolved
         *   topic list with the whole set of possible matches
         *   undifferentiated.  Subclasses for specialized actions might want
         *   to differentiate the items in the list, based on things like the
         *   actor's knowledge so far or what's in physical scope.
         */
        resolveTopic(lst, requiredNum, np)
        {
            /* return a ResolvedTopic that lumps everything in the main list */
            return new ResolvedTopic(lst, [], [], topicProd);
        }

        /*
         *   Resolve an unknown phrase.  We allow unknown words to be used in
         *   topics; we simply return a ResolvedTopic that doesn't refer to
         *   any simulation objects at all.
         */
        resolveUnknownNounPhrase(tokList)
        {
            /*
             *   Create our ResolvedTopic object for the results.  We have
             *   words we don't know, so we're not referring to any objects,
             *   so our underlying simulation object list is empty.
             */
            local rt = new ResolvedTopic([], [], [], topicProd);

            /* return a resolved topic object with the empty list */
            return [new ResolveInfo(rt, 0, new TokenListProd(tokList))];
        }

        /* filter a plural */
        filterPluralPhrase(lst, np)
        {
            /*
             *   Handle this the same way we handle an ambiguous noun phrase,
             *   so that we yield only one object.  Topics are inherently
             *   singular; we'll allow asking about a grammatically plural
             *   term, but we'll turn it into a single topic result.
             */
            return filterAmbiguousNounPhrase(lst, 1, np);
        }

        /* get a default object */
        getDefaultObject(np)
        {
            /* there is never a default for a topic */
            return nil;
        }

        /* it's fine not to match a topic phrase */
        noVocabMatch(action, txt) { }
        noMatch(action, txt) { }
        noMatchPoss(action, txt) { }

        /* we don't allow ALL or provide defaults */
        getAll(np) { return []; }
        getAllDefaults() { return []; }

        /* the production match tree for the topic phrase we're resolving */
        topicProd = nil
    ;

    /*
     *   A topic resolver specialized for TopicTActions - actions involving a
     *   topic and a physical object, such as CONSULT ABOUT.  For these topics,
     *   we'll let the action handle the resolution.
     */
    class TActionTopicResolver: TopicResolver
        resolveTopic(lst, requiredNum, np)
        {
            /* let the action handle it */
            return action_.filterTopic(lst, np, self);
        }
    ;

    /*
     *   A topic resolver specialized for conversational actions (ASK ABOUT,
     *   TELL ABOUT, etc).  When we resolve the topic, we'll differentiate the
     *   resolution to differentiate based on the knowledge of the actor who's
     *   performing the command.
     */
    class ConvTopicResolver: TopicResolver
        /*
         *   Resolve the topic phrase.  We'll break up the vocabulary matches
         *   into three sublists: the objects that are either in physical scope
         *   or known to the actor performing the command; objects that the
         *   actor considers likely topics; and everything else.
         */
        resolveTopic(lst, requiredNum, np)
        {
            local inScope;
            local actorPrefs;

            /*
             *   First, get the subset of items that are in conversational
             *   scope - we'll consider this the best set of matches.
             */
            inScope = lst.subset({x: objInConvScope(x.obj_)});

            /*
             *   eliminate the in-scope items from the list, so we can
             *   consider only what remains
             */
            lst -= inScope;

            /*
             *   ask the actor to pick out the most likely set of topics from
             *   the ones that remain
             */
            actorPrefs = lst.subset({x: actor_.isLikelyTopic(x.obj_)});

            /* eliminate those items from the list */
            lst -= actorPrefs;

            /* create our ResolvedTopic object and return it */
            return new ResolvedTopic(inScope, actorPrefs, lst, topicProd);
        }

        /*
         *   Determine if an object is in "conversational" scope - this returns
         *   true if the object is in physical scope or it's known to the actor
         *   performing the command.
         */
        objInConvScope(obj)
        {
            /*
             *   if it's in physical scope, or the actor knows about it, it's
             *   in conversation scope
             */
            return (objInPhysicalScope(obj) || actor_.knowsTopic(obj));
        }
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   System action.  These actions are for out-of-game meta-verbs (save,
     *   restore, undo).  These verbs take no objects, must be performed by
     *   the player (thus by the player character, not an NPC), and consume no
     *   game clock time.
     */
    class SystemAction: IAction
        /* execute the action */
        execAction()
        {
            /*
             *   Conceptually, system actions are performed by the player
             *   directly, not by any character in the game (not even the
             *   player character).  However, we don't distinguish in the
             *   command-entry user interface between a command the player is
             *   performing and a command to the player character, hence we
             *   can merely ensure that the command is not directed to a
             *   non-player character.
             */
            if (!gActor.isPlayerChar)
            {
                gLibMessages.systemActionToNPC();
                exit;
            }

            /*
             *   system actions sometimes need to prompt for interactive
             *   responses, so deactivate the report list - this will allow
             *   interactive prompts to be shown immediately, not treated as
             *   reports to be deferred until the command is finished
             */
            gTranscript.showReports(true);
            gTranscript.clearReports();

            /* perform our specific action */
            execSystemAction();

            /* re-activate the transcript for the next command */
            gTranscript.activate();
        }

        /* each subclass must override this to perform its actual action */
        execSystemAction() { }

        /*
         *   Ask for an input file.  We call the input manager, which freezes
         *   the real-time clock, displays the appropriate local file selector
         *   dialog, and restarts the clock.
         */
        getInputFile(prompt, dialogType, fileType, flags)
        {
            return inputManager.getInputFile(prompt, dialogType, fileType, flags);
        }

        /* system actions consume no game time */
        actionTime = 0
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   An exception class for remappings that we can't handle.
     */
    class ActionRemappingTooComplexError: Exception
    ;

TADS 3 Library Manual
Generated on 8/21/2012 from TADS version 3.1.2