# exec.t | documentation

    #charset "us-ascii"

    /*
     *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
     *
     *   TADS 3 Library: command execution
     *
     *   This module defines functions that perform command execution.
     */

    #include "adv3.h"

    /* ------------------------------------------------------------------------ */
    /*
     *   Execute a command line, as issued by the given actor and as given as a
     *   list of tokens.
     *
     *   If 'firstInSentence' is true, we're at the start of a "sentence."  The
     *   meaning and effect of this may vary by language.  In English, a
     *   sentence ends with certain punctuation marks (a period, semicolon,
     *   exclamation mark, or question mark), so anything after one of these
     *   punctuation marks is the start of a new sentence.  Also in English, we
     *   can address a command to an explicit target actor using the "actor,"
     *   prefix syntax, which we can't use except at the start of a sentence.
     *
     *   If the command line consists of multiple commands, we will only
     *   actually execute the first command before returning.  We'll schedule
     *   any additional commands for later execution by putting them into the
     *   target actor's pending command queue before we return, but we won't
     *   actually execute them.
     */
    executeCommand(targetActor, issuingActor, toks, firstInSentence)
    {
        local actorPhrase;
        local actorSpecified;

        /*
         *   Turn on sense caching while we're working, until execution
         *   begins.  The parsing and resolution phases of command processing
         *   don't involve any changes to game state, so we can safely cache
         *   sense information; caching sense information during these phases
         *   is desirable because these steps (noun resolution in particular)
         *   involve repeated inspection of the current sensory environment,
         *   which can require expensive calculations.
         */
        libGlobal.enableSenseCache();

        /* we don't have an explicit actor phrase yet */
        actorPhrase = nil;

        /* presume an actor will not be specified */
        actorSpecified = nil;

        /*
         *   If this is the start of a new sentence, and the issuing actor
         *   wants to cancel any target actor designation at the end of each
         *   sentence, change the target actor back to the issuing actor.
         */
        if (firstInSentence
            && issuingActor != targetActor
            && issuingActor.revertTargetActorAtEndOfSentence)
        {
            /* switch to the target actor */
            targetActor = issuingActor;

            /* switch to the issuer's sense context */
            senseContext.setSenseContext(targetActor, sight);
        }

        /*
         *   Keep going until we've processed the command.  This might take
         *   several iterations, because we might have replacement commands to
         *   execute.
         */
    parseTokenLoop:
        for (;;)
        {
            local lst;
            local action;
            local match;
            local nextIdx;
            local nextCommandTokens;
            local extraIdx;
            local extraTokens;

            /*
             *   Catch any errors that occur while executing the command,
             *   since some of them are signals to us that we should reparse
             *   some new text read or generated deep down inside the command
             *   processing.
             */
            try
            {
                local rankings;

                /* we have no extra tokens yet */
                extraTokens = [];

                /*
                 *   Parse the token list.  If this is the first command on
                 *   the command line, allow an actor prefix.  Otherwise, just
                 *   look for a command.
                 */
                lst = (firstInSentence ? firstCommandPhrase : commandPhrase)
                      .parseTokens(toks, cmdDict);

                /*
                 *   As a first cut at reducing the list of possible matches
                 *   to those that make sense, eliminate from this list any
                 *   matches which do not have valid actions.  In grammars for
                 *   "scrambling" languages (i.e., languages with flexible
                 *   word ordering), it is possible to construct commands that
                 *   fit the grammatical rules of sentence construction but
                 *   which make no sense because of the specific constraints
                 *   of the verbs involved; we can filter out such nonsense
                 *   interpretations immediately by keeping only those
                 *   structural interpretations that can resolve to valid
                 *   actions.
                 */
                lst = lst.subset(
                    {x: x.resolveFirstAction(issuingActor, targetActor) != nil});

                /* if we have no matches, the command isn't understood */
                if (lst.length() == 0)
                {
                    /*
                     *   If this is a first-in-sentence phrase, try looking for
                     *   a target actor phrase.  If we can find one, we can
                     *   direct the 'oops' to that actor, to allow the game to
                     *   customize messages more specifically.
                     */
                    if (firstInSentence)
                    {
                        local i;

                        /* try parsing an "actor, <unknown>" phrase */
                        lst = actorBadCommandPhrase.parseTokens(toks, cmdDict);

                        /* if we got any matches, try to resolve actors */
                        lst = lst.mapAll({x: x.resolveNouns(
                            issuingActor, issuingActor,
                            new TryAsActorResolveResults())});

                        /* drop any that didn't yield any results */
                        lst = lst.subset({x: x != nil && x.length() != 0});

                        /*
                         *
                         *   if anything's left, and one of the entries
                         *   resolves to an actor, arbitrarily pick the
                         *   first such entry use the resolved actor as the
                         *   new target actor
                         */
                        if (lst.length() != 0
                            && (i = lst.indexWhich(
                                {x: x[1].obj_.ofKind(Actor)})) != nil)
                            targetActor = lst[i][1].obj_;
                    }

                    /*
                     *   We don't understand the command.  Check for unknown
                     *   words - if we have any, give them a chance to use
                     *   OOPS to correct a typo.
                     */
                    tryOops(toks, issuingActor, targetActor,
                            1, toks, rmcCommand);

                    /*
                     *   try running it by the SpecialTopic history to see if
                     *   they're trying to use a special topic in the wrong
                     *   context - if so, explain that they can't use the
                     *   command right now, rather than claiming that the
                     *   command is completely invalid
                     */
                    if (specialTopicHistory.checkHistory(toks))
                    {
                        /* the special command is not currently available */
                        targetActor.notifyParseFailure(
                            issuingActor, &specialTopicInactive, []);
                    }
                    else
                    {
                        /* tell the issuer we didn't understand the command */
                        targetActor.notifyParseFailure(
                            issuingActor, &commandNotUnderstood, []);
                    }

                    /*
                     *   we're done with this command, and we want to abort
                     *   any subsequent commands on the command line
                     */
                    return;
                }

                /* show the matches if we're in debug mode */
                dbgShowGrammarList(lst);

                /*
                 *   Perform a tentative resolution on each alternative
                 *   structural interpretation of the command, and rank the
                 *   interpretations in order of "goodness" as determined by
                 *   our ranking criteria encoded in CommandRanking.
                 *
                 *   Note that we perform the tentative resolution and ranking
                 *   even if we have only one interpretation, because the
                 *   tentative resolution is often useful for the final
                 *   resolution pass.
                 */
                rankings = CommandRanking
                           .sortByRanking(lst, issuingActor, targetActor);

                /*
                 *   Take the interpretation that came up best in the rankings
                 *   (or, if we have multiple at the same ranking, arbitrarily
                 *   pick the one that happened to come up first in the list)
                 *   - they're ranked in descending order of goodness, so take
                 *   the first one.
                 */
                match = rankings[1].match;

                /* if we're in debug mode, show the winner */
                dbgShowGrammarWithCaption('Winner', match);

                /*
                 *   Get the token list for the rest of the command after what
                 *   we've parsed so far.
                 *
                 *   Note that we'll start over and parse these tokens anew,
                 *   even though we might have parsed them already into a
                 *   subcommand on the previous iteration.  Even if we already
                 *   parsed these tokens, we want to parse them again, because
                 *   we did not have a suitable context for evaluating the
                 *   semantic strengths of the possible structural
                 *   interpretations this command on the previous iteration;
                 *   for example, it was not possible to resolve noun phrases
                 *   in this command because we could not guess what the scope
                 *   would be when we got to this point.  So, we'll simply
                 *   discard the previous match tree, and start over, treating
                 *   the new tokens as a brand new command.
                 */
                nextIdx = match.getNextCommandIndex();
                nextCommandTokens = toks.sublist(nextIdx);

                /* if the pending command list is empty, make it nil */
                if (nextCommandTokens.length() == 0)
                    nextCommandTokens = nil;

                /*
                 *   Get the part of the token list that the match doesn't use
                 *   at all.  These are the tokens following the tokens we
                 *   matched.
                 */
                extraIdx = match.tokenList.length() + 1;
                extraTokens = toks.sublist(extraIdx);

                /*
                 *   We now have the best match for the command tree.
                 *
                 *   If the command has an actor clause, resolve the actor, so
                 *   that we can direct the command to that actor.  If the
                 *   command has no actor clause, the command is to the actor
                 *   who issued the command.
                 *
                 *   Do NOT process the actor clause if we've already done so.
                 *   If we edit the token list and retry the command after
                 *   this point, there is no need to resolve the actor part
                 *   again.  Doing so could be bad - if resolving the actor
                 *   required player interaction, such as asking for help with
                 *   an ambiguous noun phrase, we do not want to go through
                 *   the same interaction again just because we have to edit
                 *   and retry a later part of the command.
                 */
                if (match.hasTargetActor())
                {
                    local actorResults;

                    /*
                     *   If we haven't yet explicitly specified a target
                     *   actor, and the default target actor is different from
                     *   the issuing actor, then this is really a new command
                     *   from the issuing actor.
                     *
                     *   First, an actor change mid-command is allowed only if
                     *   the issuing actor waits for NPC commands to be
                     *   carried out; if not, then we can't change the actor
                     *   mid-command.
                     *
                     *   Second, since the command comes from the issuing
                     *   actor, not from the default target actor, we want to
                     *   issue the orders on the issuing actor's turn, so put
                     *   the command into the queue for the issuer, and let
                     *   the issuer re-parse the command on the issuer's next
                     *   turn.
                     */
                    if (!actorSpecified && issuingActor != targetActor)
                    {
                        /*
                         *   don't allow the command if the issuing actor
                         *   doesn't wait for orders to be completed
                         */
                        if (!issuingActor.issueCommandsSynchronously)
                        {
                            /* turn off any sense capturing */
                            senseContext.setSenseContext(nil, sight);

                            /* show the error */
                            issuingActor.getParserMessageObj()
                                .cannotChangeActor();

                            /* done */
                            return;
                        }

                        /* put the command into the issuer's queue */
                        issuingActor.addFirstPendingCommand(
                            firstInSentence, issuingActor, toks);

                        /* done */
                        return;
                    }

                    /* create an actor-specialized results object */
                    actorResults = new ActorResolveResults();

                    /* set up the actors in the results object */
                    actorResults.setActors(targetActor, issuingActor);

                    /* resolve the actor object */
                    match.resolveNouns(issuingActor, targetActor, actorResults);

                    /* get the target actor from the command */
                    targetActor = match.getTargetActor();

                    /* pull out the phrase specifying the actor */
                    actorPhrase = match.getActorPhrase();

                    /*
                     *   Copy antecedents from the issuing actor to the target
                     *   actor.  Since the issuer is giving us a new command
                     *   here, pronouns will be given from the issuer's
                     *   perspective.
                     */
                    targetActor.copyPronounAntecedentsFrom(issuingActor);

                    /* let the actor phrase know we're actually using it */
                    match.execActorPhrase(issuingActor);

                    /*
                     *   Ask the target actor if it's interested in the
                     *   command at all.  This only applies when the actor was
                     *   actually specified - if an actor wasn't specified,
                     *   the command is either directed to the issuer itself,
                     *   in which case the command will always be accepted; or
                     *   the command is a continuation of a command line
                     *   previously accepted.
                     */
                    if (!targetActor.acceptCommand(issuingActor))
                    {
                        /*
                         *   the command was immediately rejected - abandon
                         *   the command and any subsequent commands on the
                         *   same line
                         */
                        return;
                    }

                    /* note that an actor was specified */
                    actorSpecified = true;

                    /*
                     *   Pull out the rest of the command (other than the
                     *   target actor specification) and start over with a
                     *   fresh parse of the whole thing.  We must do this
                     *   because our tentative resolution pass that we used to
                     *   pick the best structural interpretation of the
                     *   command couldn't go far enough - since we didn't know
                     *   the actor involved, we weren't able to resolve nouns
                     *   in the rest of the command.  Now that we know the
                     *   actor, we can start over and resolve everything in
                     *   the rest of the command, and thus choose the right
                     *   structural match for the command.
                     */
                    toks = match.getCommandTokens();

                    /* what follows obviously isn't first in the sentence */
                    firstInSentence = nil;

                    /* go back to parse the rest */
                    continue parseTokenLoop;
                }

                /* pull out the first action from the command */
                action = match.resolveFirstAction(issuingActor, targetActor);

                /*
                 *   If the winning interpretation had any unknown words, run
                 *   a resolution pass to resolve those interactively, if
                 *   possible.  We want to do this before doing any other
                 *   interactive resolution because OOPS has the unique
                 *   property of forcing us to reparse the command; if we
                 *   allowed any other interactive resolution to happen before
                 *   processing an OOPS, we'd likely have to repeat the other
                 *   resolution on the reparse, which would confuse and
                 *   irritate users by asking the same question more than once
                 *   for what is apparently the same command.
                 */
                if (rankings[1].unknownWordCount != 0)
                {
                    /*
                     *   resolve using the OOPS results gatherer - this runs
                     *   essentially the same preliminary resolution process
                     *   as the ranking results gatherer, but does perform
                     *   interactive resolution of unknown words via OOPS
                     */
                    match.resolveNouns(
                        issuingActor, targetActor,
                        new OopsResults(issuingActor, targetActor));
                }

                /*
                 *   If the command is directed to a different actor than the
                 *   issuer, change to the target actor's sense context.
                 */
                if (actorSpecified && targetActor != issuingActor)
                    senseContext.setSenseContext(targetActor, sight);

                /* set up a transcript to receive the command results */
                withCommandTranscript(CommandTranscript, function()
                {
                    /*
                     *   Execute the action.
                     *
                     *   If a target actor was specified, and it's not the same
                     *   as the issuing actor, this counts as a turn for the
                     *   issuing actor.
                     */
                    executeAction(targetActor, actorPhrase, issuingActor,
                                  actorSpecified && issuingActor != targetActor,
                                  action);
                });

                /*
                 *   If we have anything remaining on the command line, insert
                 *   the remaining commands at the start of the target actor's
                 *   command queue, since we want the target actor to continue
                 *   with the rest of this command line as its next operation.
                 *
                 *   Since the remainder of the line represents part of the
                 *   work the actor pulled out of the queue in order to call
                 *   us, put the remainder back in at the START of the actor's
                 *   queue - it was before anything else that might be in the
                 *   actor's queue before, so it should stay ahead of anything
                 *   else now.
                 */
                if (nextCommandTokens != nil)
                {
                    /*
                     *   Prepend the remaining commands to the actor's queue.
                     *   The next command is the start of a new sentence if
                     *   the command we just executed ends the sentence.
                     */
                    targetActor.addFirstPendingCommand(
                        match.isEndOfSentence(), issuingActor, nextCommandTokens);
                }

                /*
                 *   If the command was directed from the issuer to a
                 *   different target actor, and the issuer wants to wait for
                 *   the full set of issued commands to complete before
                 *   getting another turn, tell the issuer to begin waiting.
                 */
                if (actorSpecified && issuingActor != targetActor)
                    issuingActor.waitForIssuedCommand(targetActor);

                /* we're done */
                return;
            }
            catch (ParseFailureException rfExc)
            {
                /*
                 *   Parsing failed in such a way that we cannot proceed.
                 *   Tell the target actor to notify the issuing actor.
                 */
                rfExc.notifyActor(targetActor, issuingActor);

                /*
                 *   the command cannot proceed, so abandon any remaining
                 *   tokens on the command line
                 */
                return;
            }
            catch (CancelCommandLineException cclExc)
            {
                /*
                 *   if there are any tokens remaining, the game might want to
                 *   show an explanation
                 */
                if (nextCommandTokens != nil)
                    targetActor.getParserMessageObj().explainCancelCommandLine();

                /* stop now, abandoning the rest of the command line */
                return;
            }
            catch (TerminateCommandException tcExc)
            {
                /*
                 *   the command cannot proceed - we can't do any more
                 *   processing of this command, so simply return, abandoning
                 *   any additional tokens we have
                 */
                return;
            }
            catch (RetryCommandTokensException rctExc)
            {
                /*
                 *   We want to replace the current command's token list with
                 *   the new token list - get the new list, and then go back
                 *   and start over processing it.
                 *
                 *   Note that we must retain any tokens beyond those that the
                 *   match tree used.  The exception only edits the current
                 *   match tree's matched tokens, since it doesn't have access
                 *   to any of the original tokens beyond those, so we must
                 *   now add back in any tokens beyond the originals.
                 */
                toks = rctExc.newTokens_ + extraTokens;

                /* go back and process the command again with the new tokens */
                continue parseTokenLoop;
            }
            catch (ReplacementCommandStringException rcsExc)
            {
                local str;

                /* retrieve the new command string from the exception */
                str = rcsExc.newCommand_;

                /*
                 *   if the command string is nil, it means that the command
                 *   has been fully handled already, so we simply return
                 *   without any further work
                 */
                if (str == nil)
                    return;

                /*
                 *   Replace the entire command string with the one from the
                 *   exception - this cancels any previous command that we
                 *   had.
                 */
                toks = cmdTokenizer.tokenize(str);

                /*
                 *   we have a brand new command line, so we're starting a
                 *   brand new sentence
                 */
                firstInSentence = true;

                /* set the issuing and target actor according to the exception */
                issuingActor = rcsExc.issuingActor_;
                targetActor = rcsExc.targetActor_;

                /*
                 *   Put this work into the target actor's work queue, so that
                 *   the issuer will carry out the command at the next
                 *   opportunity.  This is a brand new command line, so it
                 *   starts a new sentence.
                 */
                targetActor.addPendingCommand(true, issuingActor, toks);

                /* we're done processing this command */
                return;
            }
        }
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   GlobalRemapping makes it possible to transform one action into another
     *   globally - as opposed to the remapTo mechanism, which lets an object
     *   involved in the command perform a remapping.  The key difference
     *   between global remappings and remapTo is that the latter can't happen
     *   until after the objects are resolved (for fairly obvious reasons: each
     *   remapTo mapping is associated with an object, so you can't know which
     *   mapping to apply until you know which object is involved).  In
     *   contrast, global remappings are performed *before* object resolution -
     *   this is possible because the mappings don't depend on the objects
     *   involved in the action.
     *
     *   Whenever an action is about to be executed, the parser runs through
     *   all of the defined global remappings, and gives each one a chance to
     *   remap the command.  If any remapping succeeds, we replace the original
     *   command with the remapped version, then repeat the scan of the global
     *   remapping list from the start - we do another complete scan of the
     *   list in case there's another global mapping that applies to the
     *   remapped version of the command.  We repeat this process until we make
     *   it through the whole list without finding a remapping.
     *
     *   GlobalRemapping instances are added to the master list of mappings
     *   automatically at pre-init time, and any time you construct one
     *   dynamically with 'new'.
     */
    class GlobalRemapping: PreinitObject
        /*
         *   Check for and apply a remapping.  This method must be implemented
         *   in each GlobalRemapping instance to perform the actual remapping
         *   work.
         *
         *   This routine should first check to see if the command is relevant
         *   to this remapping.  In most cases, this means checking that the
         *   command matches some template, such as having a particular action
         *   (verb) and combination of potential objects.  Note that the
         *   objects aren't fully resolved during global remapping - the whole
         *   point of global remapping is to catch certain phrasings before we
         *   get to the noun resolution phase - but the *phrases* involved will
         *   be known, so the range of potential matches is knowable.
         *
         *   If the routine decides that the action isn't relevant to this
         *   remapping, it should simply return nil.
         *
         *   If the action decides to remap the action, it must create a new
         *   Action object representing the replacement version of the command.
         *   Then, return a list, [targetActor, action], giving the new target
         *   actor and the new action.  You don't have to change the target
         *   actor, of course, but it's included in the result so that you can
         *   change it if you want to.  For example, you could use this to
         *   remap a command of the form "X, GIVE ME Y" to "ME, ASK X FOR Y" -
         *   note that the target actor changes from X to ME.
         */
        getRemapping(issuingActor, targetActor, action)
        {
            /*
             *   this must be overridden to perform the actual remapping; by
             *   default, simply return nil to indicate that we don't want to
             *   remap this action
             */
            return nil;
        }

        /*
         *   Remapping order - the parser applies global remappings in
         *   ascending order of this value.  In most cases, the order shouldn't
         *   matter, since most remappings should be narrow enough that a given
         *   command will only be subject to one remapping rule.  However, in
         *   some cases you might need to define rules that overlap, so the
         *   ordering lets you specify which one goes first.  In most cases
         *   you'll want to apply the more specific rule first.
         */
        remappingOrder = 100

        /*
         *   Static class method: look for a remapping.  This runs through the
         *   master list of mappings, looking for a mapping that applies to the
         *   given command.  If we find one, we'll replace the command with the
         *   remapped version, then start over with a fresh scan of the entire
         *   list to see if there's a remapping for the *new* version of the
         *   command.  We repeat this until we get through the whole list
         *   without finding a remapping.
         *
         *   The return value is a list, [targetActor, action], giving the
         *   resulting target actor and new action object.  If we don't find
         *   any remapping, this will simply be the original values passed in
         *   as our arguments; if we do find a remapping, this will be the new
         *   version of the command.
         */
        findGlobalRemapping(issuingActor, targetActor, action)
        {
            /* get the global remapping list */
            local v = GlobalRemapping.allGlobalRemappings;
            local cnt = v.length();

            /* if necessary, sort the list */
            if (GlobalRemapping.listNeedsSorting)
            {
                /* sort it by ascending remappingOrder value */
                v.sort(SortAsc, {a, b: a.remappingOrder - b.remappingOrder});

                /* note that it's now sorted */
                GlobalRemapping.listNeedsSorting = nil;
            }

            /*
             *   iterate through the list repeatedly, until we make it all the
             *   way through without finding a mapping
             */
            for (local done = nil ; !done ; )
            {
                /* presume we won't find a remapping on this iteration */
                done = true;

                /* run through the list, looking for a remapping */
                for (local i = 1 ; i <= cnt ; ++i)
                {
                    local rm;

                    /* check for a remapping */
                    rm = v[i].getRemapping(issuingActor, targetActor, action);
                    if (rm != nil)
                    {
                        /* found a remapping - apply it */
                        targetActor = rm[1];
                        action = rm[2];

                        /*
                         *   we found a remapping, so we have to repeat the
                         *   scan of the whole list - note that we're not done,
                         *   and break out of the current scan so that we start
                         *   over with a fresh scan
                         */
                        done = nil;
                        break;
                    }
                }
            }

            /* return the final version of the command */
            return [targetActor, action];
        }

        /* pre-initialization: add each instance to the master list */
        execute()
        {
            /* add me to the master list */
            registerGlobalRemapping();
        }

        /* construction: add myself to the master list */
        construct()
        {
            /* add me to the master list */
            registerGlobalRemapping();
        }

        /* register myself with the global list, making this an active mapping */
        registerGlobalRemapping()
        {
            /* add myself to the global list */
            GlobalRemapping.allGlobalRemappings.append(self);

            /* note that a sort is required the next time we run */
            GlobalRemapping.listNeedsSorting = true;
        }

        /*
         *   unregister - this removes me from the global list, making this
         *   mapping inactive: after being unregistered, the parser won't apply
         *   this mapping to new commands
         */
        unregisterGlobalRemapping()
        {
            GlobalRemapping.allGlobalRemappings.removeElement(self);
        }

        /*
         *   Static class property: the master list of remappings.  We build
         *   this automatically at preinit time, and manipulate it via our
         *   constructor.
         */
        allGlobalRemappings = static new Vector(10)

        /*
         *   static class property: the master list needs to be sorted; this is
         *   set to true each time we update the list, so that the list scanner
         *   knows to sort it before doing its scan
         */
        listNeedsSorting = nil
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Execute an action, as specified by an Action object.  We'll resolve
     *   the nouns in the action, then perform the action.
     */
    executeAction(targetActor, targetActorPhrase,
                  issuingActor, countsAsIssuerTurn, action)
    {
        local rm, results;

    startOver:
        /* check for a global remapping */
        rm = GlobalRemapping.findGlobalRemapping(
            issuingActor, targetActor, action);
        targetActor = rm[1];
        action = rm[2];

        /* create a basic results object to handle the resolution */
        results = new BasicResolveResults();

        /* set up the actors in the results object */
        results.setActors(targetActor, issuingActor);

        /* catch any "remap" signals while resolving noun phrases */
        try
        {
            /* resolve noun phrases */
            action.resolveNouns(issuingActor, targetActor, results);
        }
        catch (RemapActionSignal sig)
        {
            /* mark the new action as remapped */
            sig.action_.setRemapped(action);

            /* get the new action from the signal */
            action = sig.action_;

            /* start over with the new action */
            goto startOver;
        }

        /*
         *   Check to see if we should create an undo savepoint for the
         *   command.  If the action is not marked for inclusion in the undo
         *   log, there is no need to log a savepoint for it.
         *
         *   Don't save undo for nested commands.  A nested command is part of
         *   a main command, and we only want to save undo for the main
         *   command, not for its individual sub-commands.
         */
        if (action.includeInUndo
            && action.parentAction == nil
            && (targetActor.isPlayerChar()
                || (issuingActor.isPlayerChar() && countsAsIssuerTurn)))
        {
            /*
             *   Remember the command we're about to perform, so that if we
             *   undo to here we'll be able to report what we undid.  Note that
             *   we do this *before* setting the savepoint, because we want
             *   after the undo to know the command we were about to issue at
             *   the savepoint.
             */
            libGlobal.lastCommandForUndo = action.getOrigText();
            libGlobal.lastActorForUndo =
                (targetActorPhrase == nil
                 ? nil
                 : targetActorPhrase.getOrigText());

            /*
             *   set a savepoint here, so that we on 'undo' we'll restore
             *   conditions to what they were just before we executed this
             *   command
             */
            savepoint();
        }

        /*
         *   If this counts as a turn for the issuer, adjust the issuer's busy
         *   time.
         *
         *   However, this doesn't apply if the command is conversational (that
         *   is, it's something like "BOB, HELLO").  A conversational command
         *   is conceptually carried out by the issuer, not the target actor,
         *   since the action consists of the issuer actually saying something
         *   to the target actor.  The normal turn accounting in Action will
         *   count a conversational command this way, so we don't have to do
         *   the extra bookkeeping for such a command here.
         */
        if (countsAsIssuerTurn && !action.isConversational(issuingActor))
        {
            /*
             *   note in the issuer that the target is the most recent
             *   conversational partner
             */
            issuingActor.lastInterlocutor = targetActor;

            /* make the issuer busy for the order-giving interval */
            issuingActor.addBusyTime(nil,
                                     issuingActor.orderingTime(targetActor));

            /* notify the target that this will be a non-idle turn */
            targetActor.nonIdleTurn();
        }

        /*
         *   If the issuer is directing the command to a different actor, and
         *   it's not a conversational command, check with the target actor to
         *   see if it wants to accept the command.  Don't check
         *   conversational commands, since these aren't of the nature of
         *   orders to be obeyed.
         */
        if (issuingActor != targetActor
            && !action.isConversational(issuingActor)
            && !targetActor.obeyCommand(issuingActor, action))
        {
            /*
             *   if the issuing actor's "ordering time" is zero, make this take
             *   up a turn anyway, just for the refusal
             */
            if (issuingActor.orderingTime(targetActor) == 0)
                issuingActor.addBusyTime(nil, 1);

            /*
             *   Since we're aborting the command, we won't get into the normal
             *   execution for it.  However, we might still want to save it for
             *   an attempted re-issue with AGAIN, so do so explicitly now.
             */
            action.saveActionForAgain(issuingActor, countsAsIssuerTurn,
                                      targetActor, targetActorPhrase);

            /*
             *   This command was rejected, so don't process it any further,
             *   and give up on processing any remaining commands on the same
             *   command line.
             */
            throw new TerminateCommandException();
        }

        /* execute the action */
        action.doAction(issuingActor, targetActor, targetActorPhrase,
                        countsAsIssuerTurn);
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Try an implicit action.
     *
     *   Returns true if the action was attempted, whether or not it
     *   succeeded, nil if the command was not even attempted.  We will not
     *   attempt an implied command that verifies as "dangerous," since this
     *   means that it should be obvious to the player character that such a
     *   command should not be performed lightly.
     */
    _tryImplicitAction(issuingActor, targetActor, msgProp, actionClass, [objs])
    {
        local action;

        /* create an instance of the desired action class */
        action = actionClass.createActionInstance();

        /* mark the action as implicit */
        action.setImplicit(msgProp);

        /* install the resolved objects in the action */
        action.setResolvedObjects(objs...);

        /*
         *   For an implicit action, we must check the objects involved to make
         *   sure they're in scope.  If any of the objects aren't in scope,
         *   there is no way the actor would know to perform the command, so
         *   the command would not be implied in the first place.  Simply fail
         *   without trying the command.
         */
        if (!action.resolvedObjectsInScope())
            return nil;

        /*
         *   catch the abort-implicit signal, so we can turn it into a result
         *   code for our caller instead of an exception
         */
        try
        {
            /* in NPC mode, add a command separator before each implied action */
            if (targetActor.impliedCommandMode() == ModeNPC)
                gTranscript.addCommandSep();

            /* execute the action */
            action.doAction(issuingActor, targetActor, nil, nil);

            /*
             *   if the actor is in "NPC" mode for implied commands, do some
             *   extra work
             */
            if (targetActor.impliedCommandMode() == ModeNPC)
            {
                /*
                 *   we're in NPC mode, so if the implied action failed, then
                 *   act as though the command had never been attempted
                 */
                if (gTranscript.actionFailed(action))
                {
                    /* the implied command failed - act like we didn't even try */
                    return nil;
                }

                /*
                 *   In "NPC" mode, we display the results from implied
                 *   commands as though they had been explicitly entered as
                 *   separate actions.  So, add visual separation after the
                 *   results from the implied command.
                 */
                gTranscript.addCommandSep();
            }

            /* tell the caller we at least tried to execute the command */
            return true;
        }
        catch (AbortImplicitSignal sig)
        {
            /* tell the caller we didn't execute the command at all */
            return nil;
        }
        catch (ParseFailureException exc)
        {
            /*
             *   Parse failure.  If the actor is in NPC mode, we can't have
             *   asked for a new command, so this must be some failure in
             *   processing the implied command itself; most likely, we tried
             *   to resolve a missing object or the like and found that we
             *   couldn't perform interactive resolution (because of the NPC
             *   mode).  In this case, simply treat this as a failure of the
             *   implied command itself, and act as though we didn't even try
             *   the implied command.
             *
             *   If the actor is in player mode, then we *can* perform
             *   interactive resolution, so we won't have thrown a parser
             *   failure before trying to solve the problem interactively.  The
             *   failure must therefore be in an interactive response.  In this
             *   case, simply re-throw the failure so that it reaches the main
             *   parser.
             */
            if (targetActor.impliedCommandMode() == ModeNPC)
            {
                /* NPC mode - the implied command itself failed */
                return nil;
            }
            else
            {
                /* player mode - interactive resolution failed */
                throw exc;
            }
        }
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Run a replacement action.
     */
    _replaceAction(actor, actionClass, [objs])
    {
        /* run the replacement action as a nested action */
        _nestedAction(true, actor, actionClass, objs...);

        /* the invoking command is done */
        exit;
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Resolve and execute a replacement action.  This differs from the
     *   normal replacement action execution in that the action we execute
     *   requires resolution before execution.
     */
    resolveAndReplaceAction(newAction)
    {
        /* prepare the replacement action */
        prepareNestedAction(true, nil, newAction);

        /*
         *   resolve and execute the new action, using the same target and
         *   issuing actors as the original action
         */
        executeAction(gActor, nil, gIssuingActor, nil, newAction);

        /* the invoking command has been replaced, so it's done */
        exit;
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Run an action as a new turn.  Returns the CommandTranscript describing
     *   the action's results.
     */
    _newAction(transcriptClass, issuingActor, targetActor, actionClass, [objs])
    {
        local action;

        /* create an instance of the desired action class */
        action = actionClass.createActionInstance();

        /* execute the command with the action instance */
        return newActionObj(transcriptClass, issuingActor, targetActor,
                            action, objs...);
    }

    /*
     *   Run an action as a new turn.  This is almost the same as _newAction,
     *   but should be used when the caller has already explicitly created an
     *   instance of the Action to be performed.
     *
     *   If issuingActor is nil, we'll use the current global issuing actor; if
     *   that's also nil, we'll use the target actor.
     *
     *   Returns a CommandTranscript object describing the result of the
     *   action.
     */
    newActionObj(transcriptClass, issuingActor, targetActor, actionObj, [objs])
    {
        /* create the results object and install it as the global transcript */
        return withCommandTranscript(transcriptClass, function()
        {
            /* install the resolved objects in the action */
            actionObj.setResolvedObjects(objs...);

            /*
             *   if the issuing actor isn't specified, use the current global
             *   issuing actor; if that's also not set, use the target actor
             */
            if (issuingActor == nil)
                issuingActor = gIssuingActor;
            if (issuingActor == nil)
                issuingActor = targetActor;

            /*
             *   Execute the given action.  Because this is a new action,
             *   execute the action in a new sense context for the given actor.
             */
            callWithSenseContext(targetActor.isPlayerChar()
                                 ? nil : targetActor, sight,
                {: actionObj.doAction(issuingActor, targetActor, nil, nil)});

            /* return the current global transcript object */
            return gTranscript;
        });
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Run a nested action.  'isReplacement' has the same meaning as in
     *   execNestedAction().
     */
    _nestedAction(isReplacement, actor, actionClass, [objs])
    {
        local action;

        /* create an instance of the desired action class */
        action = actionClass.createActionInstance();

        /* install the resolved objects in the action */
        action.setResolvedObjects(objs...);

        /* execute the new action */
        execNestedAction(isReplacement, nil, actor, action);
    }

    /*
     *   Execute a fully-constructed nested action.
     *
     *   'isReplacement' indicates whether the action is a full replacement or
     *   an ordinary nested action.  If it's a replacement, then we use the
     *   game time taken by the replacement, and set the enclosing action
     *   (i.e., the current gAction) to take zero time.  If it's an ordinary
     *   nested action, then we consider the nested action to take zero time,
     *   using the current action's time as the overall command time.
     *
     *   'isRemapping' indicates whether or not this is a remapped action.  If
     *   we're remapping from one action to another, this will be true; for
     *   any other kind of nested or replacement action, this should be nil.
     */
    execNestedAction(isReplacement, isRemapping, actor, action)
    {
        /* prepare the nested action */
        prepareNestedAction(isReplacement, isRemapping, action);

        /* execute the new action in the actor's sense context */
        callWithSenseContext(
            actor.isPlayerChar() ? nil : actor, sight,
            {: action.doAction(gIssuingActor, actor, nil, nil) });
    }

    /*
     *   Prepare a nested or replacement action for execution.
     */
    prepareNestedAction(isReplacement, isRemapping, action)
    {
        /*
         *   if the original action is an implicit command, make the new
         *   command implicit as well
         */
        if (gAction.isImplicit)
        {
            /*
             *   make the new action implicit, but don't describe it as a
             *   separate implicit command - it's effectively part of the
             *   original implicit command
             */
            action.setImplicit(nil);
        }

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

        /* a nested action is part of the enclosing action */
        action.setOriginalAction(gAction);

        /* if this is a remapping, mark it as such */
        if (isRemapping)
            action.setRemapped(gAction);

        /*
         *   Set either the nested action's time or the enclosing (current)
         *   action's time to zero - we want to count only the time of one
         *   command or the other.
         *
         *   If we're running an ordinary nested command, set the nested
         *   command's time to zero, since we want to consider it just a part
         *   of the enclosing command and thus to take no time of its own.
         *
         *   If we're running a full replacement command, and we're replacing
         *   something other than an implied command, don't consider the
         *   enclosing command to take any time, since the enclosing command is
         *   carrying out its entire function via the replacement and thus
         *   requires no time of its own.  If we're replacing an implied
         *   command, this doesn't apply, since the implied command defers to
         *   its enclosing command for timing.  If we're replacing a command
         *   that already has zero action time, this also doesn't apply, since
         *   we're presumably replacing a command that's itself nested.
         */
        if (isReplacement && !gAction.isImplicit && gAction.actionTime != 0)
            gAction.zeroActionTime();
        else
            action.actionTime = 0;
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Run a previously-executed command as a nested action, re-resolving
     *   all of its objects to ensure they are still valid.
     */
    nestedActionAgain(action)
    {
        /* reset the any cached information for the new command context */
        action.resetAction();

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

        /*
         *   do not count any time for the nested action, since it's merely
         *   part of the main turn and doesn't count as a separate turn of its
         *   own
         */
        action.actionTime = 0;

        /* execute the command */
        executeAction(gActor, nil, gIssuingActor, nil, action);
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Run some code in a simulated Action environment.  We'll create a dummy
     *   instance of the given Action class, and set up a command transcript,
     *   then invoke the function.  This is useful for writing daemon code that
     *   needs to invoke other code that's set up to expect a normal action
     *   processing environment.
     */
    withActionEnv(actionClass, actor, func)
    {
        local oldAction, oldActor;

        /* remember the old globals */
        oldAction = gAction;
        oldActor = gActor;

        try
        {
            /* set up a dummy action */
            gAction = actionClass.createInstance();

            /* use the player character as the actor */
            gActor = actor;

            /*
             *   execute the function with a command transcript active; obtain
             *   and return the return value of the function
             */
            return withCommandTranscript(CommandTranscript, func);
        }
        finally
        {
            /* restore globals on the way out */
            gAction = oldAction;
            gActor = oldActor;
        }
    }

    /* ------------------------------------------------------------------------ */
    /*
     *   Exit signal.  This signal indicates that we're finished with the
     *   entire command execution sequence for an action; the remainder of the
     *   command execution sequence is to be skipped for the action.  Throw
     *   this from within the command execution sequence in order to skip
     *   directly to the end-of-turn processing.  This skips everything
     *   remaining in the action, including after-action notification and the
     *   like.  This signal skips directly past the 'afterAction' phase of the
     *   command.
     *
     *   Note that this doesn't prevent further processing of the same command
     *   if there are multiple objects involved, and it doesn't affect
     *   processing of additional commands on the same command line.  If you
     *   want to cancel further iteration of the same command for additional
     *   objects, call gAction.cancelIteration().
     */
    class ExitSignal: Exception
    ;

    /*
     *   Exit Action signal.  This signal indicates that we're finished with
     *   the execAction portion of processing the command, but we still want
     *   to proceed with the rest of the command as normal.  This can be used
     *   when a step in the action processing wants to preempt any of the more
     *   default processing that would normally follow.  This skips directly
     *   to the 'afterAction' phase of the command.
     *
     *   Note that this doesn't prevent further processing of the same command
     *   if there are multiple objects involved, and it doesn't affect
     *   processing of additional commands on the same command line.  If you
     *   want to cancel further iteration of the same command for additional
     *   objects, call gAction.cancelIteration().
     */
    class ExitActionSignal: Exception
    ;

    /*
     *   Abort implicit command signal.  This exception indicates that we are
     *   aborting an implicit command without having tried to execute the
     *   command at all.  This is thrown when an implied command is to be
     *   aborted before it's even attempted, such as when verification shows
     *   the command is obviously dangerous and thus should never be attempted
     *   without the player having explicitly requesting it.
     */
    class AbortImplicitSignal: Exception
    ;

    /* ------------------------------------------------------------------------ */
    /*
     *   Action Remap signal.  This signal can be thrown only during the noun
     *   phrase resolution phase of execution, and indicates that we want to
     *   remap the action to a different action, specified in the signal.
     *
     *   This is useful when an object is always used in a special way, so
     *   that a generic verb used with the object must be mapped to a more
     *   specific verb on the object.  For example, a game with a generic USE
     *   verb might convert USE PAINTBRUSH ON WALL to PAINT WALL WITH
     *   PAINTBRUSH by remapping the UseWith action to a PaintWith action
     *   instead.
     */
    class RemapActionSignal: Exception
        construct(action)
        {
            /* remember the new action */
            action_ = action;
        }

        /* the new action that should replace the original action */
        action_ = nil
    ;

    /*
     *   Remap a 'verify' method for a remapped action.  This is normally
     *   invoked through the remapTo() macro.
     */
    remapVerify(oldRole, resultSoFar, remapInfo)
    {
        local newAction;
        local objs;
        local idx;
        local newRole;

        /* extract new action's object list from the remapping info list */
        objs = remapInfo.sublist(2);

        /*
         *   Create a new action object.  We only perform verification
         *   remapping during the resolution phase of the command processing,
         *   because once we've finished resolving, we'll actually replace the
         *   action with the remapped action and thus won't have to remap
         *   verification (or anything else) at that point.  So, pass true for
         *   the in-resolve flag to the action creation routine.
         */
        newAction = remapActionCreate(true, oldRole, remapInfo);

        /*
         *   Find the object that's given as a resolved object, rather than as
         *   a DirectObject (etc) identifier - the one given as a specific
         *   object is the one that corresponds to the original object.
         */
        idx = objs.indexWhich({x: dataType(x) == TypeObject});

        /* get the role identifier (DirectObject, etc) for the slot position */
        newRole = newAction.getRoleFromIndex(idx);

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

        /* if we found a remapping, verify it */
        if (idx != nil)
        {
            /*
             *   Remember the remapped object in the result list.  Note that we
             *   do this first, before calling the remapped verification
             *   property, so that our call to the remapped verification
             *   property will overwrite this setting if it does further
             *   remapping.  We want the ultimate target object represented
             *   here, after all remappings are finished.
             */
            resultSoFar.remapAction_ = newAction;
            resultSoFar.remapTarget_ = objs[idx];
            resultSoFar.remapRole_ = newRole;

            /* install the new action as the current action while verifying */
            local oldAction = gAction;
            gAction = newAction;

            try
            {
                /* call verification on the new object in the new role */
                return newAction.callVerifyProp(
                    objs[idx],
                    newAction.getVerifyPropForRole(newRole),
                    newAction.getPreCondPropForRole(newRole),
                    newAction.getRemapPropForRole(newRole),
                    resultSoFar, newRole);
            }
            finally
            {
                /* restore the old gAction on the way out */
                gAction = oldAction;
            }
        }
        else
        {
            /* there's no remapping, so there's nothing to verify */
            return resultSoFar;
        }
    }

    /*
     *   Perform a remapping to a new action.  This is normally invoked
     *   through the remapTo() macro.
     */
    remapAction(inResolve, oldRole, remapInfo)
    {
        local newAction;

        /* get the new action */
        newAction = remapActionCreate(inResolve, oldRole, remapInfo);

        /*
         *   replace the current action, using the appropriate mechanism
         *   depending on the current processing phase
         */
        if (inResolve)
        {
            /*
             *   we're still resolving the objects, so we must use a signal to
             *   start the resolution process over for the new action
             */
            throw new RemapActionSignal(newAction);
        }
        else
        {
            /*
             *   We've finished resolving everything, so we can simply use the
             *   new action as a replacement action.
             */
            execNestedAction(true, true, gActor, newAction);

            /*
             *   the remapped action replaces the original action, so
             *   terminate the original action
             */
            exit;
        }
    }

    /*
     *   Create a new action object for the given remapped action.
     */
    remapActionCreate(inResolve, oldRole, remapInfo)
    {
        local newAction;
        local newObjs;
        local newActionClass;
        local objs;

        /* get the new action class and object list from the remap info */
        newActionClass = remapInfo[1];
        objs = remapInfo.sublist(2);

        /*
         *   create a new instance of the replacement action, carrying forward
         *   the properties of the original (current) action
         */
        newAction = newActionClass.createActionFrom(gAction);

        /* remember the original action we're remapping */
        newAction.setOriginalAction(gAction);

        /* set up an empty vector for the match trees for the new action */
        newObjs = new Vector(objs.length());

        /* remap according to the phase of the execution */
        if (inResolve)
        {
            /* translate the object mappings */
            foreach (local cur in objs)
            {
                /* check what we have to translate */
                if (dataType(cur) == TypeEnum)
                {
                    /*
                     *   it's an object role - if it's the special OtherObject
                     *   designator, get the other role of a two-object
                     *   command
                     */
                    if (cur == OtherObject)
                        cur = gAction.getOtherObjectRole(oldRole);

                    /*
                     *   get the match tree for this role from the old action
                     *   and add it to our list
                     */
                    newObjs.append(gAction.getMatchForRole(cur));
                }
                else
                {
                    /* append the new ResolveInfo to the new object list */
                    newObjs.append(gAction.getResolveInfo(cur, oldRole));
                }
            }

            /* set the object matches in the new action */
            newAction.setObjectMatches(newObjs.toList()...);
        }
        else
        {
            /* translate the object mappings */
            foreach (local cur in objs)
            {
                /* check what we have to translate */
                if (dataType(cur) != TypeEnum)
                {
                    /* it's an explicit object - use it directly */
                    newObjs.append(cur);
                }
                else
                {
                    /* it's a role - translate OtherObject if needed */
                    if (cur == OtherObject)
                        cur = gAction.getOtherObjectRole(oldRole);

                    /* get the resolved object for this role */
                    newObjs.append(gAction.getObjectForRole(cur));
                }
            }

            /* set the resolved objects in the new action */
            newAction.setResolvedObjects(newObjs.toList()...);
        }

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

    /* ------------------------------------------------------------------------ */
    /*
     *   Result message object.  This is used for verification results and
     *   main command reports, which must keep track of messages to display.
     */
    class MessageResult: object
        /*
         *   Construct given literal message text, or alternatively a property
         *   of the current actor's verb messages object.  In either case,
         *   we'll expand the message immediately to allow the message to be
         *   displayed later with any parameters fixed at the time the message
         *   is constructed.
         */
        construct(msg, [params])
        {
            /* if we're based on an existing object, copy its characteristics */
            if (dataType(msg) == TypeObject && msg.ofKind(MessageResult))
            {
                /* base it on the existing object */
                messageText_ = msg.messageText_;
                messageProp_ = msg.messageProp_;
                return;
            }

            /*
             *   if the message was given as a property, remember the property
             *   for identification purposes
             */
            if (dataType(msg) == TypeProp)
                messageProp_ = msg;

            /*
             *   Resolve the message and store the text.  Use the action's
             *   objects (the direct object, indirect object, etc) as the
             *   sources for message overrides - this makes it easy to override
             *   messages on a per-object basis without having to rewrite the
             *   whole verify/check/action routines.
             */
            messageText_ = resolveMessageText(gAction.getCurrentObjects(),
                                              msg, params);
        }

        /*
         *   Static method: resolve a message.  If the message is given as a
         *   property, we'll look up the message in the given source objects
         *   and in the actor's "action messages" object.  We'll return the
         *   resolved message string.
         */
        resolveMessageText(sources, msg, params)
        {
            /*
             *   If we have more than one source object, it means that the
             *   command has more than one object slot (such as a TIAction,
             *   which has direct and indirect objects).  Rearrange the list so
             *   that the nearest caller is the first object in the list.  If
             *   one of these source objects provides an override, we generally
             *   want to get the message from the immediate caller rather than
             *   the other object.  Note that we only care about the *first*
             *   source object we find in the stack trace, because we only care
             *   about the actual message generator call; enclosing calls
             *   aren't relevant to the message priority because they don't
             *   necessarily have anything to do with the messaging.
             */
            if (sources.length() > 1)
            {
                /* look through the stack trace for a 'self' in the source list */
                local tr = t3GetStackTrace();
                for (local i = 1, local trCnt = tr.length() ; i <= trCnt ; ++i)
                {
                    /* check this 'self' */
                    local s = tr[i].self_;
                    local sIdx = sources.indexOf(s);
                    if (sIdx != nil)
                    {
                        /*
                         *   it's a match - move this object to the head of the
                         *   list so that we give its message bindings priority
                         */
                        if (sIdx != 1)
                            sources = [s] + (sources - s);

                        /* no need to look any further */
                        break;
                    }
                }
            }

            /*
             *   The message can be given either as a string or as a property
             *   of the actor's verb message object.  If it's the latter, look
             *   up the text of the property from the appropriate object.
             */
        findTextSource:
            if (dataType(msg) == TypeProp)
            {
                local msgObj;

                /*
                 *   Presume that we'll read the message from the current
                 *   actor's "action message object."  This is typically
                 *   playerActionMessages or npcActionMessages, but it's up to
                 *   the actor to specify which object we get our messages
                 *   from.
                 */
                msgObj = gActor.getActionMessageObj();

                /*
                 *   First, look up the message property in the action's
                 *   objects (the direct object, indirect object, etc).  This
                 *   makes it easy to override messages on a per-object basis
                 *   without having to rewrite the whole verify/check/action
                 *   routine.
                 */
                foreach (local cur in sources)
                {
                    /* check to see if this object defines the message property */
                    if (cur != nil && cur.propDefined(msg))
                    {
                        local res;

                        /*
                         *   This object defines the property, so check what
                         *   we have.
                         */
                        switch (cur.propType(msg))
                        {
                        case TypeProp:
                            /*
                             *   It's another property, so we're being
                             *   directed back to the player action message
                             *   object.  The object does override the
                             *   message, but the override points to another
                             *   message property in the action object message
                             *   set.  Simply redirect 'msg' to point to the
                             *   new property, and use the same action message
                             *   object we already assumed we'd use.
                             */
                            msg = cur.(msg);
                            break;

                        case TypeSString:
                            /* it's a simple string - retrieve it */
                            msg = cur.(msg);

                            /*
                             *   since it's just a string, we're done finding
                             *   the message text - there's no need to do any
                             *   further property lookup, since we've obviously
                             *   reached the end of that particular line
                             */
                            break findTextSource;

                        case TypeCode:
                            /*
                             *   Check the parameter count - we'll allow this
                             *   method to take the full set of parameters, or
                             *   no parameters at all.  We allow the no-param
                             *   case for convenience in cases where the method
                             *   simply wants to return a string or property ID
                             *   from a short method that doesn't need to know
                             *   the parameters; in these cases, it's
                             *   syntactically a lot nicer looking to write it
                             *   as a "prop = (expresion)" than to write the
                             *   full method-with-params syntax.
                             */
                            if (cur.getPropParams(msg) == [0, 0, nil])
                                res = cur.(msg);
                            else
                                res = cur.(msg)(params...);

                            /*
                             *   If that returned nil, ignore it entirely and
                             *   keep scanning the remaining source objects.
                             *   The object must have decided it didn't want to
                             *   provide the message override in this case
                             *   after all.
                             */
                            if (res == nil)
                                continue;

                            /* we didn't get nil, so use the result */
                            msg = res;

                            /*
                             *   if we got a string, we've fully resolved the
                             *   message text, so we can stop searching for it
                             */
                            if (dataType(msg) == TypeSString)
                                break findTextSource;

                            /*
                             *   It's not nil and it's not a string, so it must
                             *   be a property ID.  In this case, the property
                             *   ID is a property to evaluate in the normal
                             *   action message object.  Simply proceed to
                             *   evaluate the new message property as normal.
                             */
                            break;

                        case TypeNil:
                            /*
                             *   it's explicitly nil, which simply means to
                             *   ignore this definition; keep scanning other
                             *   source objects
                             */
                            continue;

                        default:
                            /*
                             *   In any other case, this must simply be the
                             *   message we're to use.  For this case, the
                             *   source of the message is this object, so
                             *   forget about the normal action message object
                             *   and instead use the current object.  Then
                             *   proceed to evaluate the message property as
                             *   normal, which will fetch it from the current
                             *   object.
                             */
                            msgObj = cur;
                            break;
                        }

                        /*
                         *   we found a definition, so we don't need to look
                         *   at any of the other objects involved in the
                         *   action - we just use the first override we find
                         */
                        break;
                    }
                }

                /* look up the message in the actor's message generator */
                msg = msgObj.(msg)(params...);
            }

            /*
             *   format the string and remember the result - do the formatting
             *   immediately, because we want to make sure we expand any
             *   substitution parameters in the context of the current
             *   command, since the parameters might change (and thus alter
             *   the meaning of the message) by the time it's displayed
             */
            msg = langMessageBuilder.generateMessage(msg);

            /*
             *   "quote" the message text - it's fully expanded now, so
             *   there's no need to further expand anything that might by
             *   coincidence look like substitution parameters in its text
             */
            msg = langMessageBuilder.quoteMessage(msg);

            /* return the resolved message string */
            return msg;
        }

        /*
         *   set a new message, given the same type of information as we'd use
         *   to construct the object
         */
        setMessage(msg, [params])
        {
            /* simply invoke the constructor to re-fill the message data */
            construct(msg, params...);
        }

        /*
         *   Display a message describing why the command isn't allowed.
         */
        showMessage()
        {
            /* show our message string */
            say(messageText_);
        }

        /* the text of our result message */
        messageText_ = nil

        /* the message property, if we have one */
        messageProp_ = nil
    ;

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