#charset "us-ascii"

/* 
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
 *   
 *   TADS 3 Library: Actions.
 *   
 *   This module defines the set of built-in library actions.  
 */

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

/* ------------------------------------------------------------------------ */
/*
 *   Special "debug" action - this simply breaks into the debugger, if the
 *   debugger is present. 
 */
DefineIAction(Debug)
    execAction()
    {
        /* if the debugger is present, break into it */
        if (t3DebugTrace(T3DebugCheck))
            t3DebugTrace(T3DebugBreak);
        else
            "Debugger not present. ";
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Special internal action to note a change to the darkness level.  This
 *   command is invoked internally when a change to the darkness level
 *   occurs.  
 */
DefineIAction(NoteDarkness)
    execAction()
    {
        /* 
         *   if we're in the dark, note that darkness has fallen;
         *   otherwise, show the player character's room description as
         *   though the player had typed "look" 
         */
        if (gActor.isLocationLit())
        {
            /* look around */
            gActor.lookAround(true);
        }
        else
        {
            /* it is now dark */
            mainReport(&newlyDarkMsg);
        }
    }

    /* this is an internal command that takes no time */
    actionTime = 0

    /* this isn't a real action, so it's not repeatable */
    isRepeatable = nil

    /* this action doesn't do anything; don't include it in undo */
    includeInUndo = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Special "again" action.  This command repeats the previous command.
 */
DefineIAction(Again)
    /* for obvious reasons, 'again' is not itself repeatable with 'again' */
    isRepeatable = nil

    /* 
     *   the undo command itself is not undoable (but the underlying
     *   command that we repeat might be) 
     */
    includeInUndo = nil

    /* information on the most recent command */
    lastIssuingActor = nil
    lastTargetActor = nil
    lastTargetActorPhrase = nil
    lastAction = nil

    /* save the most recent command so that it can be repeated if desired */
    saveForAgain(issuingActor, targetActor, targetActorPhrase, action)
    {
        /* save the information */
        lastIssuingActor = issuingActor;
        lastTargetActor = targetActor;
        lastTargetActorPhrase = targetActorPhrase;
        lastAction = action;
    }

    /* forget the last command, so that AGAIN cannot be used */
    clearForAgain() { lastAction = nil; }

    /* 
     *   Execute the 'again' command.  This action is special enough that
     *   we override its entire action processing sequence - this is
     *   necessary in case we're repeating another special command, such
     *   as 'again', and in any case is desirable because we don't want
     *   'again' to count as a command in its own right; it's essentially
     *   just a macro that we replace with the original command. 
     */
    doAction(issuingActor, targetActor, targetActorPhrase,
             countsAsIssuerTurn)
    {
        /* if there's nothing to repeat, show an error and give up */
        if (lastAction == nil)
        {
            gLibMessages.noCommandForAgain();
            return;
        }

        /* 
         *   'again' cannot be executed with a target actor - the target
         *   actor must be the player character 
         */
        if (!targetActor.isPlayerChar)
        {
            gLibMessages.againCannotChangeActor();
            return;
        }

        /* 
         *   if the issuing actor isn't the same as the target actor, make
         *   sure the issuer can still talk to the target 
         */
        if (lastIssuingActor != lastTargetActor
            && !lastIssuingActor.canTalkTo(lastTargetActor))
        {
            /* complain that we can no longer talk to the target */
            gLibMessages.againCannotTalkToTarget(
                lastIssuingActor, lastTargetActor);
            return;
        }

        /*
         *   If the last issuing actor is different from the last target
         *   actor, then the command counts as an issuer turn, because
         *   we're effectively repeating the entire last command, including
         *   the target actor specification.  That is, after a command like
         *   "bob, go east", saying "again" is just like saying "bob, go
         *   east" again, which counts as an issuer turn.  
         */
        if (lastTargetActor != lastIssuingActor)
            countsAsIssuerTurn = true;

        /* reset any cached information for the new command context */
        lastAction.resetAction();
        
        /* repeat the action */
        lastAction.repeatAction(lastTargetActor, lastTargetActorPhrase,
                                lastIssuingActor, countsAsIssuerTurn);

        /*
         *   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 (lastTargetActor != lastIssuingActor)
            lastIssuingActor.waitForIssuedCommand(lastTargetActor);
    }

    /* 
     *   this command itself consumes no time on the game clock (although
     *   the action we perform might) 
     */
    actionTime = 0
;

/* ------------------------------------------------------------------------ */
/*
 *   PreSaveObject - every instance of this class is notified, via its
 *   execute() method, just before we save the game.  This uses the
 *   ModuleExecObject framework, so the sequencing lists (execBeforeMe,
 *   execAfterMe) can be used to control relative ordering of execution
 *   among instances.  
 */
class PreSaveObject: ModuleExecObject
    /*
     *   Each instance must override execute() with its specific pre-save
     *   code. 
     */
;

/*
 *   PostRestoreObject - every instance of this class is notified, via its
 *   execute() method, immediately after we restore the game. 
 */
class PostRestoreObject: ModuleExecObject
    /* 
     *   note: each instance must override execute() with its post-restore
     *   code 
     */

    /*
     *   The "restore code," which is the (normally integer) value passed
     *   as the second argument to restoreGame().  The restore code gives
     *   us some idea of what triggered the restoration.  By default, we
     *   define the following restore codes:
     *   
     *   1 - the system is restoring a game as part of interpreter
     *   startup, usually because the user explicitly specified a game to
     *   restore on the interpreter command line or via a GUI shell
     *   mechanism, such as double-clicking on a saved game file from the
     *   desktop.
     *   
     *   2 - the user is explicitly restoring a game via a RESTORE command.
     *   
     *   Games and library extensions can use their own additional restore
     *   codes in their calls to restoreGame().  
     */
    restoreCode = nil
;

/*
 *   PreRestartObject - every instance of this class is notified, via its
 *   execute() method, just before we restart the game (with a RESTART
 *   command, for example). 
 */
class PreRestartObject: ModuleExecObject
    /* 
     *   Each instance must override execute() with its specific
     *   pre-restart code.  
     */
;

/*
 *   PostUndoObject - every instance of this class is notified, via its
 *   execute() method, immediately after we perform an 'undo' command. 
 */
class PostUndoObject: ModuleExecObject
    /* 
     *   Each instance must override execute() with its specific post-undo
     *   code.  
     */
;

/* ------------------------------------------------------------------------ */
/*
 *   Special "save" action.  This command saves the current game state to
 *   an external file for later restoration. 
 */
DefineSystemAction(Save)
    execSystemAction()
    {
        local result;
        local origElapsedTime;

        /* note the current elapsed game time */
        origElapsedTime = realTimeManager.getElapsedTime();

        /* ask for a file */
        result = getInputFile(gLibMessages.getSavePrompt(), InFileSave,
                              FileTypeT3Save, 0);

        /* check the inputFile response */
        switch(result[1])
        {
        case InFileSuccess:
            /* perform the save on the given file */
            performSave(result[2]);

            /* done */
            break;

        case InFileFailure:
            /* advise of the failure of the prompt */
            gLibMessages.filePromptFailed();
            break;

        case InFileCancel:
            /* acknowledge the cancellation */
            gLibMessages.saveCanceled();
            break;
        }

        /* 
         *   restore the original elapsed game time, so that the time spent
         *   in the file selector dialog doesn't count against the game
         *   time 
         */
        realTimeManager.setElapsedTime(origElapsedTime);
    }

    /* perform a save */
    performSave(fname)
    {
        /* before saving the game, notify all PreSaveObject instances */
        PreSaveObject.classExec();
        
        /* 
         *   Save the game to the given file.  If an error occurs, the
         *   save routine will throw a runtime error.  
         */
        try
        {
            /* try saving the game */
            saveGame(fname);
        }
        catch (RuntimeError err)
        {
            /* the save failed - mention the problem */
            gLibMessages.saveFailed(err);
            
            /* done */
            return;
        }
        
        /* note the successful save */
        gLibMessages.saveOkay();
    }

    /* 
     *   Saving has no effect on game state, so it's irrelevant whether or
     *   not it's undoable; but it might be confusing to say we undid a
     *   "save" command, because the player might think we deleted the
     *   saved file.  To avoid such confusion, do not include "save"
     *   commands in the undo log.  
     */
    includeInUndo = nil

    /* 
     *   Don't allow this to be repeated with AGAIN.  There's no point in
     *   repeating a SAVE immediately, as nothing will have changed in the
     *   game state to warrant saving again.  
     */
    isRepeatable = nil
;

/*
 *   Subclass of Save action that takes a literal string as part of the
 *   command.  The filename must be a literal enclosed in quotes, and the
 *   string (with the quotes) must be stored in our fname_ property by
 *   assignment of a quotedStringPhrase production in the grammar rule.  
 */
DefineAction(SaveString, SaveAction)
    execSystemAction()
    {
        /* 
         *   Perform the save, using the filename given in our fname_
         *   parameter, trimmed of quotes.  
         */
        performSave(fname_.getStringText());
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   Special "restore" action.  This action restores game state previously
 *   saved with the "save" action.
 */
DefineSystemAction(Restore)
    execSystemAction()
    {
        /* ask for a file and restore it */
        askAndRestore();

        /* 
         *   regardless of what happened, abandon any additional commands
         *   on the same command line 
         */
        throw new TerminateCommandException();
    }

    /*
     *   Ask for a file and try to restore it.  Returns true on success,
     *   nil on failure.  (Failure could indicate that the user chose to
     *   cancel out of the file selector, that we couldn't find the file to
     *   restore, or that the file isn't a valid saved state file.  In any
     *   case, we show an appropriate message on failure.)  
     */
    askAndRestore()
    {
        local succ;        
        local result;
        local origElapsedTime;

        /* presume failure */
        succ = nil;

        /* note the current elapsed game time */
        origElapsedTime = realTimeManager.getElapsedTime();

        /* ask for a file */
        result = getInputFile(gLibMessages.getRestorePrompt(), InFileOpen,
                              FileTypeT3Save, 0);

        /* 
         *   restore the real-time clock, so that the time spent in the
         *   file selector dialog doesn't count against the game time 
         */
        realTimeManager.setElapsedTime(origElapsedTime);

        /* check the inputFile response */
        switch(result[1])
        {
        case InFileSuccess:
            /* 
             *   try restoring the file; use code 2 to indicate that the
             *   restoration was performed by an explicit RESTORE command 
             */
            if (performRestore(result[2], 2))
            {
                /* note that we succeeded */
                succ = true;
            }
            else
            {
                /* 
                 *   failed - in case the failed restore took some time,
                 *   restore the real-time clock, so that the file-reading
                 *   time doesn't count against the game time 
                 */
                realTimeManager.setElapsedTime(origElapsedTime);
            }

            /* done */
            break;

        case InFileFailure:
            /* advise of the failure of the prompt */
            gLibMessages.filePromptFailed();
            break;

        case InFileCancel:
            /* acknowledge the cancellation */
            gLibMessages.restoreCanceled();
            break;
        }

        /* 
         *   If we were successful, clear out the AGAIN memory.  This
         *   avoids any confusion about whether we're repeating the RESTORE
         *   command itself, the command just before RESTORE from the
         *   current session, or the last command before SAVE from the
         *   restored game. 
         */
        if (succ)
            AgainAction.clearForAgain();

        /* return the success/failure indication */
        return succ;
    }

    /*
     *   Restore a game on startup.  This can be called from mainRestore()
     *   to restore a saved game directly as part of loading the game.
     *   (Most interpreters provide a way of starting the interpreter
     *   directly with a saved game to be restored, skipping the
     *   intermediate step of running the game and using a RESTORE
     *   command.)
     *   
     *   Returns true on success, nil on failure.  On failure, the caller
     *   should simply exit the program.  On success, the caller should
     *   start the game running, usually using runGame(), after showing any
     *   desired introductory messages.  
     */
    startupRestore(fname)
    {
        /* 
         *   try restoring the game, using code 1 to indicate that this is
         *   a direct startup restore 
         */
        if (performRestore(fname, 1))
        {
            /* success - tell the caller to proceed with the restored game */
            return true;
        }
        else
        {
            /* 
             *   Failure.  We've described the problem, so ask the user
             *   what they want to do about it. 
             */
            try
            {
                /* show options and read the response */
                failedRestoreOptions();

                /* if we get here, proceed with the game */
                return true;
            }
            catch (QuittingException qe)
            {
                /* quitting - tell the caller to terminate */
                return nil;
            }
        }
    }
    

    /*
     *   Restore a file.  'code' is the restoreCode value for the
     *   PostRestoreObject notifications.  Returns true on success, nil on
     *   failure.  
     */
    performRestore(fname, code)
    {
        try
        {
            /* restore the file */
            restoreGame(fname);
        }
        catch (RuntimeError err)
        {
            /* failed - check the error to see what went wrong */
            switch(err.errno_)
            {
            case 1201:
                /* not a saved state file */
                gLibMessages.restoreInvalidFile();
                break;
                
            case 1202:
                /* saved by different game or different version */
                gLibMessages.restoreInvalidMatch();
                break;
                
            case 1207:
                /* corrupted saved state file */
                gLibMessages.restoreCorruptedFile();
                break;
                
            default:
                /* some other failure */
                gLibMessages.restoreFailed(err);
                break;
            }

            /* indicate failure */
            return nil;
        }

        /* note that we've successfully restored the game */
        gLibMessages.restoreOkay();
        
        /* set the appropriate restore-action code */
        PostRestoreObject.restoreCode = code;

        /* notify all PostRestoreObject instances */
        PostRestoreObject.classExec();

        /* 
         *   look around, to refresh the player's memory of the state the
         *   game was in when saved 
         */
        "\b";
        libGlobal.playerChar.lookAround(true);

        /* indicate success */
        return true;
    }
    
    /* 
     *   There's no point in including this in undo.  If the command
     *   succeeds, it's not undoable itself, and there won't be any undo
     *   information in the newly restored state.  If the command fails, it
     *   won't make any changes to the game state, so there won't be
     *   anything to undo.  
     */
    includeInUndo = nil
;

/*
 *   Subclass of Restore action that takes a literal string as part of the
 *   command.  The filename must be a literal enclosed in quotes, and the
 *   string (with the quotes) must be stored in our fname_ property by
 *   assignment of a quotedStringPhrase production in the grammar rule.  
 */
DefineAction(RestoreString, RestoreAction)
    execSystemAction()
    {
        /* 
         *   Perform the restore, using the filename given in our fname_
         *   parameter, trimmed of quotes.  Use code 2, the same as any
         *   other explicit RESTORE command.  
         */
        performRestore(fname_.getStringText(), 2);

        /* abandon any additional commands on the same command line */
        throw new TerminateCommandException();
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Restart the game from the beginning.
 */
DefineSystemAction(Restart)
    execSystemAction()
    {
        /* confirm that they really want to restart */
        gLibMessages.confirmRestart();
        if (yesOrNo())
        {
            /* 
             *   The confirmation input will have put us into
             *   start-of-command mode for sequencing purposes; force the
             *   sequencer back to mid-command mode, so we can show
             *   inter-command separation before the restart. 
             */

            /* restart the game */
            doRestartGame();
        }
        else
        {
            /* confirm that we're not really restarting */
            gLibMessages.notRestarting();
        }
    }

    /* carry out the restart action */
    doRestartGame()
    {
        /* 
         *   Show a command separator, to provide separation from any
         *   introductory text that we'll show on restarting.  Note that
         *   we probably just asked for confirmation, which means that the
         *   command sequencer will be in start-of-command mode; force it
         *   back to mid-command mode so we show inter-command separation.
         */
        commandSequencer.setCommandMode();
        "<.commandsep>";

        /* before restarting, notify anyone interested of our intentions */
        PreRestartObject.classExec();

        /* 
         *   Throw a 'restart' signal; the main entrypoint loop will catch
         *   this and actually perform the restart.
         *   
         *   Note that we *could* do the VM reset (via restartGame()) here,
         *   but there's an advantage to doing it in the main loop: we
         *   won't be in the stack context of whatever command we're
         *   performing.  If we did the restart here, it's possible that
         *   some useless objects would survive the VM reset just because
         *   they're referenced from within a caller's stack frame.  Those
         *   objects would immediately go out of scope when we get back to
         *   the main loop, but they might survive long enough to create
         *   apparent inconsistencies.  In particular, if we did a
         *   firstObj/nextObj loop, we could discover those objects and
         *   re-establish more lasting references to them, which we
         *   certainly don't want to do.  By deferring the VM reset until
         *   we get back to the main loop, we'll ensure that objects won't
         *   survive the reset just because they're on the stack
         *   momentarily here.  
         */
        throw new RestartSignal();
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Undo one turn. 
 */
DefineSystemAction(Undo)
    /*
     *   "Undo" is so special that we must override the entire action
     *   processing sequence.  We do this because undoing will restore the
     *   game state as of the previous savepoint, which would leave all
     *   sorts of things unsynchronized in the normal action sequence.  To
     *   avoid problems, we simply leave out any other action processing
     *   and perform the 'undo' directly.  
     */
    doAction(issuingActor, targetActor, targetActorPhrase,
             countsAsIssuerTurn)
    {
        /* 
         *   don't allow this unless the player character is performing
         *   the command directly
         */
        if (!targetActor.isPlayerChar)
        {
            /* 
             *   tell them this command cannot be directed to another
             *   actor, and give up 
             */
            gLibMessages.systemActionToNPC();
            return;
        }

        /* perform the undo */
        performUndo(true);
    }

    /* 
     *   Perform undo.  Returns true if we were successful, nil if not.
     *   
     *   'asCommand' indicates whether or not the undo is being performed
     *   as an explicit command: if so, we'll save the UNDO command for use
     *   in AGAIN.  
     */
    performUndo(asCommand)
    {
        /* try undoing to the previous savepoint */
        if (undo())
        {
            local oldActor;
            local oldIssuer;
            local oldAction;

            /* notify all PostUndoObject instances */
            PostUndoObject.classExec();

            /* set up the globals for the command */
            oldActor = gActor;
            oldIssuer = gIssuingActor;
            oldAction = gAction;

            /* set the new globals */
            gActor = gPlayerChar;
            gIssuingActor = gPlayerChar;
            gAction = self;

            /* make sure we reset globals on the way out */
            try
            {
                /* success - mention what we did */
                gLibMessages.undoOkay(libGlobal.lastActorForUndo,
                                      libGlobal.lastCommandForUndo);
                
                /* look around, to refresh the player's memory */
                libGlobal.playerChar.lookAround(true);
            }
            finally
            {
                /* restore the parser globals to how we found them */
                gActor = oldActor;
                gIssuingActor = oldIssuer;
                gAction = oldAction;
            }
                
            /* 
             *   if this was an explicit 'undo' command, save the command
             *   to allow repeating it with 'again' 
             */
            if (asCommand)
                AgainAction.saveForAgain(gPlayerChar, gPlayerChar, nil, self);

            /* indicate success */
            return true;
        }
        else
        {
            /* no more undo information available */
            gLibMessages.undoFailed();

            /* indicate failure */
            return nil;
        }
    }

    /* 
     *   "undo" is not undoable - if we undo again after an undo, we undo
     *   the next most recent command
     */
    includeInUndo = nil
;

/* ------------------------------------------------------------------------ */
/*
 *   Save the defaults 
 */
DefineSystemAction(SaveDefaults)
    execSystemAction()
    {
        /* tell SettingsItem to save all settings */
        settingsUI.saveSettingsMsg();
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   Restore defaults 
 */
DefineSystemAction(RestoreDefaults)
    execSystemAction()
    {
        /* 
         *   Tell SettingsItem to restore all settings.  This is an
         *   explicit request, so we want SettingsItem to describe what
         *   happened. 
         */
        settingsUI.restoreSettingsMsg();
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Quit the game. 
 */
DefineSystemAction(Quit)
    execSystemAction()
    {
        /* confirm that they really want to quit */
        gLibMessages.confirmQuit();
        if (yesOrNo())
        {
            /* carry out the termination */
            terminateGame();
        }
        else
        {
            /* show the confirmation that we're not quitting */
            gLibMessages.notTerminating();
        }
    }

    /*
     *   Carry out game termination.  This can be called when we wish to
     *   end the game without asking for any additional player
     *   confirmation.  
     */
    terminateGame()
    {
        /* acknowledge that we're quitting */
        gLibMessages.okayQuitting();
            
        /* throw a 'quitting' signal to end the game */
        throw new QuittingException;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   Pause the game.  This stops the real-time clock until the user
 *   presses a key.  Games that don't use the real-time clock will have no
 *   use for this. 
 */
DefineSystemAction(Pause)
    execSystemAction()
    {
        local elapsed;
        
        /* 
         *   remember the current elapsed game real time - when we are
         *   released from the pause, we'll restore this time 
         */
        elapsed = realTimeManager.getElapsedTime();

        /* show our prompt */
        gLibMessages.pausePrompt();

        /* keep going until we're released */
    waitLoop:
        for (;;)
        {
            /* 
             *   Wait for a key, and see what we have.  Note that we
             *   explicitly do not want to allow any real-time events to
             *   occur, so we simply wait forever without timeout. 
             */
            switch(inputKey())
            {
            case ' ':
                /* space key - end the wait */
                break waitLoop;

            case 's':
            case 'S':
                /* mention that we're saving */
                gLibMessages.pauseSaving();
                
                /* 
                 *   set the elapsed time to the time when we started, so
                 *   that the saved position reflects the time at the
                 *   start of the pause 
                 */
                realTimeManager.setElapsedTime(elapsed);

                /* save the game - go run the normal SAVE command */
                SaveAction.execSystemAction();

                /* show our prompt again */
                "<.p>";
                gLibMessages.pausePrompt();

                /* go back to wait for another key */
                break;
                
            case '[eof]':
                /* end-of-file on keyboard input - throw an error */
                "\b";
                throw new EndOfFileException();

            default:
                /* ignore other keys; just go back to wait again */
                break;
            }
        }

        /* show the released-from-pause message */
        gLibMessages.pauseEnded();

        /* 
         *   set the real-time clock to the same elapsed game time
         *   that we had when we started the pause, so that the
         *   elapsed real time of the pause itself doesn't count
         *   against the game elapsed time 
         */
        realTimeManager.setElapsedTime(elapsed);
    }
;

/*
 *   Change to VERBOSE mode. 
 */
DefineSystemAction(Verbose)
    execSystemAction()
    {
        /* set the global 'verbose' mode */
        gameMain.verboseMode.isOn = true;

        /* acknowledge it */
        gLibMessages.acknowledgeVerboseMode(true);
    }
;

/*
 *   Change to TERSE mode. 
 */
DefineSystemAction(Terse)
    execSystemAction()
    {
        /* set the global 'verbose' mode */
        gameMain.verboseMode.isOn = nil;

        /* acknowledge it */
        gLibMessages.acknowledgeVerboseMode(nil);
    }
;

/* in case the score module isn't present */
property showScore;
property showFullScore;
property scoreNotify;

/*
 *   Show the current score.
 */
DefineSystemAction(Score)
    execSystemAction()
    {
        /* show the simple score */
        if (libGlobal.scoreObj != nil)
        {
            /* show the score */
            libGlobal.scoreObj.showScore();

            /* 
             *   Mention the FULL SCORE command to the player if we haven't
             *   already.  Note that we only want to mention 
             */
            if (!mentionedFullScore)
            {
                /* explain about it */
                gLibMessages.mentionFullScore;

                /* don't mention it again */
                ScoreAction.mentionedFullScore = true;
            }
        }
        else
            gLibMessages.scoreNotPresent;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil

    /* have we mentioned the FULL SCORE command yet? */
    mentionedFullScore = nil
;

/*
 *   Show the full score. 
 */
DefineSystemAction(FullScore)
    execSystemAction()
    {
        /* show the full score in response to an explicit player request */
        showFullScore();

        /* this counts as a mention of the FULL SCORE command */
        ScoreAction.mentionedFullScore = true;
    }

    /* show the full score */
    showFullScore()
    {
        /* show the full score */
        if (libGlobal.scoreObj != nil)
            libGlobal.scoreObj.showFullScore();
        else
            gLibMessages.scoreNotPresent;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   Show the NOTIFY status. 
 */
DefineSystemAction(Notify)
    execSystemAction()
    {
        /* show the current notification status */
        if (libGlobal.scoreObj != nil)
            gLibMessages.showNotifyStatus(
                libGlobal.scoreObj.scoreNotify.isOn);
        else
            gLibMessages.commandNotPresent;
    }
;

/*
 *   Turn score change notifications on. 
 */
DefineSystemAction(NotifyOn)
    execSystemAction()
    {
        /* turn notifications on, and acknowledge the status */
        if (libGlobal.scoreObj != nil)
        {
            libGlobal.scoreObj.scoreNotify.isOn = true;
            gLibMessages.acknowledgeNotifyStatus(true);
        }
        else
            gLibMessages.commandNotPresent;
    }
;

/*
 *   Turn score change notifications off. 
 */
DefineSystemAction(NotifyOff)
    execSystemAction()
    {
        /* turn notifications off, and acknowledge the status */
        if (libGlobal.scoreObj != nil)
        {
            libGlobal.scoreObj.scoreNotify.isOn = nil;
            gLibMessages.acknowledgeNotifyStatus(nil);
        }
        else
            gLibMessages.commandNotPresent;
    }
;

/*
 *   Show version information for the game and the library modules the
 *   game is using.  
 */
DefineSystemAction(Version)
    execSystemAction()
    {
        /* show the version information for each library */
        foreach (local cur in ModuleID.getModuleList())
            cur.showVersion();
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   Show the credits for the game and the library modules the game
 *   includes. 
 */
DefineSystemAction(Credits)
    execSystemAction()
    {
        /* show the credits for each library */
        foreach (local cur in ModuleID.getModuleList())
            cur.showCredit();
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   Show the "about" information for the game and library modules.
 */
DefineSystemAction(About)
    execSystemAction()
    {
        local anyOutput;
        
        /* watch for any output while showing module information */
        anyOutput = outputManager.curOutputStream
                    .watchForOutput(new function()
        {
            /* show information for each module */
            foreach (local cur in ModuleID.getModuleList())
                cur.showAbout();
        });

        /* 
         *   if we didn't have any ABOUT information to show, display a
         *   message to this effect 
         */
        if (!anyOutput)
            gLibMessages.noAboutInfo;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

/*
 *   A state object that keeps track of our logging (scripting) status.
 *   This is transient, because logging is controlled through the output
 *   layer in the interpreter, which does not participate in any of the
 *   persistence mechanisms.  
 */
transient scriptStatus: object
    /*
     *   Script file name.  This is nil when logging is not in effect, and
     *   is set to the name of the scripting file when a log file is
     *   active. 
     */
    scriptFile = nil

    /* RECORD file name */
    recordFile = nil

    /* have we warned about using NOTE without logging in effect? */
    noteWithoutScriptWarning = nil
;

/*
 *   A base class for file-oriented actions, such as SCRIPT, RECORD, and
 *   REPLAY.  We provide common handling that prompts interactively for a
 *   filename; subclasses must override a few methods and properties to
 *   carry out the specific subclassed operation on the file.  
 */
DefineSystemAction(FileOp)
    /* our file dialog prompt message */
    filePromptMsg = ''

    /* the file dialog open/save type */
    fileDisposition = InFileSave

    /* the file dialog type ID */
    fileTypeID = FileTypeLog

    /* show our cancellation mesage */
    showCancelMsg = ""

    /* carry out our file operation */
    performFileOp(fname, ack)
    {
        /* 
         *   Each concrete action subclass must override this to carry out
         *   our operation.  This is called when the user has successfully
         *   selected a filename for the operatoin.  
         */
    }

    execSystemAction()
    {
        /* 
         *   ask for a file and carry out our action; since the command is
         *   being performed directly from the command line, we want an
         *   acknowledgment message on success 
         */
        setUpFileOp(true);
    }

    /* ask for a file, and carry out our operation is we get one */
    setUpFileOp(ack)
    {
        local result;
        local origElapsedTime;

        /* note the current game time */
        origElapsedTime = realTimeManager.getElapsedTime();

        /* ask for a file */
        result = getInputFile(filePromptMsg, fileDisposition, fileTypeID, 0);

        /* check the inputFile result */
        switch(result[1])
        {
        case InFileSuccess:
            /* carry out our file operation */
            performFileOp(result[2], ack);
            break;

        case InFileFailure:
            /* advise of the failure of the prompt */
            gLibMessages.filePromptFailed();
            break;

        case InFileCancel:
            /* acknowledge the cancellation */
            showCancelMsg();
            break;
        }

        /* 
         *   restore the original elapsed game time, so that the time spent
         *   in the file selector dialog doesn't count against the game
         *   time 
         */
        realTimeManager.setElapsedTime(origElapsedTime);
    }

    /* we can't include this in undo, as it affects external files */
    includeInUndo = nil
;

/*
 *   Turn scripting on.  This creates a text file that contains a
 *   transcript of all commands and responses from this point forward.
 */
DefineAction(Script, FileOpAction)
    /* our file dialog parameters - ask for a log file to save */
    filePromptMsg = (gLibMessages.getScriptingPrompt())
    fileTypeID = FileTypeLog
    fileDisposition = InFileSave

    /* show our cancellation mesasge */
    showCancelMsg() { gLibMessages.scriptingCanceled(); }

    /* 
     *   set up scripting - this can be used to set up scripting
     *   programmatically, in the course of carrying out another action 
     */
    setUpScripting(ack) { setUpFileOp(ack); }

    /* turn on scripting to the given file */
    performFileOp(fname, ack)
    {
        /* turn on logging */
        setLogFile(fname, LogTypeTranscript);

        /* remember that scripting is in effect */
        scriptStatus.scriptFile = fname;

        /* 
         *   forget any past warning that we've issued about NOTE without
         *   a script in effect; the next time scripting isn't active,
         *   we'll want to issue a new warning, since they might not be
         *   aware at that point that the scripting we're starting now has
         *   ended 
         */
        scriptStatus.noteWithoutScriptWarning = nil;

        /* note that logging is active, if acknowledgment is desired */
        if (ack)
            gLibMessages.scriptingOkay();
    }
;

/*
 *   Subclass of Script action taking a quoted string as part of the
 *   command syntax.  The grammar rule must set our fname_ property to a
 *   quotedStringPhrase subproduction. 
 */
DefineAction(ScriptString, ScriptAction)
    execSystemAction()
    {
        /* if there's a filename, we don't need to prompt */
        if (fname_ != nil)
        {
            /* set up scripting to the filename specified in the command */
            performFileOp(fname_.getStringText(), true);
        }
        else
        {
            /* there's no filename, so prompt as usual */
            inherited();
        }
    }
;

/*
 *   Turn scripting off.  This stops recording the game transcript started
 *   with the most recent SCRIPT command. 
 */
DefineSystemAction(ScriptOff)
    execSystemAction()
    {
        /* turn off scripting */
        turnOffScripting(true);
    }

    /* turn off scripting */
    turnOffScripting(ack)
    {
        /* if we're not in a script file, ignore it */
        if (scriptStatus.scriptFile == nil)
        {
            gLibMessages.scriptOffIgnored();
            return;
        }

        /* cancel scripting in the interpreter's output layer */
        setLogFile(nil, LogTypeTranscript);

        /* remember that scripting is no longer in effect */
        scriptStatus.scriptFile = nil;

        /* acknowledge the change, if desired */
        if (ack)
            gLibMessages.scriptOffOkay();
    }

    /* we can't include this in undo, as it affects external files */
    includeInUndo = nil
;

/*
 *   RECORD - this is similar to SCRIPT, but stores a file containing only
 *   the command input, not the output. 
 */
DefineAction(Record, FileOpAction)
    /* our file dialog parameters - ask for a log file to save */
    filePromptMsg = (gLibMessages.getRecordingPrompt())
    fileTypeID = FileTypeCmd
    fileDisposition = InFileSave

    /* show our cancellation mesasge */
    showCancelMsg() { gLibMessages.recordingCanceled(); }

    /* 
     *   set up recording - this can be used to set up scripting
     *   programmatically, in the course of carrying out another action 
     */
    setUpRecording(ack) { setUpFileOp(ack); }

    /* turn on recording to the given file */
    performFileOp(fname, ack)
    {
        /* turn on command logging */
        setLogFile(fname, LogTypeCommand);

        /* remember that recording is in effect */
        scriptStatus.recordFile = fname;

        /* note that logging is active, if acknowledgment is desired */
        if (ack)
            gLibMessages.recordingOkay();
    }
;

/* subclass of Record action taking a quoted string for the filename */
DefineAction(RecordString, RecordAction)
    execSystemAction()
    {
        /* set up scripting to the filename specified in the command */
        performFileOp(fname_.getStringText(), true);
    }
;

/*
 *   Turn command recording off.  This stops recording the command log
 *   started with the most recent RECORD command.  
 */
DefineSystemAction(RecordOff)
    execSystemAction()
    {
        /* turn off recording */
        turnOffRecording(true);
    }

    /* turn off recording */
    turnOffRecording(ack)
    {
        /* if we're not recording anything, ignore it */
        if (scriptStatus.recordFile == nil)
        {
            gLibMessages.recordOffIgnored();
            return;
        }

        /* cancel recording in the interpreter's output layer */
        setLogFile(nil, LogTypeCommand);

        /* remember that recording is no longer in effect */
        scriptStatus.recordFile = nil;

        /* acknowledge the change, if desired */
        if (ack)
            gLibMessages.recordOffOkay();
    }

    /* we can't include this in undo, as it affects external files */
    includeInUndo = nil
;

/*
 *   REPLAY - play back a command log previously recorded. 
 */
DefineAction(Replay, FileOpAction)
    /* our file dialog parameters - ask for a log file to save */
    filePromptMsg = (gLibMessages.getReplayPrompt())
    fileTypeID = FileTypeCmd
    fileDisposition = InFileOpen

    /* show our cancellation mesasge */
    showCancelMsg() { gLibMessages.replayCanceled(); }

    /* script flags passed to setScriptFile */
    scriptOptionFlags = 0

    /* replay the given file */
    performFileOp(fname, ack)
    {
        /* 
         *   Note that we're reading from the script file if desired.  Do
         *   this before opening the script, so that we display the
         *   acknowledgment even if we're in 'quiet' mode. 
         */
        if (ack)
            gLibMessages.inputScriptOkay(fname);

        /* activate the script file */
        setScriptFile(fname, scriptOptionFlags);
    }
;

/* subclass of Replay action taking a quoted string for the filename */
DefineAction(ReplayString, ReplayAction)
    execSystemAction()
    {
        /* 
         *   if there's a string, use the string as the filename;
         *   otherwise, inherit the default handling to ask for a filename 
         */
        if (fname_ != nil)
        {
            /* set up scripting to the filename specified in the command */
            performFileOp(fname_.getStringText(), true);
        }
        else
        {
            /* inherit the default handling to ask for a filename */
            inherited();
        }
    }
;


/* in case the footnote module is not present */
property showFootnote;

/*
 *   Footnote - this requires a numeric argument parsed via the
 *   numberPhrase production and assigned to the numMatch property.  
 */
DefineSystemAction(Footnote)
    execSystemAction()
    {
        /* ask the Footnote class to do the work */
        if (libGlobal.footnoteClass != nil)
            libGlobal.footnoteClass.showFootnote(numMatch.getval());
        else
            gLibMessages.commandNotPresent;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

property footnoteSettings;

/* base class for FOOTNOTES xxx commands */
DefineSystemAction(Footnotes)
    execSystemAction()
    {
        if (libGlobal.footnoteClass != nil)
        {
            /* set my footnote status in the global setting */
            libGlobal.footnoteClass.footnoteSettings.showFootnotes =
                showFootnotes;

            /* acknowledge it */
            gLibMessages.acknowledgeFootnoteStatus(showFootnotes);
        }
        else
            gLibMessages.commandNotPresent;
    }

    /* 
     *   the footnote status I set when this command is activated - this
     *   must be overridden by each subclass 
     */
    showFootnotes = nil
;

DefineAction(FootnotesFull, FootnotesAction)
    showFootnotes = FootnotesFull
;

DefineAction(FootnotesMedium, FootnotesAction)
    showFootnotes = FootnotesMedium
;

DefineAction(FootnotesOff, FootnotesAction)
    showFootnotes = FootnotesOff
;

DefineSystemAction(FootnotesStatus)
    execSystemAction()
    {
        /* show the current status */
        if (libGlobal.footnoteClass != nil)
            gLibMessages.showFootnoteStatus(libGlobal.footnoteClass.
                                            footnoteSettings.showFootnotes);
        else
            gLibMessages.commandNotPresent;
    }

    /* there's no point in including this in undo */
    includeInUndo = nil
;

DefineIAction(Inventory)
    execAction()
    {
        /* show the actor's inventory in the current mode */
        gActor.showInventory(inventoryMode == InventoryTall);
    }

    /* current inventory mode - start in 'wide' mode by default */
    inventoryMode = InventoryWide;
;

DefineIAction(InventoryTall)
    execAction()
    {
        /* set inventory mode to 'tall' */
        InventoryAction.inventoryMode = InventoryTall;

        /* run the inventory action */
        InventoryAction.checkAction();
        InventoryAction.execAction();
    }
;

DefineIAction(InventoryWide)
    execAction()
    {
        /* set inventory mode to 'wide' */
        InventoryAction.inventoryMode = InventoryWide;

        /* run the inventory action */
        InventoryAction.checkAction();
        InventoryAction.execAction();
    }
;

DefineIAction(Wait)
    execAction()
    {
        /* just show the "time passes" message */
        defaultReport(&timePassesMsg);
    }
;

DefineIAction(Look)
    execAction()
    {
        /* show the actor's current location in verbose mode */
        gActor.lookAround(true);
    }
;

DefineIAction(Sleep)
    execAction()
    {
        /* let the actor handle it */
        gActor.goToSleep();
    }
;

DefineTAction(Take)
    /* this is a basic inventory-management verb, so allow ALL with it */
    actionAllowsAll = true

    /* get the ALL list for the direct object */
    getAllDobj(actor, scopeList)
    {
        local locList;
        local dropLoc;
        local actorLoc;
        
        /*
         *   Include all of the objects that are directly in the actor's
         *   immediate container, the container's container, and so on out
         *   to the "drop destination" location (which is where things go
         *   when we DROP them, and is meant to represent the nearest
         *   platform-like or floor-like container).  Also include anything
         *   that's directly in anything fixed in place within one of these
         *   containers.  Don't include anything that actually contains the
         *   actor, since we normally can't pick up something we're inside.
         *   
         *   Start by getting the actor's immediate location and drop
         *   destination location.  
         */
        actorLoc = actor.location;
        dropLoc = actor.getDropDestination(nil, nil);

        /* 
         *   create a vector to hold the location list, and start it off
         *   with the drop location 
         */
        locList = new Vector(10);
        locList.append(dropLoc);

        /* now work up the location list until we hit the drop location */
        for (local cur = actorLoc ; cur != nil && cur != dropLoc ;
             cur = cur.location)
        {
            /* add this container to the list */
            locList.append(cur);
        }

        /* 
         *   now generate the subset of in-scope objects that are directly
         *   in any of these locations (or directly in items fixed in place
         *   within any of these locations), and return the result 
         */
        return scopeList.subset(
            {x: (locList.indexWhich(
                {loc: x.isDirectlyIn(loc) || x.isInFixedIn(loc)}) != nil
                 && !actor.isIn(x)) });
    }
;

DefineTIAction(TakeFrom)
    /* this is a basic inventory-management verb, so allow ALL with it */
    actionAllowsAll = true

    /* get the ALL list for the direct object */
    getAllDobj(actor, scopeList)
    {
        /* ask the indirect object for the list */
        return getIobj() == nil
            ? []
            : getIobj().getAllForTakeFrom(scopeList);
    }
;

DefineTAction(Remove)
;

DefineTAction(Drop)
    /* this is a basic inventory-management verb, so allow ALL with it */
    actionAllowsAll = true

    /* get the ALL list for the direct object */
    getAllDobj(actor, scopeList)
    {
        /* include only objects directly held by the actor */
        return scopeList.subset({x: x.isDirectlyIn(actor)});
    }
;

DefineTAction(Examine)
;

DefineTAction(Read)
;

DefineTAction(LookIn)
;

DefineTAction(Search)
;

DefineTAction(LookUnder)
;

DefineTAction(LookBehind)
;

DefineTAction(LookThrough)
;

DefineTAction(Feel)
;

DefineTAction(Taste)
;

DefineTAction(Smell)
;

DefineTAction(ListenTo)
;

/*
 *   Base class for undirected sensing, such as "listen" or "smell" with no
 *   object.  We'll scan for things that have a presence in the
 *   corresponding sense and describe each one.  
 */
DefineIAction(SenseImplicit)
    /* the sense in which I operate */
    mySense = nil

    /* the object property to display this sense's description */
    descProp = nil
    
    /* the default message to display if we find nothing specific to sense */
    defaultMsgProp = nil

    /* the Lister we use to show the items */
    resultLister = nil

    /* execute the action */
    execAction()
    {
        local senseTab;
        local presenceList;
            
        /* get a list of everything in range of this sense for the actor */
        senseTab = gActor.senseInfoTable(mySense);

        /* get a list of everything with a presence in this sense */
        presenceList = senseInfoTableSubset(senseTab,
            {obj, info: obj.(mySense.presenceProp)});

        /* 
         *   if there's anything in the list, show it; otherwise, show a
         *   default report 
         */
        if (presenceList.length() != 0)
        {
            /* show the list using our lister */
            resultLister.showList(gActor, nil, presenceList, 0, 0,
                                  senseTab, nil);
        }
        else
        {
            /* there's nothing to show - say so */
            defaultReport(defaultMsgProp);
        }
    }
;

DefineAction(SmellImplicit, SenseImplicitAction)
    mySense = smell
    descProp = &smellDesc
    defaultMsgProp = &nothingToSmellMsg
    resultLister = smellActionLister
;

DefineAction(ListenImplicit, SenseImplicitAction)
    mySense = sound
    descProp = &soundDesc
    defaultMsgProp = &nothingToHearMsg
    resultLister = listenActionLister
;

DefineTIAction(PutIn)
    /* this is a basic inventory-management verb, so allow ALL with it */
    actionAllowsAll = true

    /* get the ALL list for the direct object */
    getAllDobj(actor, scopeList)
    {
        local loc;
        local iobj = nil;
        local iobjIdent = nil;

        /* get the actor's location */
        loc = actor.location;

        /* if we have an iobj list, retrieve its first element */
        if (iobjList_ != nil && iobjList_.length() > 0)
        {
            iobj = iobjList_[1].obj_;
            iobjIdent = iobj.getIdentityObject();
        }
        
        /*
         *   Include objects that are directly in the actor's location, or
         *   within fixed items in the actor's location, or directly in the
         *   actor's inventory.
         *   
         *   Exclude the indirect object and its "identity" object (since
         *   we obviously can't put the indirect object in itself), and
         *   exclude everything already directly in the indirect object.  
         */
        return scopeList.subset({x:
                                (x.isDirectlyIn(loc)
                                 || x.isInFixedIn(loc)
                                 || x.isDirectlyIn(actor))
                                && x != iobj
                                && x != iobjIdent
                                && !x.isDirectlyIn(iobj)});
    }
;

DefineTIAction(PutOn)
    /* this is a basic inventory-management verb, so allow ALL with it */
    actionAllowsAll = true

    /* get the ALL list for the direct object */
    getAllDobj(actor, scopeList)
    {
        /* use the same strategy that we do in PutIn */
        local loc = actor.location;
        return scopeList.subset({x:
                                (x.isDirectlyIn(loc)
                                 || x.isInFixedIn(loc)
                                 || x.isDirectlyIn(actor))
                                && x != getIobj()
                                && !x.isDirectlyIn(getIobj())});
    }
;

DefineTIAction(PutUnder)
;

DefineTIAction(PutBehind)
;

DefineTAction(Wear)
;

DefineTAction(Doff)
;

DefineConvTopicTAction(AskFor, IndirectObject)
;

DefineConvTopicTAction(AskAbout, IndirectObject)
;

DefineConvTopicTAction(TellAbout, IndirectObject)
    /*
     *   TELL ABOUT is a conversational address, as opposed to an order,
     *   if the direct object of the action is the same as the issuer: in
     *   this case, the command has the form <actor>, TELL ME ABOUT
     *   <topic>, which has exactly the same meaning as ASK <actor> ABOUT
     *   <topic>. 
     */
    isConversational(issuer)
    {
        local dobj;
        
        /* 
         *   if the resolved direct object matches the issuer, it's
         *   conversational 
         */
        dobj = getResolvedDobjList();
        return (dobj.length() == 1 && dobj[1] == issuer);
    }
;

/*
 *   AskVague and TellVague are for syntactically incorrect phrasings that
 *   a player might accidentally type, especially in conjunction with a
 *   past SpecialTopic prompt; in English, for example, we define these as
 *   ASK <actor> <topic> and TELL <actor> <topic>.  These are used only to
 *   provide more helpful error messages.  
 */
DefineTopicTAction(AskVague, IndirectObject)
;
DefineTopicTAction(TellVague, IndirectObject)
;

DefineConvIAction(Hello)
    execAction()
    {
        /* the issuing actor is saying hello to the target actor */
        gIssuingActor.sayHello(gActor);
    }
;

DefineConvIAction(Goodbye)
    execAction()
    {
        /* the issuing actor is saying goodbye to the target actor */
        gIssuingActor.sayGoodbye(gActor);
    }
;

DefineConvIAction(Yes)
    execAction()
    {
        /* the issuing actor is saying yes to the target actor */
        gIssuingActor.sayYes(gActor);
    }
;

DefineConvIAction(No)
    execAction()
    {
        /* the issuing actor is saying no to the target actor */
        gIssuingActor.sayNo(gActor);
    }
;

/*
 *   Invoke the active SpecialTopic.  This isn't a real command - the
 *   player will never actually type this; rather, it's a pseudo-command
 *   that we send to ourselves from a string pre-parser when we recognize
 *   input that matches a SpecialTopic's custom command syntax.
 *   
 *   Note that we actually define the syntax for this command right here
 *   in the language-independent library, because this isn't a real
 *   command.  The user never needs to type this command, since it's
 *   something we generate internally.  The only important language issue
 *   is that we use a command keyword that no language will ever want to
 *   use for a real command, so we intentionally use some near-English
 *   gibberish.  
 */
DefineLiteralAction(SpecialTopic)
    execAction()
    {
        /* 
         *   the issuing actor is saying the current special topic to the
         *   actor's current interlocutor 
         */
        gIssuingActor.saySpecialTopic();
    }

    /* 
     *   Repeat the action, for an AGAIN command.  We need to make sure
     *   the special text interpretation we gave to the command still
     *   holds; if not, reparse the original text and try that.  
     */
    repeatAction(lastTargetActor, lastTargetActorPhrase,
                 lastIssuingActor, countsAsIssuerTurn)
    {
        local cmd;

        /* get the original text the player entered */
        cmd = getEnteredText();
        
        /* 
         *   try running this through the special topic pre-parser again,
         *   to see if it still has the special meaning 
         */
        if (specialTopicPreParser.doParsing(cmd, rmcCommand)
            .startsWith('xspcltopic '))
        {
            /* 
             *   it still has the special meaning, so simply execute as we
             *   normally would, by inheriting the standard Action
             *   handling 
             */
            inherited(lastTargetActor, lastTargetActorPhrase,
                      lastIssuingActor, countsAsIssuerTurn);
        }
        else
        {
            /*
             *   The command no longer has the special meaning it did on
             *   the last command, so we can't repeat this command.  
             */
            gLibMessages.againNotPossible(lastIssuingActor);
        }
    }

    /*
     *   Get the original player-entered text.  This is our literal
     *   phrase, with the embedded-quote encoding decoded.
     */
    getEnteredText() { return decodeOrig(getLiteral()); }

    /* 
     *   encode the original text for our literal phrase: turn double
     *   quotes into '%q' sequences, and turn percent signs into '%%'
     *   sequences 
     */
    encodeOrig(txt)
    {
        /* first, replace any percent signs with '%%' sequences */
        txt = txt.findReplace('%', '%%', ReplaceAll);

        /* next, replace any double quotes with '%q' sequences */
        txt = txt.findReplace('"', '%q', ReplaceAll);

        /* return the text */
        return txt;
    }

    /* decode our encoding */
    decodeOrig(txt)
    {
        /* find each '%' sequence and decode it */
        for (local idx = 1 ; idx <= txt.length() ; )
        {
            /* find the next '%' */
            idx = txt.find('%', idx);

            /* if we found nothing, we're done */
            if (idx == nil)
                break;

            /* 
             *   if it's the last character, ignore it, as all of our
             *   encodings are two characters long 
             */
            if (idx == txt.length())
                break;

            /* check what we have next */
            switch (txt.substr(idx + 1, 1))
            {
            case 'q':
                /* it's a quote */
                txt = txt.findReplace('%q', '"', ReplaceOnce, idx);
                break;

            case '%':
                /* it's a single percent sign */
                txt = txt.findReplace('%%', '%', ReplaceOnce, idx);
                break;
            }

            /* continue searching from the next character */
            ++idx;
        }

        /* return the result */
        return txt;
    }
;

grammar predicate(SpecialTopic):
    'xspcltopic' literalPhrase->literalMatch
    : SpecialTopicAction

    /*
     *   Use the text of the command as originally typed by the player as
     *   our apparent original text.  
     */
    getOrigText() { return getEnteredText(); }
;

/* in case they try typing just 'xspcltopic' */
grammar predicate(EmptySpecialTopic):
    'xspcltopic' : IAction

    /* just act like we don't understand this command */
    execAction() { gLibMessages.commandNotPresent; }
;

DefineTAction(Kiss)
;

DefineIAction(Yell)
    execAction()
    {
        /* yelling generally has no effect; issue a default response */
        mainReport(&okayYellMsg);
    }
;

DefineTAction(TalkTo)
;

DefineSystemAction(Topics)
    execSystemAction()
    {
        /* check to see if any suggestions are defined in the entire game */
        if (firstObj(SuggestedTopic, ObjInstances) != nil)
        {
            /* we have topics - let the actor handle it */
            gActor.suggestTopics(true);
        }
        else
        {
            /* there are no topics at all, so this command isn't used */
            gLibMessages.commandNotPresent;
        }
    }

    /* don't include this in undo */
    includeInUndo = nil
;

DefineTIAction(GiveTo)
    getDefaultIobj(np, resolver)
    {
        local obj;

        /* check the actor for a current interlocutor */
        obj = resolver.getTargetActor().getCurrentInterlocutor();
        if (obj != nil)
            return [new ResolveInfo(obj, 0)];
        else
            return inherited(np, resolver);
    }
;

DefineTIAction(ShowTo)
    getDefaultIobj(np, resolver)
    {
        local obj;

        /* check the actor for a current interlocutor */
        obj = resolver.getTargetActor().getCurrentInterlocutor();
        if (obj != nil)
            return [new ResolveInfo(obj, 0)];
        else
            return inherited(np, resolver);
    }
;

DefineTAction(Follow)
    /*
     *   For resolving our direct object, we want to include in the scope
     *   any item that isn't present but which the actor saw departing the
     *   present location.  
     */
    initResolver(issuingActor, targetActor)
    {
        /* inherit the base resolver initialization */
        inherited(issuingActor, targetActor);

        /* 
         *   add to the scope all of the actor's followable objects -
         *   these are the objects which the actor has witnessed leaving
         *   the actor's present location 
         */
        scope_ += targetActor.getFollowables();
    }
;

DefineTAction(Attack)
;

DefineTIAction(AttackWith)
    /* 
     *   for the indirect object, limit 'all' and defaults to the items in
     *   inventory 
     */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineTAction(Throw)
;

DefineTAction(ThrowDir)
    /* get the direction of the throwing (as a Direction object) */
    getDirection() { return dirMatch.dir; }
;

DefineTIAction(ThrowAt)
;

DefineTIAction(ThrowTo)
;

DefineTAction(Dig)
;

DefineTIAction(DigWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineIAction(Jump)
    preCond = [actorStanding]
    execAction()
    {
        /* show the default report for jumping in place */
        mainReport(&okayJumpMsg);
    }
;

DefineTAction(JumpOver)
;

DefineTAction(JumpOff)
;

DefineIAction(JumpOffI)
    execAction()
    {
        mainReport(&cannotJumpOffHereMsg);
    }
;

DefineTAction(Push)
;

DefineTAction(Pull)
;

DefineTAction(Move)
;

DefineTIAction(MoveWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineTIAction(MoveTo)
;

DefineTAction(Turn)
;

DefineTIAction(TurnWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineLiteralTAction(TurnTo, IndirectObject)
;

DefineTAction(Set)
;

DefineLiteralTAction(SetTo, IndirectObject)
;

DefineTAction(TypeOn)
;

DefineLiteralTAction(TypeLiteralOn, DirectObject)
;

DefineLiteralTAction(EnterOn, DirectObject)
;

DefineTAction(Consult)
;

DefineTopicTAction(ConsultAbout, IndirectObject)
    getDefaultDobj(np, resolver)
    {
        local actor;
        local obj;
        
        /* 
         *   if the actor has consulted something before, and that object
         *   is still visible, use it as the default this consultation 
         */
        actor = resolver.getTargetActor();
        obj = actor.lastConsulted;
        if (obj != nil && actor.canSee(obj))
            return [new ResolveInfo(obj, DefaultObject)];
        else
            return inherited(np, resolver);
    }

    /*
     *   Filter the topic phrase resolution.  If we know our direct object,
     *   and it's a Consultable, refer the resolution to the Consultable.  
     */
    filterTopic(lst, np, resolver)
    {
        local dobj;
        
        /* check the direct object */
        if (dobjList_ != nil
            && dobjList_.length() == 1
            && (dobj = dobjList_[1].obj_).ofKind(Consultable))
        {
            /* 
             *   we have a Consultable direct object - let it handle the
             *   topic phrase resolution 
             */
            return dobj.resolveConsultTopic(lst, topicMatch, resolver);
        }
        else
        {
            /* otherwise, use the default handling */
            return inherited(lst, np, resolver);
        }
    }
;

DefineTAction(Switch)
;

DefineTAction(Flip)
;

DefineTAction(TurnOn)
;

DefineTAction(TurnOff)
;

DefineTAction(Light)
;

DefineTAction(Burn)
;

DefineTIAction(BurnWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }

    /* resolve the direct object first */
    resolveFirst = DirectObject
;

DefineTAction(Extinguish)
;

DefineTIAction(AttachTo)
;

DefineTIAction(DetachFrom)
;

DefineTAction(Detach)
;

DefineTAction(Break)
;

DefineTAction(Cut)
;

DefineTIAction(CutWith)
;

DefineTAction(Climb)
;

DefineTAction(ClimbUp)
;

DefineTAction(ClimbDown)
;

DefineTAction(Open)
;

DefineTAction(Close)
;

DefineTAction(Lock)
;

DefineTAction(Unlock)
;

DefineTIAction(LockWith)
    /* 
     *   Resolve the direct object (the lock) first, so that we know what
     *   we're trying to unlock when we're verifying the key.  This allows
     *   us to (optionally) boost the likelihood of a known good key for
     *   disambiguation. 
     */
    resolveFirst = DirectObject

    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineTIAction(UnlockWith)
    /* resolve the direct object first, for the same reason as in LockWith */
    resolveFirst = DirectObject

    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineTAction(Eat)
;

DefineTAction(Drink)
;

DefineTAction(Pour)
;

DefineTIAction(PourInto)
;

DefineTIAction(PourOnto)
;

DefineTAction(Clean)
;

DefineTIAction(CleanWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineIAction(Sit)
    execAction()
    {
        /* 
         *   if the actor is already sitting, just say so; otherwise, ask
         *   what they want to sit on 
         */
        if (gActor.posture == sitting)
            reportFailure(&alreadySittingMsg);
        else
            askForDobj(SitOn);
    }
;

DefineTAction(SitOn)
;

DefineIAction(Lie)
    execAction()
    {
        /* 
         *   if the actor is already lying down, just say so; otherwise,
         *   ask what they want to lie on 
         */
        if (gActor.posture == lying)
            reportFailure(&alreadyLyingMsg);
        else
            askForDobj(LieOn);
    }
;

DefineTAction(LieOn)
;

DefineTAction(StandOn)
;

DefineIAction(Stand)
    execAction()
    {
        /* let the actor handle it */
        gActor.standUp();
    }
;

DefineTAction(Board)
;

DefineTAction(GetOutOf)
    getAllDobj(actor, scopeList)
    {
        /* 'all' for 'get out of' is the actor's immediate container */
        return scopeList.subset({x: actor.isDirectlyIn(x)});
    }
;

DefineTAction(GetOffOf)
    getAllDobj(actor, scopeList)
    {
        /* 'all' for 'get off of' is the actor's immediate container */
        return scopeList.subset({x: actor.isDirectlyIn(x)});
    }
;

DefineIAction(GetOut)
    execAction()
    {
        /* let the actor handle it */
        gActor.disembark();
    }
;

DefineTAction(Fasten)
;

DefineTIAction(FastenTo)
;

DefineTAction(Unfasten)
;

DefineTIAction(UnfastenFrom)
;

DefineTAction(PlugIn)
;

DefineTIAction(PlugInto)
;

DefineTAction(Unplug)
;

DefineTIAction(UnplugFrom)
;

DefineTAction(Screw)
;

DefineTIAction(ScrewWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

DefineTAction(Unscrew)
;

DefineTIAction(UnscrewWith)
    /* limit 'all' for the indirect object to items in inventory */
    getAllIobj(actor, scopeList)
    {
        return scopeList.subset({x: x.isIn(actor)});
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Travel Action - this is the base class for verbs that attempt to move
 *   an actor to a new location via one of the directional connections
 *   from the current location.
 *   
 *   Each grammar rule for this action must set the 'dirMatch' property to
 *   a DirectionProd match object that gives the direction.  
 */
DefineIAction(Travel)
    execAction()
    {
        local conn;
        
        /* 
         *   Perform the travel via the connector, if we have one.  If
         *   there's no connector defined for this direction, show a
         *   default "you can't go that way" message. 
         */
        if ((conn = getConnector()) != nil)
        {
            /* 
             *   we have a connector - use the pseudo-action TravelVia with
             *   the connector to carry out the travel 
             */
            replaceAction(TravelVia, conn);
        }
        else
        {
            /* no connector - show a default "can't go that way" error */
            mainReport(&cannotGoThatWayMsg);
        }
    }

    /* get the direction object for the travel */
    getDirection() { return dirMatch != nil ? dirMatch.dir : nil; }

    /*
     *   Get my travel connector.  My connector is given by the travel
     *   link property for this action as defined in the actor's current
     *   location. 
     */
    getConnector()
    {
        /* ask the location for the connector in my direction */
        return gActor.location == nil
            ? nil
            : gActor.location.getTravelConnector(getDirection(), gActor);
    }

    /*
     *   The grammar rules for the individual directions will usually just
     *   create a base TravelAction object, rather than one of the
     *   direction-specific subclasses (NorthAction, etc).  For
     *   convenience in testing the action, though, treat ourself as
     *   matching the subclass with the same direction.  
     */
    actionOfKind(cls)
    {
        /* 
         *   If they're asking about a specific-direction TravelAction
         *   subclass, then we match it if our own direction matches that
         *   of the given subclass, and we fail to match if our direction
         *   doesn't match the given direction.  
         */
        if (cls.ofKind(TravelAction) && cls.getDirection() != nil)
        {
            /* we match if and only if the direction matches */
            return (getDirection() == cls.getDirection());
        }

        /* otherwise, inherit the default handling */
        return inherited(cls);
    }
;

/* for a vague command such as GO, which doesn't say where to go */
DefineIAction(VagueTravel)
    execAction()
    {
        /* simply ask for a direction */
        reportFailure(&whereToGoMsg);
    }
;

/*
 *   This class makes it convenient to synthesize a TravelAction given a
 *   Direction object.  
 */
DefineAction(TravelDir, TravelAction)
    construct(dir)
    {
        /* remember my direction */
        dir_ = dir;
    }
    
    /* get my direction */
    getDirection() { return dir_; }
    
    /* my direction, normally specified during construction */
    dir_ = nil
;

/*
 *   To make it more convenient to use directional travel actions as
 *   synthesized commands, define a set of action classes for the specific
 *   directions.  
 */
DefineAction(North, TravelAction)
    getDirection = northDirection
;
DefineAction(South, TravelAction)
    getDirection = southDirection
;
DefineAction(East, TravelAction)
    getDirection = eastDirection
;
DefineAction(West, TravelAction)
    getDirection = westDirection
;
DefineAction(Northeast, TravelAction)
    getDirection = northeastDirection
;
DefineAction(Northwest, TravelAction)
    getDirection = northwestDirection
;
DefineAction(Southeast, TravelAction)
    getDirection = southeastDirection
;
DefineAction(Southwest, TravelAction)
    getDirection = southwestDirection
;
DefineAction(In, TravelAction)
    getDirection = inDirection
;
DefineAction(Out, TravelAction)
    getDirection = outDirection
;
DefineAction(Up, TravelAction)
    getDirection = upDirection
;
DefineAction(Down, TravelAction)
    getDirection = downDirection
;
DefineAction(Fore, TravelAction)
    getDirection = foreDirection
;
DefineAction(Aft, TravelAction)
    getDirection = aftDirection
;
DefineAction(Port, TravelAction)
    getDirection = portDirection
;
DefineAction(Starboard, TravelAction)
    getDirection = starboardDirection
;

/*
 *   Non-directional travel actions 
 */

DefineTAction(GoThrough)
;

DefineTAction(Enter)
;

/*
 *   An internal action for traveling via a connector.  This isn't a real
 *   action, and shouldn't have a grammar defined for it.  The purpose of
 *   this action is to allow real actions that cause travel via a
 *   connector to be implemented by mapping to this internal action, which
 *   we implement on the base travel connector class.  
 */
DefineTAction(TravelVia)
    /* 
     *   The direct object of this synthetic action isn't necessarily an
     *   ordinary simulation object: it could be a TravelConnector
     *   instead.  Don't return it in the list of objects to the command.
     *   This synthetic action doesn't necessarily involve any simulation
     *   objects.  
     */
    getCurrentObjects = []
;

/* "go back" */
DefineIAction(GoBack)
    execAction()
    {
        /* ask the actor to handle it */
        gActor.reverseLastTravel();
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Combined pushing-and-traveling action ("push crate north", "drag sled
 *   into cave").  All of these are based on a base action class, which
 *   defines the methods invoked on the object being pushed; the
 *   subclasses provide a definition of the connector that determines
 *   where the travel takes us.  
 */
DefineTAction(PushTravel)
    /*
     *   Carry out the nested travel action for the special combination
     *   push-traveler.  This should carry out the same action we would
     *   have performed for the underlying basic travel.
     *   
     *   This method is invoked by the TravelPushable to carry out a
     *   push-travel action.  The TravelPushable object will first set up
     *   a PushTraveler as the actor's global traveler, and it will then
     *   invoke this method to carry out the actual travel with that
     *   special traveler in effect.  Our job is to provide the mapping to
     *   the correct underlying simple travel action; since we'll be
     *   moving the PushTraveler object, we can move it using the ordinary
     *   non-push travel action as though it were any other traveler.
     *   
     *   This method is abstract - each subclass must define it
     *   appropriately.  
     */
    // performTravel() { }
;

/*
 *   For directional push-and-travel commands, we define a common base
 *   class that does the work to find the connector based on the room's
 *   directional connector.
 *   
 *   Subclasses for grammar rules must define the 'dirMatch' property to
 *   be a DirectionProd object for the associated direction.  
 */
DefineAction(PushTravelDir, PushTravelAction)
    /*
     *   Get the direction we're going.  By default, we return the
     *   direction associated with the dirMatch match object from our
     *   grammar match.  
     */
    getDirection() { return dirMatch.dir; }

    /* carry out the nested travel action for a PushTravel */
    performTravel()
    {
        local conn;
        
        /* ask the actor's location for the connector in our direction */
        conn = gActor.location.getTravelConnector(getDirection(), gActor);

        /* perform a nested TravelVia on the connector */
        nestedAction(TravelVia, conn);
    }
;

/*
 *   To make it easy to synthesize actions for pushing objects, define
 *   individual subclasses for the various directions.
 */
DefineAction(PushNorth, PushTravelDirAction)
    getDirection = northDirection
;

DefineAction(PushSouth, PushTravelDirAction)
    getDirection = southDirection
;

DefineAction(PushEast, PushTravelDirAction)
    getDirection = eastDirection
;

DefineAction(PushWest, PushTravelDirAction)
    getDirection = westDirection
;

DefineAction(PushNorthwest, PushTravelDirAction)
    getDirection = northwestDirection
;

DefineAction(PushNortheast, PushTravelDirAction)
    getDirection = northeastDirection
;

DefineAction(PushSouthwest, PushTravelDirAction)
    getDirection = southwestDirection
;

DefineAction(PushSoutheast, PushTravelDirAction)
    getDirection = southeastDirection
;

DefineAction(PushUp, PushTravelDirAction)
    getDirection = upDirection
;

DefineAction(PushDown, PushTravelDirAction)
    getDirection = downDirection
;

DefineAction(PushIn, PushTravelDirAction)
    getDirection = inDirection
;

DefineAction(PushOut, PushTravelDirAction)
    getDirection = outDirection
;

DefineAction(PushFore, PushTravelDirAction)
    getDirection = foreDirection
;

DefineAction(PushAft, PushTravelDirAction)
    getDirection = aftDirection
;

DefineAction(PushPort, PushTravelDirAction)
    getDirection = portDirection
;

DefineAction(PushStarboard, PushTravelDirAction)
    getDirection = starboardDirection
;

/*
 *   Base class for two-object push-travel commands, such as "push boulder
 *   out of cave" or "drag sled up hill".  For all of these, the connector
 *   is given by the indirect object.
 */
DefineAction(PushTravelViaIobj, TIAction, PushTravelAction)
    /*
     *   Verify the indirect object of the push-travel action.  We'll
     *   remap this to given corresponding simple travel action, and call
     *   that action's verifier.  
     */
    verifyPushTravelIobj(obj, action)
    {
        /* handle this by remapping it to the underlying simple action */
        remapVerify(IndirectObject, gVerifyResults, [action, obj]);
    }
;

DefineTIActionSub(PushTravelThrough, PushTravelViaIobjAction)
    /* 
     *   Carry out the underlying simple travel action.  This simply
     *   performs a GoThrough on my indirect object, as though we had
     *   typed simply GO THROUGH iobj.  The PushTraveler will already be
     *   set up as the actor's special traveler, so the ordinary GO
     *   THROUGH command will move the special PushTraveler object as
     *   though it were the original actor.  
     */
    performTravel() { nestedAction(GoThrough, getIobj()); }
;

DefineTIActionSub(PushTravelEnter, PushTravelViaIobjAction)
    /* carry out the underlying simple travel as an ENTER action */
    performTravel() { nestedAction(Enter, getIobj()); }
;

DefineTIActionSub(PushTravelGetOutOf, PushTravelViaIobjAction)
    /* carry out the underlying simple travel as a GET OUT OF action */
    performTravel() { nestedAction(GetOutOf, getIobj()); }
;

DefineTIActionSub(PushTravelClimbUp, PushTravelViaIobjAction)
    /* carry out the underlying simple travel as a CLIMB UP action */
    performTravel() { nestedAction(ClimbUp, getIobj()); }
;

DefineTIActionSub(PushTravelClimbDown, PushTravelViaIobjAction)
    /* carry out the underlying simple travel as an CLIMB DOWN action */
    performTravel() { nestedAction(ClimbDown, getIobj()); }
;

/*
 *   The "exits" verb.  This verb explicitly shows all of the exits from
 *   the current location. 
 */
DefineIAction(Exits)
    execAction()
    {
        /* 
         *   if we have an exit lister object, invoke it; otherwise,
         *   explain that this command isn't supported in this game 
         */
        if (gExitLister != nil)
            gExitLister.showExitsCommand();
        else
            gLibMessages.commandNotPresent;
    }
;

/* in case the exits module isn't included */
property showExitsCommand, exitsOnOffCommand;

/*
 *   Change the exit display mode.  The grammar must set one of the mode
 *   token properties to a non-nil value, according to which mode the
 *   player selected: on_ for turning on statusline and description lists;
 *   stat_ for turning on only the statusline list; look_ for turning on
 *   only the room description list; and off_ for turning off everything.  
 */
DefineSystemAction(ExitsMode)
    execSystemAction()
    {
        local stat, look;

        /* 
         *   If it's EXITS ON, turn on both statusline and room description
         *   lists.  If it's EXITS LOOK or EXITS STATUS, just turn on one
         *   or the other.  Otherwise, turn both off. 
         */
        stat = (stat_ != nil || on_ != nil);
        look = (look_ != nil || on_ != nil);

        /* update the exit display */
        if (gExitLister != nil)
            gExitLister.exitsOnOffCommand(stat, look);
        else
            gLibMessages.commandNotPresent;
    }
;

/*
 *   Dummy OOPS action for times when OOPS isn't in context.  We'll simply
 *   explain how OOPS works, and that you can't use it right now.  
 */
DefineLiteralAction(Oops)
    execAction()
    {
        /* simply explain how this command works */
        gLibMessages.oopsOutOfContext;
    }

    /* this is a meta-command, so don't consume any time */
    actionTime = 0
;

/* intransitive form of "oops" */
DefineIAction(OopsI)
    doActionMain()
    {
        /* as with OOPS with a literal, simply explain the problem */
        gLibMessages.oopsOutOfContext;
    }

    /* this is a meta-command, so don't consume any time */
    actionTime = 0
;

property disableHints, showHints;

/* hint system - disable hints for this session */
DefineSystemAction(HintsOff)
    execSystemAction()
    {
        if (gHintManager != nil)
            gHintManager.disableHints();
        else
            mainReport(gLibMessages.hintsNotPresent);
    }
;

/* invoke hint system */
DefineSystemAction(Hint)
    execSystemAction()
    {
        if (gHintManager != nil)
            gHintManager.showHints();
        else
            mainReport(gLibMessages.hintsNotPresent);
    }
;

/* ------------------------------------------------------------------------ */
/*
 *   Parser debugging verbs 
 */

#ifdef PARSER_DEBUG

DefineIAction(ParseDebug)
    execAction()
    {
        local newMode;

        /* 
         *   get the mode - if the mode is explicitly stated in the
         *   command, use the stated new mode, otherwise invert the current
         *   mode 
         */
        newMode = (onOrOff_ == 'on'
                   ? true
                   : onOrOff_ == 'off'
                   ? nil
                   : !libGlobal.parserDebugMode);

        /* set the new mode */
        libGlobal.parserDebugMode = newMode;

        /* mention the change */
        "Parser debugging is now
        <<libGlobal.parserDebugMode ? 'on' : 'off'>>.\n";
    }
;

grammar predicate(ParseDebug):
    'parse-debug' 'on'->onOrOff_
    | 'parse-debug' 'off'->onOrOff_
    | 'parse-debug'
    : ParseDebugAction
;

#endif

