input.t

documentation
#charset "us-ascii"
#include "advlite.h"

/*
 *   ************************************************************************
 *   actor.t This module forms part of the adv3Lite library
 *.  (c) 2012-13 Eric Eve.
 *.  Based substantially on input.t in the adv3 library
 *.  Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved.
 *
 *
 *
 *   This modules defines functions and objects related to reading input from
 *   the player.
 */



/* ------------------------------------------------------------------------ */
/*
 *   Keyboard input parameter definition. 
 */
class InputDef: object
    /* 
     *   The prompt function.  This is a function pointer (which is
     *   frequently given as an anonymous function) or nil; if it's nil,
     *   we won't show any prompt at all, otherwise we'll call the
     *   function pointer to display a prompt as needed. 
     */
    promptFunc = nil

    
    /* 
     *   Begin the input style.  This should do anything required to set
     *   the font to the desired attributes for the input text.  By
     *   default, we'll simply display <.inputline> to set up the default
     *   input style.  
     */
    beginInputFont() { "<.inputline>"; }

    /* 
     *   End the input style.  By default, we'll close the <.inputline>
     *   that we opened in beginInputFont(). 
     */
    endInputFont() { "<./inputline>"; }
;

/*
 *   Basic keyboard input parameter definition.  This class defines
 *   keyboard input parameters with the real-time status and prompt
 *   function specified via the constructor.  
 */
