#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library - browser (Web UI) input/output manager
*
* This module defines the low-level functions for handling input and
* output via the Web UI.
*
* The functions in this module are designed primarily for internal use
* within the library itself. Games should use the higher level objects
* and functions defined in input.t and output.t instead of directly
* calling the functions defined here. The reason for separating these
* functions is to make the UI selection pluggable, so that the same game
* can be compiled for either the traditional UI or the Web UI simply by
* plugging in the correct i/o module.
*/
/* include the library header */
#include "advlite.h"
/* ------------------------------------------------------------------------ */
/*
* Browser globals
*/
transient browserGlobals: object
/* the HTTPServer object for the browser UI session */
httpServer = nil
/*
* Log file handle. For a LogTypeTranscript file, this is a
* LogConsole object; for other types, it's a regular file handle.
*/
logFile = nil
/* logging type (LogTypeXxx from tadsio.h, or nil if not logging) */
logFileType = nil
;
/* ------------------------------------------------------------------------ */
/*
* Initialize the user interface. The library calls this once at the
* start of the interpreter session to set up the UI. For the Web UI, we
* create the HTTP server and send connection instructions to the client.
*/
initUI()
{
/*
* Set up the HTTP server. Listen on the launch address, which is
* the address that the client used to reach the external Web server
* that launched the interpreter. For local stand-alone launches,
* the launch address is nil, so the HTTP server will listen on
* localhost, which is just what we need in order to connect to the
* local UI.
*/
local srv = browserGlobals.httpServer = new HTTPServer(
getLaunchHostAddr(), nil, 1024*1024);
/* send connection instructions to the client */
webSession.connectUI(srv);
}
/*
* Initialize the display. We call this when we first enter the
* interpreter, and again at each RESTART, to set up the main game
* window's initial layout. We set up the conventional IF screen layout,
* with the status line across the top and the transcript/command window
* filling the rest of the display.
*/
initDisplay()
{
/* set up the command window and status line */
webMainWin.createFrame(commandWin, 'command',
'0, statusline.bottom, 100%, 100%');
webMainWin.createFrame(statuslineBanner, 'statusline',
'0, 0, 100%, content.height');
/* capture the title string */
local title = mainOutputStream.captureOutput(
{: gameMain.setGameTitle() });
/* parse out the contents of the <title> tag */
if (rexSearch('<nocase>[<]title[>](.*)[<]/title[>]', title))
title = rexGroup(1)[3];
/* initialize the statusline window object */
statuslineBanner.init();
statusLine.statusDispMode = StatusModeBrowser;
/* set the title */
webMainWin.setTitle(title);
/* get the session parameters from the arguments */
local arg = libGlobal.getCommandSwitch('-gameid=');
if (arg != nil && arg != '')
webSession.launcherGameID = arg;
arg = libGlobal.getCommandSwitch('-storagesid=');
if (arg != nil && arg != '')
webSession.storageSID = arg;
arg = libGlobal.getCommandSwitch('-username=');
if (arg != nil && arg != '')
webSession.launcherUsername = arg;
}
/*
* Shut down the user interface. The library calls this when the game is
* about to terminate.
*/
terminateUI()
{
/* if we have an HTTP server, shut it down */
if (browserGlobals.httpServer != nil)
{
/* flush our windows */
webMainWin.flushWin();
commandWin.sendWinEvent('<scrollToBottom/>');
/* end any scripting */
aioSetLogFile(nil, LogTypeTranscript);
aioSetLogFile(nil, LogTypeCommand);
/*
* keep running for a few more minutes, to give clients a chance
* to perform final tasks like downloading log files
*/
ClientSession.shutdownWait(5*60*1000);
/* send the shutdown message */
eventPage.sendEvent('<shutdown/>');
/* wait a short time for clients to process the shutdown event */
ClientSession.shutdownWait(5*1000);
/* shut down the http server */
browserGlobals.httpServer.shutdown(true);
}
}
/* ------------------------------------------------------------------------ */
/*
* Check to see if we're in HTML mode
*/
checkHtmlMode()
{
/*
* The web UI is always in HTML mode. This is regardless of the
* interpreter class, because that only tells us about the
* interpreter's own native UI. The actual user interface in Web UI
* mode runs in a separate Web browser app, which is inherently HTML
* capable.
*/
return true;
}
/* ------------------------------------------------------------------------ */
/*
* Write text to the main game window
*/
aioSay(txt)
{
/* write the text to the main command window */
commandWin.write(txt);
/* if we're logging a full transcript, write the text */
if (browserGlobals.logFileType == LogTypeTranscript)
browserGlobals.logFile.writeToStream(txt);
}
/* ------------------------------------------------------------------------ */
/*
* Is a script file active?
*/
readingScript()
{
return setScriptFile(ScriptReqGetStatus) != nil;
}
/*
* Is an event script active?
*/
readingEventScript()
{
local s = setScriptFile(ScriptReqGetStatus);
return (s != nil && (s & ScriptFileEvent) != 0);
}
/* ------------------------------------------------------------------------ */
/*
* Get a line of input from the keyboard, with timeout
*/
aioInputLineTimeout(timeout)
{
/* check for script input */
local scriptMode = setScriptFile(ScriptReqGetStatus);
if (scriptMode != nil)
{
/* we're in a script, so use the regular input line reader */
local e = inputLineTimeout(timeout);
/*
* If it's not an end-of-file indication, return the event. An
* EOF means that there are no more events in the script, so
* return to reading from the live client UI.
*/
if (e[1] != InEvtEof)
{
/* echo the input if we're not in quiet mode */
if (e[1] == InEvtLine && !(scriptMode & ScriptFileQuiet))
aioSay(e[2].htmlify() + '\n');
/* log and return the event */
return aioLogInputEvent(e);
}
}
/*
* read an input line event from the main command window, log it, and
* return it
*/
return aioLogInputEvent(commandWin.getInputLine(timeout));
}
/*
* Cancel a suspended input line
*/
aioInputLineCancel(reset)
{
/* cancel the input line in the command window */
commandWin.cancelInputLine(reset);
}
/* ------------------------------------------------------------------------ */
/*
* Read an input event
*/
aioInputEvent(timeout)
{
/* check for script input */
if (readingEventScript())
{
/* we're in a script, so use the regular input line reader */
local e = inputEvent(timeout);
/*
* If it's not an end-of-file indication, return the event. An
* EOF means that there are no more events in the script, so
* return to reading from the live client UI.
*/
if (e[1] != InEvtEof)
return aioLogInputEvent(e);
}
/* read an event from the main command window, log it, and return it */
return aioLogInputEvent(webMainWin.getInputEvent(timeout));
}
/* ------------------------------------------------------------------------ */
/*
* Show a "More" prompt
*/
aioMorePrompt()
{
/* show a More prompt in the main command window */
commandWin.showMorePrompt();
}
/* ------------------------------------------------------------------------ */
/*
* Clear the screen
*/
aioClearScreen()
{
/* clear the main transcript window */
commandWin.clearWindow();
}
/* ------------------------------------------------------------------------ */
/*
* Show a file selector dialog
*/
aioInputFile(prompt, dialogType, fileType, flags)
{
/*
* First, try reading from the local console. Even though we're
* using the Web UI, there are two special cases where the input will
* come from the local (server-side) console instead of from the
* browser UI:
*
* 1. We're reading from an event script. In this case, regardless
* of the UI mode, the interpreter reads from a server-side file and
* parses the results into an inputFile() result, bypassing any UI
* interaction.
*
* 2. We're running in the Web UI's local stand-alone configuration,
* where the browser is actually an integrated window within the
* interpreter. This configuration simulates the traditional UI by
* running everything locally - the client and server are running on
* the same machine, so there's really no distinction between
* client-side and server-side. Because everything's local, files
* are local, so we want to display traditional local file selector
* dialogs. The stand-alone interpreter does this for us via the
* standard inputFile() function when it detects this configuration.
*
* If neither of these special cases apply, inputFile() will return
* an error to let us know that it can't show a file dialog in the
* current configuration, so we'll continue on to showing the dialog
* on the client side via the Web UI.
*/
local f = inputFile(prompt, dialogType, fileType, flags);
/* if that failed, forget the result */
if (f[1] == InFileFailure)
f = nil;
/* if we got a file, check for warnings */
if (f != nil && f.length() >= 4 && f[4] != nil)
{
/* keep going until we get a definitive answer */
for (local done = nil ; !done ; )
{
/* show the warning dialog */
local d = webMainWin.getInputDialog(
InDlgIconWarning,
libMessages.inputFileScriptWarning(f[4], f[2]),
libMessages.inputFileScriptWarningButtons, 1, 3);
/* check the result */
switch (d)
{
case 0:
case 3:
/* dialog error or Cancel Script - stop the script */
setScriptFile(nil);
/* return a Cancel result */
return [InFileCancel];
case 1:
/* "Yes" - proceed */
done = true;
break;
case 2:
/* Choose New File button - show a file dialog */
local fNew = webMainWin.getInputFile(
prompt, dialogType, fileType, flags);
switch (fNew[1])
{
case InFileSuccess:
/* success - use the new file, and we're done */
f = fNew;
done = true;
break;
case InFileCancel:
/* cancel - repeat the prompt */
break;
case InFileFailure:
/* dialog error - cancel the script */
setScriptFile(nil);
return [InFileCancel];
}
}
}
}
/*
* if we didn't get a result from a script or from the local console,
* tell the client UI to display its file dialog
*/
if (f == nil)
f = webMainWin.getInputFile(prompt, dialogType, fileType, flags);
/* log a synthetic <file> event, if applicable */
aioLogInputEvent(
['<file>',
f[1] != InFileSuccess ? '' :
dataType(f[2]) == TypeObject && !f[2].ofKind(FileName) ? 't' :
f[2]]);
/* return the file information */
return f;
}
/* ------------------------------------------------------------------------ */
/*
* Show an input dialog
*/
aioInputDialog(icon, prompt, buttons, defaultButton, cancelButton)
{
/* check for script input */
local d = nil;
if (readingEventScript())
{
/* we're in a script, so use the regular dialog event reader */
d = inputDialog(icon, prompt, buttons, defaultButton, cancelButton);
/* if it failed, forget the result */
if (d == 0)
d = nil;
}
/* if we didn't get script input, show the dialog via the client UI */
if (d == nil)
d = webMainWin.getInputDialog(icon, prompt, buttons,
defaultButton, cancelButton);
/* log a synthetic <dialog> event, if applicable */
aioLogInputEvent(['<dialog>', d]);
/* return the result */
return d;
}
/* ------------------------------------------------------------------------ */
/*
* Set/remove the output logging file
*/
aioSetLogFile(fname, typ = LogTypeTranscript)
{
/* if there's currently a log file open, close it */
local log = browserGlobals.logFile;
if (log != nil)
{
switch (browserGlobals.logFileType)
{
case LogTypeTranscript:
/* for a transcript, we have a log console as the handle */
log.closeConsole();
break;
default:
/* for other types, we have a regular file handle */
try
{
log.closeFile();
}
catch (Exception exc)
{
/* ignore errors, as we have no way to return them */
}
break;
}
/* we've closed the handle, so forget it */
log = nil;
}
/* presume success */
local ok = true;
/* if there's a filename, create a new console for this file */
if (fname != nil)
{
/* create the output handle according to the type */
switch (typ)
{
case LogTypeTranscript:
/*
* full transcript - create a log console, which will do the
* standard output formatting for us
*/
log = new LogConsole(fname, nil, 80);
break;
case LogTypeCommand:
case LogTypeScript:
/* for other types, create an ordinary text file */
try
{
/* open the log file */
log = File.openTextFile(fname, FileAccessWrite, nil);
/* for an event script, write the <eventscript> opener */
if (typ == LogTypeScript)
log.writeFile('<eventscript>\n');
}
catch (Exception exc)
{
/* if anything went wrong, we have no log file */
log = nil;
}
break;
default:
throw RuntimeError.newRuntimeError(2306, 'bad log file type');
}
/* we failed if the log handle is nil */
if (log == nil)
{
ok = nil;
typ = nil;
}
}
else
{
/* no longer logging */
typ = nil;
}
/* remember the new handle and log type */
browserGlobals.logFile = log;
browserGlobals.logFileType = typ;
/* return the success/failure indicator */
return ok;
}
/*
* Log an input event. We call this internally from each of the event
* input routines to add the event to any event or command log we're
* creating.
*/
aioLogInputEvent(evt)
{
/* if the system is maintaining its own input log, write it there */
logInputEvent(evt);
/* get the script globals */
local ltyp = browserGlobals.logFileType;
local log = browserGlobals.logFile;
/* get the basic event parameters */
local evtType = evt[1];
local param = (evt.length() > 1 ? evt[2] : nil);
/* format the event based on the event type */
switch (ltyp)
{
case LogTypeTranscript:
/* transcript - echo command line input */
if (evt[1] == InEvtLine)
log.writeToStream(evt[2].htmlify() + '\n');
break;
case LogTypeCommand:
/* command script - write command inputs only */
if (evt[1] == InEvtLine)
log.writeFile('>' + evt[2] + '\n');
break;
case LogTypeScript:
/* event script - write all event types */
switch (evtType)
{
case InEvtKey:
log.writeFile('<key>' + evtCharForScript(param));
break;
case InEvtTimeout:
log.writeFile('<timeout>' + param);
break;
case InEvtHref:
log.writeFile('<href>' + param);
break;
case InEvtNoTimeout:
log.writeFile('<notimeout>');
break;
case InEvtEof:
log.writeFile('<eof>');
break;
case InEvtLine:
log.writeFile('<line>' + param);
break;
case InEvtSysCommand:
log.writeFile('<command>' + param);
break;
case InEvtEndQuietScript:
log.writeFile('<endqs>');
break;
default:
/* if it's a string value, it's the literal event tag */
if (dataType(evtType) == TypeSString)
log.writeFile(evtType + param);
break;
}
/* add a newline at the end of the event line */
log.writeFile('\n');
break;
}
/*
* return the event, so that the caller can conveniently return it
* after logging it
*/
return evt;
}
/*
* Get an InEvtKey event parameter in suitable format for script file
* output. This returns the key as it appears in the event, except that
* ASCII control characters are translated to '[ctrl-X]'.
*/
evtCharForScript(c)
{
if (c.toUnicode(1) < 32)
{
/* it's a control character - return the [ctrl-X] sequence */
return '[ctrl-<<makeString(c.toUnicode(1) + 64)>>]';
}
else
{
/* return everything else as it appears in the event descriptor */
return c;
}
}
/* ------------------------------------------------------------------------ */
/*
* Generate a string to show hyperlinked text. The browser UI is always
* in HTML mode, so we unconditionally generate the hyperlink.
*
* If the display text is included, we'll generate the entire link,
* including the <A HREF> tag, the hyperlinked text contents, and the
* </A> end tag. If the text is omitted, we'll simply generate the <A
* HREF> tag itself, leaving it to the caller to display the text and the
* </A>.
*
* The optional 'flags' is a combination of AHREF_xxx flags indicating
* any special properties of the hyperlink.
*/
aHref(href, txt?, title?, flags = 0)
{
/* figure extra properties, based on the flags */
local props = '';
if (flags & AHREF_Plain)
props += 'class="plain" ';
/* generate the <A HREF>, text, and </A>, as applicable */
return '<a <<props>> href="<<href.findReplace('"', '%22')>>"<<
(title != nil
? ' title="' + title.findReplace('"', '"') + '"'
: '')
>> onclick="javascript:return gamehref(event,\'<<
href.findReplace(['\'', '"'],
['\\\'', '\'+String.fromCharCode(34)+\''])
>>\', \'main.command\', this);"><.a><<
(txt != nil ? txt + '<./a></a>' : '')>>';
}
/* ------------------------------------------------------------------------ */
/*
* Generate a string to show hyperlinked text, with alternate text if
* we're not in HTML mode. The browser UI is always in HTML mode, so we
* unconditionally generate the hyperlink.
*/
aHrefAlt(href, linkedText, altText, title?)
{
return aHref(href, linkedText, title);
}
/* ------------------------------------------------------------------------ */
/*
* The standard main command window.
*/
transient commandWin: WebCommandWin
;
/* ------------------------------------------------------------------------ */
/*
* Generate HTML to wrap the left/right portions of the status line. The
* basic status line has three stages: stage 0 precedes the left portion,
* stage 1 comes between the left and right portions, and stage 2 follows
* the right portion. If we're listing exits, we get two more stages:
* stage 3 precedes the exit listing, stage 4 follows it.
*/
statusHTML(stage)
{
switch(stage)
{
case 0:
/* start the left-aligned portion */
return '<div class="statusleft">';
case 1:
/* close the left portion, and start the right-aligned portion */
return '</div><div class="statusright">';
case 2:
/*
* Close the right portion, and break clear of the floating
* sections. The break is necessary to make sure that the
* contents of the two sections count in the window height; some
* browsers don't include floating boxes in the content height,
* so we need to manually extend the main vertical box's height
* past the floating sections.
*/
return '</div><div class="statusStrut"></div>';
case 3:
case 4:
/* before/after exit listing - we have nothing to add here */
return '';
default:
return '';
}
}
/* ------------------------------------------------------------------------ */
/*
* Web Banner Window. This is designed as a *partial* drop-in
* replacement for the BannerWindow class, using Web UI windows as
* implemented in the core TADS javascript client.
*
* This class is designed to be mixed with a WebWindow subclass.
*
* This isn't a complete replacement for BannerWindow, because the layout
* model for the Web UI is different from the banner window model (the
* Web UI model is better and more flexible). This class implements the
* parts of the BannerWindow API related to the stream-oriented output to
* the window, so you shouldn't have to change anything that writes HTML
* text to the window. However, you will have to rework code that sets
* up the window's layout to use the Web UI model.
*/
class WebBannerWin: OutputStreamWindow
/*
* Initialize. Call this when first displaying the window in the UI.
*/
init()
{
/* set up our output stream */
createOutputStream();
}
/* create our output stream subclass */
createOutputStreamObj()
{
return new transient WebWinOutputStream(self);
}
/* flush output */
flushBanner()
{
flushWin();
}
/* write text */
writeToBanner(txt)
{
outputStream_.writeToStream(txt);
}
/*
* Banner window size settings. We simply ignore these; callers must
* rework their layout logic for the Web UI, since the javascript
* layout system is so different.
*/
setSize(siz, units, advisory) { }
sizeToContents() { }
;
/*
* Output stream for web banner windows
*/
class WebWinOutputStream: OutputStream
/* construct */
construct(win)
{
/* do the base class construction */
inherited();
/* save our window */
win_ = win;
}
/* ignore preinit - we're always created dynamically */
execute() { }
/* write to the underlying window */
writeFromStream(txt)
{
/* add the text to the window */
win_.write(txt);
}
/* our status line window */
win_ = nil
;
/* ------------------------------------------------------------------------ */
/*
* The basic status line window. The "banner" in the name is historical,
* because the traditional console UI implements the status line as a
* banner window. We don't actually have banner windows in the Web UI;
* we use iframes instead. But we keep the name to make it easier to
* port games written for the traditional UI to the Web UI.
*/
transient statuslineBanner: WebStatusWin, WebBannerWin
;
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1