forked from EricEve/adv3lite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbrowser.t
812 lines (702 loc) · 24.3 KB
/
browser.t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
#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
;