class BasicInputDef: InputDef
    construct(promptFunc)   
    {
        self.promptFunc = promptFunc;
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   Keyboard input manager. 
 */
inputManager: PostRestoreObject
    /*
     *   Read a line of input from the keyboard.
     *
     *   promptFunc can either be a callback function to invoke to display the
     *   prompt, or a single-quoted string containing the prompt. Of course, the
     *   caller can simply display the prompt before calling this routine rather
     *   than passing in a prompt callback, if desired.
     *
     *   If we're in HTML mode, this will switch into the 'tads-input' font
     *   while reading the line, so this routine should be used wherever
     *   possible rather than calling inputLine() or inputLineTimeout()
     *   directly.
     */
    getInputLine(promptFunc?)
    {
        /* read input using a basic InputDef for the given parameters */
        return getInputLineExt(new BasicInputDef(promptFunc));
    }

    /*
     *   Read a line of input from the keyboard - extended interface,
     *   using the InputDef object to define the input parameters.
     *   'defObj' is an instance of class InputDef, defining how we're to
     *   handle the input.  
     */
    getInputLineExt(defObj)
    {

        /* 
         *   If a previous input was in progress, cancel it - this must be
         *   a recursive entry from a real-time event that's interrupting
         *   the enclosing input attempt. Simply cancel out the enclosing
         *   read attempt entirely in this case; if and when we return to
         *   the enclosing reader, that reader will start over with a
         *   fresh read attempt at that point.  
         */
        cancelInputInProgress(true);
        
        /* 
         *   Keep going until we finish reading the command.  We might
         *   have to try several times, because our attempts might be
         *   interrupted by real-time events. 
         */
        for (;;)
        {
            local result;
            local timeout;


            /* show the prompt and any pre-input codes */
            inputLineBegin(defObj);

        getInput:
            /* 
             *   Read the input.  (Note that if our timeout is nil, this
             *   will simply act like the ordinary untimed inputLine.)  
             */
            result = aioInputLineTimeout(timeout);

            
            /* check the event code from the result list */
            switch(result[1])
            {
            case InEvtNoTimeout:
                /* 
                 *   the platform doesn't support timeouts - note it for
                 *   future reference so that we don't ask for input with
                 *   timeout again, then go back to try the input again
                 *   without a timeout 
                 */
                noInputTimeout = true;
                timeout = nil;
                goto getInput;
            
            case InEvtLine:
                /* we've finished the current line - end input mode */
                inputLineEnd();

                /* return the line of text we got */
                return result[2];
            
            case InEvtTimeout:
                /* 
                 *   We got a timeout without finishing the input line.
                 *   This means that we've reached the time when the next
                 *   real-time event is ready to execute.  Simply continue
                 *   looping; we'll process all real-time events that are
                 *   ready to go, then we'll resume reading the command.  
                 *   
                 *   Before we proceed, though, notify the command
                 *   sequencer (via the command-interrupt pseudo-tag) that
                 *   we're at the start of output text after an interrupted
                 *   command line input 
                 */
                "<.commandint>";
                break;
            
            case InEvtEof:
                /* 
                 *   End of file - this indicates that the user has closed
                 *   down the application, or that the keyboard has become
                 *   unreadable due to a hardware or OS error.
                 *   
                 *   Write a blank line to the display in an attempt to
                 *   flush any partially-entered command line text, then
                 *   throw an error to signal the EOF condition.  
                 */
                "\b";
                throw new EndOfFileException();

            case InEvtEndQuietScript:
                /* 
                 *   End of "quiet" script - this indicates that we've
                 *   been reading input from a script file, but we've now
                 *   reached the end of that file and are about to return
                 *   to reading from the keyboard.
                 *   
                 *   "Quiet script" mode causes all output to be hidden
                 *   while the script is being processed.  This means that
                 *   we won't have displayed a prompt for the current
                 *   line, or updated the status line.  We'll
                 *   automatically display a new prompt when we loop back
                 *   for another line of input, but we have to mark the
                 *   current input line as actually ended now for that to
                 *   happen.  
                 */
                inputLineInProgress = nil;
                inProgressDefObj = nil;

                /* 
                 *   update the status line, since the quiet script mode
                 *   will have suppressed all status line updates while we
                 *   were reading the script, and thus the last update
                 *   before this prompt won't have been shown 
                 */
                statusLine.showStatusLine();

                /* back for more */
                break;

            case 'newGuest':
                /* 
                 *   Synthetic "new guest" event from the Web UI.  This
                 *   indicates that a new user has joined the session.  The
                 *   parameter is the new user's screen name.  Announce the
                 *   new user's arrival as a real-time event, and go back
                 *   to reading input.  
                 */
                "<.commandint>";
                libMessages.webNewUser(result[2]);
                break;

            case 'logError':
                /*
                 *   Synthetic "log error" event from the Web UI.  The UI
                 *   posts this type of an event when an error occurs in an
                 *   asynchronous task, where it's not possible to display
                 *   an error message directly. 
                 */
                "<.commandint>\b<<result[2]>>\b";
                break;
            }
        }
    }

    /*
     *   Pause for a MORE prompt.            
     */
    pauseForMore()
    {
        /* run the MORE prompt */
        aioMorePrompt();
    }

    /*
     *   Ask for an input file. 
     */
    getInputFile(prompt, dialogType, fileType, flags)
    {
       
        /* ask for a file */
        local result = aioInputFile(prompt, dialogType, fileType, flags);

        /* return the result from inputFile */
        return result;
    }

    /*
     *   Ask for input through a dialog.    The arguments are the same as for
     *   the built-in inputDialog() function.
     */
    getInputDialog(icon, prompt, buttons, defaultButton, cancelButton)
    {       
        /* show the dialog */
        local result = aioInputDialog(icon, prompt, buttons,
                                      defaultButton, cancelButton);

        /* return the dialog result */
        return result;
    }
    

    /*
     *   Read a keystroke, processing real-time events while waiting.
     *   'promptFunc' works the same way it does with getInputLine().
     */
    getKey(promptFunc?)
    {
        local evt;
        
        /* get an event */
        evt = getEventOrKey(promptFunc, true);

        /* 
         *   the only event that getEventOrKey will return is a keystroke,
         *   so return the keystroke from the event record 
         */
        return evt[2];
    }

    /*
     *   Read an event, processing real-time events while waiting, if
     *   desired.  'allowRealTime' and 'promptFunc' work the same way they
     *   do with getInputLine().  
     */
    getEvent(promptFunc?)
    {
        /* read and return an event */
        return getEventOrKey(promptFunc, nil);
    }

    /*
     *   Read an event or keystroke.  'promptFunc' works the same way it does in
     *   getInputLine().  If 'keyOnly' is true, then we're only interested in
     *   keystroke events, and we'll ignore any other events entered.
     *
     *   Note that this routine is not generally called directly; callers should
     *   usually call the convenience routines getKey() or getEvent(), as
     *   needed.
     */
    getEventOrKey(promptFunc, keyOnly)
    {
        
        /* 
         *   Cancel any in-progress input.  If there's an in-progress
         *   input, a real-time event must be interrupting the input,
         *   which is recursively invoking us to start a new input. 
         */
        cancelInputInProgress(true);
        
        /* keep going until we get a keystroke or other event */
        for (;;)
        {
            local result;
            local timeout;


            /* show the prompt and any pre-input codes */
            inputEventBegin(promptFunc);

        getInput:
            /* 
             *   Read the input.  (Note that if our timeout is nil, this
             *   will simply act like the ordinary untimed inputLine.)  
             */
            result = aioInputEvent(timeout);
           

            /* check the event code from the result list */
            switch(result[1])
            {
            case InEvtNoTimeout:
                /* 
                 *   the platform doesn't support timeouts - note it for
                 *   future reference so that we don't ask for input with
                 *   timeout again, then go back to try the input again
                 *   without a timeout 
                 */
                noInputTimeout = true;
                timeout = nil;
                goto getInput;
            
            case InEvtTimeout:
                /* 
                 *   We got a timeout without finishing the input line.
                 *   This means that we've reached the time when the next
                 *   real-time event is ready to execute.  Simply continue
                 *   looping; we'll process all real-time events that are
                 *   ready to go, then we'll restart the event wait.
                 */
                break;
            
            case InEvtEof:
                /* 
                 *   End of file - this indicates that the user has closed
                 *   down the application, or that the keyboard has become
                 *   unreadable due to a hardware or OS error.
                 *   
                 *   Write a blank line to the display in an attempt to
                 *   flush any partially-entered command line text, then
                 *   throw an error to signal the EOF condition.  
                 */
                "\b";
                throw new EndOfFileException();

            case InEvtKey:
                /* keystroke - finish the input and return the event */
                inputEventEnd();
                return result;

            case InEvtHref:
                /* 
                 *   Hyperlink activation - if we're allowed to return
                 *   events other than keystrokes, finish the input and
                 *   return the event; otherwise, ignore the event and keep
                 *   looping.  
                 */
                if (!keyOnly)
                {
                    inputEventEnd();
                    return result;
                }
                break;

            default:
                /* ignore other events */
                break;
            }
        }
    }

    /*
     *   Cancel input in progress.
     *   
     *   If 'reset' is true, we'll clear any input state saved from the
     *   interrupted in-progress editing session; otherwise, we'll retain
     *   the saved editing state for restoration on the next input.
     *   
     *   This MUST be called before calling tadsSay().  Games should
     *   generally never call tadsSay() directly (call the library
     *   function say() instead), so in most cases authors will not need
     *   to worry about calling this on output.
     *   
     *   This MUST ALSO be called before performing any keyboard input.
     *   Callers using inputManager methods for keyboard operations won't
     *   have to worry about this, because the inputManager methods call
     *   this routine when necessary.  
     */
    cancelInputInProgress(reset)
    {
        /* cancel the interpreter's internal input state */
        aioInputLineCancel(reset);

        /* if we were editing a command line, terminate the editing session */
        if (inputLineInProgress)
        {
            /* do our normal after-input work */
            inputLineEnd();
        }

        /* if we were waiting for event input, note that we are no longer */
        if (inputEventInProgress)
        {
            /* do our normal after-input work */
            inputEventEnd();
        }
    }

    

    /*
     *   Begin reading key/event input.  We'll cancel any report gatherer
     *   so that prompt text shows immediately, and show the prompt if
     *   desired.  
     */
    inputEventBegin(promptFunc)
    {
        /* if we're not continuing previous input, show the prompt */
        if (!inputEventInProgress)
        {
            inputBegin(promptFunc);

            /* note that we're in input mode */
            inputEventInProgress = true;
        }
    }

    /*
     *   End keystroke/event input.
     */
    inputEventEnd()
    {
        /* if input is in progress, terminate it */
        if (inputEventInProgress)
        {
            /* note that we're no longer reading an event */
            inputEventInProgress = nil;
        }
    }

    /*
     *   Begin command line editing.  If we're in HTML mode, we'll show
     *   the appropriate codes to establish the input font.  
     */
    inputLineBegin(defObj)
    {
        /* notify the command sequencer that we're reading a command */
        "<.commandbefore>";
        
        /* if we're not resuming a session, set up a new session */
        if (!inputLineInProgress)
        {
            /* begin input */
            inputBegin(defObj.promptFunc);
            
            /* switch to input font */
            defObj.beginInputFont();

            /* note that we're in input mode */
            inputLineInProgress = true;

            /* remember the parameter object for this input */
            inProgressDefObj = defObj;
        }
    }

    /*
     *   End command line editing.  If we're in HTML mode, we'll show the
     *   appropriate codes to close the input font.  
     */
    inputLineEnd()
    {
        /* if input is in progress, terminate it */
        if (inputLineInProgress)
        {
            /* note that we're no longer reading a line of input */
            inputLineInProgress = nil;

            /* end input font mode */
            inProgressDefObj.endInputFont();

            /* notify the command sequencer that we're done reading */
            "<.commandafter>";

            /* 
             *   tell the main text area's output stream that we just
             *   ended an input line 
             */
            mainOutputStream.inputLineEnd();

            /* forget the parameter object for the input */
            inProgressDefObj = nil;
        }
    }

    /*
     *   Begin generic input.  Cancels command report list capture, and
     *   shows the prompt if given.  
     */
    inputBegin(promptFunc)
    {        
        switch(dataTypeXlat(promptFunc))
        {
            /* if we have a prompt, display it */
        case TypeSString:
            say(promptFunc);
            break;
        case TypeFuncPtr:         
            (promptFunc)();
            break;
        default:
            /* Do nothing */
            break;
        }
    }
    
    /* receive post-restore notification */
    execute()
    {
        /* 
         *   Reset the inputLine state.  If we had any previously
         *   interrupted input from the current interpreter session, forget
         *   it by canceling and resetting the input line.  If we had an
         *   interrupted line in the session being restored, forget about
         *   that, too.  
         */
        aioInputLineCancel(true);
        inputLineInProgress = nil;
        inputEventInProgress = nil;

        /* 
         *   Clear the inputLineTimeout disabling flag - we might be
         *   restoring the game on a different platform from the one where
         *   the game started, so we might be able to use timed command
         *   line input even if we didn't when we started the game.  By
         *   clearing this flag, we'll check again to see if we can
         *   perform timed input; if we can't, we'll just set the flag
         *   again, so there will be no harm done.  
         */
        noInputTimeout = nil;
    }

    /* 
     *   Flag: command line input is in progress.  If this is set, it means
     *   that we interrupted command-line editing by a timeout, so we
     *   should not show a prompt the next time we go back to the keyboard
     *   for input.  
     */
    inputLineInProgress = nil

    /* the InputDef object for the input in progress */
    inProgressDefObj = nil

    /* flag: keystroke/event input is in progress */
    inputEventInProgress = nil

    /*
     *   Flag: inputLine does not support timeouts on the current platform.
     *   We set this when we get an InEvtNoTimeout return code from
     *   inputLineTimeout, so that we'll know not to try calling again with
     *   a timeout.  This applies to the current interpreter only, so we
     *   must ignore any value restored from a previously saved game, since
     *   the game might have been saved on a different platform.
     *   
     *   Note that if this value is nil, it means only that we've never
     *   seen an InEvtNoTimeout return code from inputLineEvent - it does
     *   NOT mean that timeouts are supported locally.
     *   
     *   We assume that the input functions are uniform in their treatment
     *   of timeouts; that is, we assume that if inputLineTimeout supports
     *   timeout, then so does inputEvent, and that if one doesn't support
     *   timeout, the other won't either.  
     */
    noInputTimeout = nil
;



/* ------------------------------------------------------------------------ */
/*
 *   End-of-file exception - this is thrown when readMainCommand()
 *   encounters end of file reading the console input. 
 */
class EndOfFileException: Exception
;


/* ------------------------------------------------------------------------ */
/*
 *   'Quitting' exception.  This isn't an error - it merely indicates that
 *   the user has explicitly asked to quit the game. 
 */
class QuittingException: Exception
;

/* ------------------------------------------------------------------------ */
/*
 *   Base class for command input string preparsers.
 *   
 *   Preparsers must be registered in order to run.  During
 *   preinitialization, we will automatically register any existing
 *   preparser objects; preparsers that are created dynamically during
 *   execution must be registered explicitly, which can be accomplished by
 *   inheriting the default constructor from this class.  
 */
class StringPreParser: PreinitObject
    /*
     *   My execution order number.  When multiple preparsers are
     *   registered, we'll run the preparsers in ascending order of this
     *   value (i.e., smallest runOrder goes first).  
     */
    runOrder = 100

    /*
     *   Do our parsing.  Each instance should override this method to
     *   define the parsing that it does.
     *   
     *   'str' is the string to parse, and 'which' is the rmcXxx enum
     *   giving the type of command we're working with.
     *   
     *   This method returns a string or nil.  If the method returns a
     *   string, the caller will forget the original string and work from
     *   here on out with the new version returned; this allows the method
     *   to rewrite the original input as desired.  If the method returns
     *   nil, it means that the string has been fully handled and that
     *   further parsing of the same string is not desired.  
     */
    doParsing(str, which)
    {
        /* return the original string unchanged */
        return str;
    }

    /* 
     *   construction - when we dynamically create a preparser, register
     *   it by default
     */
    construct()
    {
        /* register the preparser */
        StringPreParser.registerPreParser(self);
    }

    /* run pre-initialization */
    execute()
    {
        /* register the preparser if it's not already registered */
        StringPreParser.registerPreParser(self);
    }

    /* register a preparser */
    registerPreParser(pp)
    {
        /* if the preparser isn't already in our list, add it */
        if (regList.indexOf(pp) == nil)
        {
            /* append this new item to the list */
            regList.append(pp);

            /* the list is no longer sorted */
            regListSorted = nil;
        }
    }

    /*
     *   Class method - Run all preparsers.  Returns the result of
     *   successively calling each preparser on the given string.  
     */
    runAll(str, which)
    {
        /* 
         *   if the list of preparsers isn't sorted, sort it in ascending
         *   order of execution order number
         */
        if (!regListSorted)
        {
            /* sort the list */
            regList.sort(SortAsc, {x, y: x.runOrder - y.runOrder});
            
            /* the list is now sorted */
            regListSorted = true;
        }

        /* run each preparser */
        foreach (local cur in regList)
        {
            /* run this preparser, provided it's active */
            if(cur.isActive)
                str = cur.doParsing(str, which);

            /* 
             *   if the result is nil, it means that the string has been
             *   fully handled, so we need not run any further preparsing 
             */
            if (str == nil)
                return nil;
        }

        /* return the result of the series of preparsing steps */
        return str;
    }

    /* class property containing the list of registered parsers */
    regList = static new Vector(10)

    /* class property - the registration list has been sorted */
    regListSorted = nil
    
    /* Flag, is this PreParser active? */
    isActive = true
;

/* ------------------------------------------------------------------------ */
/*
 *   The "comment" pre-parser.  If the command line starts with a special
 *   prefix string (by default, "*", but this can be changed via our
 *   commentPrefix property), this pre-parser intercepts the command,
 *   treating it as a comment from the player and otherwise ignoring the
 *   entire input line.  The main purpose is to give players a way to put
 *   comments into recorded transcripts, as notes to themselves when later
 *   reviewing the transcripts or as notes to the author when submitting
 *   play-testing feedback.  
 */
commentPreParser: StringPreParser
    doParsing(str, which)
    {
        /* get the amount of leading whitespace, so we can ignore it */
        local sp = rexMatch(leadPat, str);
        
        /* 
         *   if the command line starts with the comment prefix, treat it
         *   as a comment 
         */
        if (str.substr(sp + 1, commentPrefix.length()) == commentPrefix)
        {
            /*
             *   It's a comment.
             *   
             *   If a transcript is being recorded, simply acknowledge the
             *   comment; if not, acknowledge it, but with a warning that
             *   the comment isn't being saved anywhere 
             */
            if (scriptStatus.scriptFile != nil)
                DMsg(note with script, 'Comment recorded. ');
            else if (warningCount++ == 0)
                DMsg(note without script warning, 'Comment NOT recorded. ');
            else
                DMsg(note without script, 'Comment NOT recorded. ');

            /* 
             *   Otherwise completely ignore the command line.  To do this,
             *   simply return nil: this tells the parser that the command
             *   has been fully handled by the preparser. 
             */
            return nil;
        }
        else
        {
            /* it's not a command - return the string unchanged */
            return str;
        }
    }

    /* 
     *   The comment prefix.  You can change this to any character, or to
     *   any sequence of characters (longer sequences, such as '//', will
     *   work fine).  If a command line starts with this exact string (or
     *   starts with whitespace followed by this string), we'll consider
     *   the line to be a comment.  
     */
    commentPrefix = '*'
    
    /* 
     *   The leading-whitespace pattern.  We skip any text that matches
     *   this pattern at the start of a command line before looking for the
     *   comment prefix.
     *   
     *   If you don't want to allow leading whitespace before the comment
     *   prefix, you can simply change this to '' - a pattern consisting of
     *   an empty string always matches zero characters, so it will prevent
     *   us from skipping any leading charactres in the player's input.  
     */
    leadPat = static new RexPattern('<space>*')

    /* warning count for entering comments without SCRIPT in effect */
    warningCount = 0

    /*
     *   Use a lower execution order than the default, so that we run
     *   before most other pre-parsers.  Most other pre-parsers are written
     *   to handle actual commands, so it's usually just a waste of time to
     *   have them look at comments at all - and can occasionally be
     *   problematic, since the free-form text of a comment could confuse a
     *   pre-parser that's expecting a more conventional command format.
     *   When the comment pre-parser detects a comment, it halts any
     *   further processing of the command - so by running ahead of other
     *   pre-parsers, we'll effectively bypass other pre-parsers when we
     *   detect a comment.  
     */
    runOrder = 50
;


Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1