input.t | documentation |
#charset "us-ascii" /* * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. * * TADS 3 Library: input * * This modules defines functions and objects related to reading input * from the player. */ #include "adv3.h" /* ------------------------------------------------------------------------ */ /* * 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 /* * Allow real-time events. If this is true, we'll allow real-time * events to interrupt the input; if it's nil, we'll freeze the * real-time clock while reading input. */ allowRealTime = 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(allowRealTime, promptFunc) { self.allowRealTime = allowRealTime; self.promptFunc = promptFunc; } ; /* ------------------------------------------------------------------------ */ /* * Keyboard input manager. */ inputManager: PostRestoreObject /* * Read a line of input from the keyboard. * * If allowRealTime is true, we'll execute any real-time events that * are already due to run, and then we'll allow the input to be * interrupted by real-time events, if interrupted input is * supported on the local platform. Otherwise, we will not process * any real-time events. * * promptFunc is a callback function to invoke to display the * prompt. This is provided as a callback so that we can re-display * the prompt as necessary after real-time event interruptions. * Note that if real-time interruption is not to be allowed, 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(allowRealTime, promptFunc) { /* read input using a basic InputDef for the given parameters */ return getInputLineExt(new BasicInputDef(allowRealTime, 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) { /* make sure the command transcript is flushed */ if (gTranscript != nil) gTranscript.flushForInput(); /* * 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; local t0; /* note the starting time, in case we want to freeze the clock */ t0 = realTimeManager.getElapsedTime(); /* process real-time events, if possible */ timeout = processRealTimeEvents(defObj.allowRealTime); /* 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); /* * If we're not allowing real-time event processing, freeze * the clock during the read - set the elapsed game * real-time clock back to the value it had on entry, so * that the input effectively consumes no real time. */ if (!defObj.allowRealTime) realTimeManager.setElapsedTime(t0); /* 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. If freezeRealTime is true, we'll stop * the real-time clock; otherwise we'll let it keep running. Even if * we don't freeze the clock, we won't actually process any real-time * events while waiting: the point of the MORE prompt is to allow the * player to read and acknowledge the on-screen display before * showing anything new, so we don't want to allow any output to * result from real-time events that occur while waiting for user * input. If any real-time events come due while we're waiting, * we'll process them when we're done. * * In order to ensure that the display makes sense to the user, we * flush any captured input in the transcript before pausing. We * re-activate transcript capture after the pause if it was active * before. Note that in some cases, this could affect the overall * output for the command, since some commands wait until the very * end of the command to go back and process the entire transcript * for the command. Since we interrupt the transcript, flushing any * output that occurred before the pause, a command that goes back * over its entire output stream at the end of the turn won't be able * to see or modify any of the output that occurred prior to the * pause, since we will have flushed the output to that point. */ pauseForMore(freezeRealTime) { local t0; local wasTranscriptActive = nil; /* * flush any command transcript and turn off transcript capture, * so that we show any pent-up reports before pausing for the * MORE prompt */ if (gTranscript != nil) wasTranscriptActive = gTranscript.flushForInput(); /* * cancel any pending input - we must be interrupting the * pending input with a real-time event */ cancelInputInProgress(true); /* note the starting time, in case we want to freeze the clock */ t0 = realTimeManager.getElapsedTime(); /* run the MORE prompt */ aioMorePrompt(); /* if the transcript was previously active, re-activate it */ if (wasTranscriptActive) gTranscript.activate(); /* * if the caller wanted us to freeze the clock, restore the * elapsed game real time to what it was when we started, so * that the time the player took to acknowledge the MORE prompt * won't count against the elapsed game time; otherwise, process * any real-time events that came due while we were waiting */ if (freezeRealTime) { /* time was frozen - restore the original elapsed time */ realTimeManager.setElapsedTime(t0); } else { /* * time wasn't frozen - check for any events that have come * due since we started waiting, and process them * immediately */ processRealTimeEvents(true); } } /* * Ask for an input file. Freezes the real-time event clock for the * duration of reading the event. */ getInputFile(prompt, dialogType, fileType, flags) { /* * note the game elapsed time before we start - we want to * freeze the real-time clock while we're waiting for the user * to respond, since this system verb exists outside of the * usual time flow of the game */ local origElapsedTime = realTimeManager.getElapsedTime(); /* ask for a file */ local result = aioInputFile(prompt, dialogType, fileType, flags); /* * restore the game real-time counter to what it was before we * started the interactive response */ realTimeManager.setElapsedTime(origElapsedTime); /* return the result from inputFile */ return result; } /* * Ask for input through a dialog. Freezes the real-time clock for * the duration of the dialog display. The arguments are the same as * for the built-in inputDialog() function. */ getInputDialog(icon, prompt, buttons, defaultButton, cancelButton) { /* * note the current elapsed game real time, so we can restore it * after the dialog is done */ local origElapsedTime = realTimeManager.getElapsedTime(); /* show the dialog */ local result = aioInputDialog(icon, prompt, buttons, defaultButton, cancelButton); /* * restore the real-time counter, so that the time spent in the * dialog doesn't count */ realTimeManager.setElapsedTime(origElapsedTime); /* return the dialog result */ return result; } /* * Read a keystroke, processing real-time events while waiting, if * desired. 'allowRealTime' and 'promptFunc' work the same way they * do with getInputLine(). */ getKey(allowRealTime, promptFunc) { local evt; /* get an event */ evt = getEventOrKey(allowRealTime, 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(allowRealTime, promptFunc) { /* read and return an event */ return getEventOrKey(allowRealTime, promptFunc, nil); } /* * Read an event or keystroke. 'allowRealTime' and 'promptFunc' work * the same way they do 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(allowRealTime, promptFunc, keyOnly) { /* make sure the command transcript is flushed */ if (gTranscript != nil) gTranscript.flushForInput(); /* * 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; local t0; /* note the starting time, in case we want to freeze the clock */ t0 = realTimeManager.getElapsedTime(); /* process real-time events, if possible */ timeout = processRealTimeEvents(allowRealTime); /* 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); /* * If we're not allowing real-time event processing, freeze * the clock during the read - set the elapsed game * real-time clock back to the value it had on entry, so * that the input effectively consumes no real time. */ if (!allowRealTime) realTimeManager.setElapsedTime(t0); /* 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(); } } /* * Process any real-time events that are ready to run, and return the * timeout until the next real-time event. * * If allowRealTime is nil, we won't process real-time events at all; * we'll merely return nil for the timeout to indicate to the caller * that any user input interaction about to be attempted should wait * indefinitely. */ processRealTimeEvents(allowRealTime) { local timeout; /* presume we will not use a timeout */ timeout = nil; /* process real-time events, if allowed */ if (allowRealTime) { local tNext; /* * Process any real-time events that are currently ready to * execute, and note the amount of time until the next * real-time event is ready. */ tNext = realTimeManager.executeEvents(); /* * If there's an event pending, note the interval between the * current time and the event's scheduled time - this will * give us the maximum amount of time we want to wait for the * user to edit the command line before interrupting to * execute the pending event. Ignore this if the platform * doesn't support timeouts to begin with. */ if (tNext != nil && !noInputTimeout) timeout = tNext - realTimeManager.getElapsedTime(); } /* return the timeout until the next real-time event */ return timeout; } /* * 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) { /* * Turn off command transcript capture, if it's active. Once * we're soliciting input interactively, we can no longer * usefully capture the text output of commands, but this is fine * because we must be doing something for which capture isn't * important anyway. Reporting capture is used for things like * selecting the kind of result to show, which clearly isn't a * factor for actions involving interactive input. */ if (gTranscript != nil) gTranscript.flushForInput(); /* if we have a prompt, display it */ if (promptFunc != nil) (promptFunc)(); } /* 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 ; /* ------------------------------------------------------------------------ */ /* * Read a command line from the player. Displays the main command * prompt and returns a line of input. * * We process any pending real-time events before reading the command. * If the local platform supports real-time command-line interruptions, * we'll continue processing real-time events as they occur in the * course of command editing. */ readMainCommand(which) { local str; /* execute any pre-command-prompt daemons */ eventManager.executePrompt(); /* * Read a line of input, allowing real-time event processing, and * return the line of text we read. Use the appropriate main * command prompt for the given prompt mode. */ str = inputManager.getInputLine( true, {: gLibMessages.mainCommandPrompt(which) }); /* return the string we read */ return str; } /* ------------------------------------------------------------------------ */ /* * 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 */ 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 ; /* ------------------------------------------------------------------------ */ /* * 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) gLibMessages.noteWithScript; else if (warningCount++ == 0) gLibMessages.noteWithoutScriptWarning; else gLibMessages.noteWithoutScript; /* * 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 ; /* ------------------------------------------------------------------------ */ /* * Read a line of text and return the token list and the original text. * We keep going until a non-empty line of text is read. * * 'which' is one of the rmcXxx enum values specifying what kind of * command line we're reading. * * The return value is a list of two elements. The first element is the * string entered, and the second element is the token list. */ readMainCommandTokens(which) { local str; local toks; /* keep going until we get a non-empty command line */ for (;;) { /* read a command line */ str = readMainCommand(which); /* run any preparsing desired on the string */ str = StringPreParser.runAll(str, which); /* * if preparsing returned nil, it means that the preparser fully * handled the string - simply return nil to tell the caller * that its work is done */ if (str == nil) return nil; try { /* tokenize the command string */ toks = cmdTokenizer.tokenize(str); } catch (TokErrorNoMatch tokExc) { /* * Invalid tokens in the response - complain about it. Flag * the error as being in the first character of the * remaining string, since that's the character for which we * could find no match. */ gLibMessages.invalidCommandToken(tokExc.curChar_.htmlify()); /* go back for another input line */ continue; } /* if we got a non-empty token list, return it */ if (toks.length() != 0) return [str, toks]; /* show the empty-command reply */ gLibMessages.emptyCommandResponse(); } }
TADS 3 Library Manual
Generated on 5/16/2013 from TADS version 3.1.3
Generated on 5/16/2013 from TADS version 3.1.3