The GlkOte, GlkAPI, and Dialog Javascript libraries are copyright 2008-2024 by Andrew Plotkin. They are distributed under the MIT license.
This documentation is licensed under a Creative Commons Attribution-Noncommercial-ShareAlike 4.0 International License.
GlkOte is based on the Glk API. However, GlkOte's API is not identical to Glk, even allowing for the differences between Javascript and C. GlkOte is adapted to the realities of a web application environment -- a thin Javascript layer which communicates with a distant server in intermittent bursts. GlkOte does not handle certain elements of the Glk standard, such as memory streams and file storage, which are better kept within the game server. On the other hand, GlkOte is more flexible than Glk in certain ways; it may be suitable for more applications than Glk per se.
GlkOte can be used from two angles. First, in a purely client-side IF application. The (included, optional) glkapi.js file facilitates this; it wraps around GlkOte and provides an API that is identical to Glk, as closely as Javascript allows. An IF interpreter written in Javascript, running entirely within the user's web browser, can use glkapi.js just as a C interpreter uses a normal Glk library. Or it could bypass glkapi.js and use GlkOte directly.
Alternatively, GlkOte could be used with RemGlk, a Glk library which acts as a web service. Any Glk-compliant interpreter (including Glulx interpreters) can be compiled with RemGlk. The resulting program is then run on a web server. A GlkOte web page sends AJAX requests to the server, which relays them (via CGI script) to the RemGlk program; the game responds back up the chain to the user's web browser. Thus, RemGlk and GlkOte together form a complete Glk web application.
Here is a demonstration of a client-side IF application. It is not a full-fledged IF game; it has only a trivial one-word parser. However, it demonstrates all the I/O capabilities of a typical IF game, including a status window, story window, line and character input, window resizing, scrollback, quote boxes, menus, and so on. (This demo does not use glkapi.js.)
A more complete example: Quixe is an implementation of the Glulx virtual machine, using glkapi.js and GlkOte.
A stripped-down, but easily extendable example. This just accepts lines of input and responds to them, in a single window (no status window). In the HTML source, you'll see stub functions which print the initial text and respond to input lines; modify them as you wish.
The differences do not affect the glkote.js module itself, so we will not discuss them here. The short answer is: under Electron, use the electrofs.js module instead of dialog.js. (These modules are concerned with file-selection dialogs and storing data.)
See Lectrote for a demonstration of GlkOte and Quixe running as an application.
<!DOCTYPE html> <html> <head> <title>GlkOte: Extremely Minimal</title> <link rel="stylesheet" href="glkote.css" type="text/css"> <style type="text/css"> body { margin: 0px; height: 100%; } #gameport { position: absolute; overflow: hidden; width: 100%; height: 100%; background: #CCAA88; margin: 0px; } </style> <script src="jquery-1.12.4.min.js" type="text/javascript"></script> <script src="glkote.js" type="text/javascript"></script> <script type="text/javascript"> var generation = 0; function accept() { generation = generation+1; var arg = { type: 'update', gen: generation }; GlkOte.update(arg); } Game = { accept: accept, }; </script> </head> <body onLoad="GlkOte.init();"> <div id="gameport"> <div id="windowport"> <noscript><hr> <p>You'll need to turn on Javascript in your web browser to play this game.</p> <hr></noscript> </div> <div id="loadingpane"> <img src="waiting.gif" alt="LOADING"><br> <em> Loading...</em> </div> <div id="errorpane" style="display:none;"><div id="errorcontent">...</div></div> </div> </body> </html>
This displays no windows and accepts no input. However, it demonstrates the essential features of all GlkOte applications. If you are creating such a thing, you will want to start with this sample page and build out.
Let us consider its elements.
<link rel="stylesheet" href="glkote.css" type="text/css">
The default GlkOte stylesheet contains style information for the windows and text elements that GlkOte generates. You will need to include this stylesheet for GlkOte to behave properly. However, you can customize most of its features, either by modifying the default stylesheet or by including more CSS style information in your page. See "Customizing Your Stylesheet".
<style type="text/css"> body { margin: 0px; height: 100%; } #gameport { position: absolute; overflow: hidden; width: 100%; height: 100%; background: #CCAA88; margin: 0px; } </style>
These declarations lay out your document, and position the gameport
div. GlkOte draws all of its content within the gameport
. In this example, the gameport
fills the entire page, but you can place it anywhere in your document (using normal CSS positioning). (You can also rename the gameport
div if you need to; see "The Game Interface Object".)
<script src="jquery-1.12.4.min.js" type="text/javascript"></script> <script src="glkote.js" type="text/javascript"></script>
These lines include the GlkOte library and the jQuery library. jQuery is an open-source Javascript library which simplifies web application work. GlkOte requires jQuery to function.
(GlkOte has been tested with jQuery version 1.12.4. It should work with 1.9.x and up, but not any earlier version.)
<script type="text/javascript"> var generation = 0; function accept() { generation = generation+1; var arg = { type: 'update', gen: generation }; GlkOte.update(arg); } Game = { accept: accept, }; </script>
To use the GlkOte library, first you need a library instance. Since you have included the glkote.js
script, you have an instance available, called GlkOte
(or window.GlkOte
). (It is possible to create more instances, but this requires extra effort, so we won't get into it now. See "Multiple GlkOte Instances", below.)
Next, you need a function, a global game object, and a global generation
variable. The function accepts input from the library (which is to say, from the user) and generates output in return. You pass this function to the GlkOte library by storing it in the global Game
object.
In return, the GlkOte library exports a handful of functions for your use. The two important ones are GlkOte.init
and GlkOte.update
.
(The generation
variable is needed to keep the library and your application in sync -- particularly if your application is actually a remote web service, like RemGlk.
See "The Generation Number".)
<body onLoad="GlkOte.init();">
The GlkOte library begins working when you call GlkOte.init
. The simplest way to do this is to put this call in the onLoad
attribute of your document.
<div id="gameport"> <div id="windowport"> <noscript><hr> <p>You'll need to turn on Javascript in your web browser to play this game.</p> <hr></noscript> </div> <div id="loadingpane"> <img src="waiting.gif" alt="LOADING"><br> <em> Loading...</em> </div> <div id="errorpane" style="display:none;"> <div id="errorcontent">...</div></div> </div>
This is the gameport
div, which was mentioned earlier. It must appear somewhere in your page body, exactly as shown in the
demo.
(Yes, it is an unwieldy block of text, but you only have to paste it in once.)
<script src="sample-demo.js" type="text/javascript"></script>
This file defines an accept function and a generation variable, but these are much more ornate. It is able to display multiple windows, quote boxes, images, and so on.
We will not go into the details here. However, note that the onLoad
line for this demo looks like this:
<body onLoad="SampleDemo.game_init(GlkOte, Game);">
SampleDemo.game_init()
stashes the GlkOte interface object away for future use, and then calls GlkOte.init()
, passing in the Game
configuration object. Again, the GlkOte library begins working at this point.
GlkOte.init
function is called. Normally you put this call in your document's onLoad
attribute, but you could call it from some other bit of script.
The first thing GlkOte.init
does is measure things; the gameport
, the size of various fonts, the amount of space needed to create a window, and so on. It assembles this information into a "metrics" object. The metrics will be used throughout the game, both by GlkOte and by your code, to lay out text and windows properly.
GlkOte.init
then invokes your game's accept
function, passing in a few bits of information. That is to say, it looks for a global Game
object, and calls
Game.accept({ type: 'init', gen: 0, support: ['timer', 'graphics', 'hyperlinks'], metrics: METRICS_OBJECT });
The type:'init'
property indicates that this is the first call to Game.accept
-- it says that the GlkOte library has just started up, and has no windows visible. The gen
field will always be 0 for this first call. The metrics
are as described above. (Your function should store a reference to this metrics object. You'll need it later to create windows.
See "Laying Out Windows"
for the details.)
The support
field is an array of strings representing the capabilities of the GlkOte library. If you are packaging your game with the library, you don't have to worry about this -- you can just use the capabilities of the version you've got. However, RemGlk needs to know the display capabilities in order to set its gestalt flags correctly; the support
list lets it do this.
The goal of your Game.accept
, on this first call, is to create the initial windows, fill them with the initial text, and then set up the initial input fields.
You might expect a long list of API functions with which to accomplish this goal. You would be wrong. Instead, you stuff all that information -- window sizes and positions, paragraphs of text, input fields and all -- into a single sprawling data structure. Then you pass that data structure to GlkOte.update
, and the library does all the work.
(This is the "intermittent burst" model I mentioned earlier. If your game is running in RemGlk, in a web server, then the Game.accept
call is a single HTTP request -- and the data structure is a single HTTP response. One exchange and the game is ready to go.)
Once Game.accept
has called GlkOte.update
, its job is done. It exits, leaving the page full of windows, text, and an input prompt. The user is then left to do what users do so well: type something at random.
Once the user hits Return, the game is once again afooting. GlkOte once again invokes your accept
function, passing in:
Game.accept({type: 'line', gen: 1, value: 'help', window: 2});
This indicates that the user entered a line of text, containing the string "help", in window number 2. And what does Game.accept
do? Why, exactly the same thing as before. It generates some more text, some more windows (if desired), a new input field; it stuffs all that information into a data structure; it passes it to GlkOte.update
. And once again, the library does all the necessary work to update the screen.
And the user enters more text, and the cycle continues.
If you will indulge an ASCII-art diagram:
+--------+ init +--------+ page +--------+ (page | GlkOte | event | Game | changes | GlkOte | loads) -> | init | ------> | accept | --------> | update | +--------+ +--------+ +--------+ +--------+ page +--------+ (user does input event | Game | changes | GlkOte | something) ----------------> | accept | --------> | update | +--------+ +--------+
(A few other kinds of events can hit your Game.accept
function. If you request character input, naturally, you'll get an event of type char
. If the user resizes the window, you'll get an event of type arrange
; you might want to redraw a status bar.)
gen
field) that shows up in these examples? It goes both directions, in fact; the library passes a generation number to Game.accept
, and you must pass one to GlkOte.update
. The generation number ensures that the library's display and the game state remain in sync.
This is not a big concern for a pure Javascript application. A browser executes Javascript in a single thread, with a synchronous flow of control. Each input event is handled before the next one arrives.
However, for RemGlk, life is more complicated. A RemGlk game is running at the far end of a daisy-chain of HTTP requests, CGI invocations, and so on. Messages can get delayed or crossed in transit.
Therefore, it is Game.accept
's responsibility to maintain a numeric generation number. Each time it calls GlkOte.update
, it must increase the number by at least one. If the library sees an update arrive with an out-of-order generation number, it knows to ignore the update.
(To be strictly pedantic, your game only needs to bump the generation number if something has actually changed. If there are no page changes at all, the generation may remain unchanged too. This might happen if the user hits "reload" in his web browser; the game would then have to send him a fresh update, even though nothing has happened in the interpreter.)
The generation number must be checked in the other direction, as well. When the library calls Game.accept
, it includes the generation number of the last update it got. If you see that this does not match your idea of the game state, you should ignore the input event. (This might happen if the user sends some input, and then sends some more before you have processed the first event. Both events would have the same generation number. So you'd process the first one, bump your generation number, send an update back; then the second event would arrive, you'd see the unbumped number, and ignore the event.)
windowport
. You specify its location by pixels (x, y, width, height). There are currently two types of windows: grid windows and buffer windows.
A grid window is a rectangular grid of fixed-width characters. (This is the upper window or status bar of most IF games.) You update a grid window by defining the lines of text that it contains. You can update any or all of a grid window's lines at a time.
A buffer window is a scrollable pane of text. (This is the lower window or story window of most IF games.) You update a buffer window by adding text to the end. You cannot change text that already appears in the buffer window, except by clearing the entire window and starting over.
Each window may have one input field. You can tell a window to accept character input (a keystroke), line input (a line of text), or no input (in which case there is no input field).
(Most IF games only have one input field at a time. However, GlkOte supports accepting input from multiple windows at the same time.)
(It is legal to have no input fields at a given time. But the game is then (mostly) paralyzed, because the player cannot type anything to change the fact that there are no input fields. You will generally only do this when a game has ended.)
An input field in a buffer window (whether it is line or character input) always appears at the end of the window's text. An input field in a grid window can be positioned anywhere in the grid.
You cannot update a window which is awaiting input. If you try, a fatal error message will pop up. If you need to do this, you must remove the window's input field, update the window contents, and then post a new input field. (The GlkOte.update
API makes this fairly simple;
see "The Input Update Array".)
The GlkOte model is simpler because it is intended to be hooked up to RemGlk. In that model, the game (running under RemGlk) defines the usual window split tree. RemGlk uses that tree, together with the metrics information provided by GlkOte, to determine the position of each window. It then passes that position information along to GlkOte. The GlkOte library doesn't need to know why the windows are where they are; it just needs the position of each window.
If you are not using RemGlk, this makes your life somewhat harder. You will have to compute the window positions on your own. You do not have to implement the full Glk window tree, but you do have to respect all of the window metrics, which are moderately complicated. See "Laying Out Windows".
GlkOte
object. The following properties and functions are available:
GlkOte.version
A string representing the library version number. This will be of the form "X.Y.Z".
GlkOte.init() GlkOte.init(interface)
Call this to begin the game. Until this is called, the gameport
displays the "Loading" pane.
You can optionally pass a game interface object as the argument. This tells the library to use the given interface, instead of looking for a global Game
object.
See "The Game Interface Object".
GlkOte.inited()
Returns true if GlkOte.init()
has been called successfully.
GlkOte.update(obj)
Tell the library to update the display. The argument is a complex object which includes information on updating the windows, window contents, and input fields. See "Updating the Display".
GlkOte.extevent(val)
Tell the library to cause a special input event, immediately. This is routed through Game.accept
, just like all other input events. (See "Input".) The argument is passed through unchanged into the input event object.
This function lets you handle external conditions (such as timers, or events caused by other libraries) in the same framework as GlkOte's events. You should only call GlkOte.extevent
while waiting for input -- that is, in the interval after a GlkOte.update
call.
GlkOte.getlibrary(val)
Return one of the library API objects that this library is using. Currently, the only valid argument is the string 'Dialog'
. You will get back the dialog.js library associated with this library.
GlkOte.getinterface()
Return the game interface object that the library is using. This is the global Game
object, unless you passed something else to GlkOte.init
. See "The Game Interface Object".
This exists in case another library (such as dialog.js) wants to imitate some of GlkOte's display preferences.
GlkOte.getdomid(val)
Check the DOM element id that the library is using for a given purpose. The argument should be one of the strings 'windowport', 'gameport', 'errorpane', 'errorcontent', 'loadingpane'
.
By default, you will get the same value back. However, if these have been customized in the game interface object (see below), you will get the customized value. For example, if Game.windowport
was passed a string, GlkOte.getdomid('windowport')
will return that string.
(You don't need to use this if you're copying the standard HTML layout.)
GlkOte.setdomcontext(domelement) GlkOte.getdomcontext()
Set and get the DOM context. This is the jQuery element within which all Glk DOM elements are looked up. (See selector context in the jQuery docs.)
In normal usage this is always undefined; DOM elements are searched for within the entire document. However, you might want to detach the Glk DOM and maintain it off-screen. To do this, set the DOM context to the detached element.
(You don't need to use this if you're copying the standard HTML layout.)
GlkOte.error(msg)
Display a message to the user. It appears in a pane overlaid on the game's windowport
. The error pane cannot be dismissed without reloading the page, so this should be reserved for fatal errors -- the server is not responding, an interpreter bug or assertion, etc.
(The standard error-pane style goes translucent on hover, so that the player can read the entire display after the error.)
GlkOte.warning(msg)
Display a warning message to the user. As with GlkOte.error
, it appears in an overlaid pane, but in a less threatening color. Unlike an error, a warning can be removed -- call GlkOte.warning()
with no argument. An error overrides a warning.
This can be used for temporary interruptions, or for an end-of-session message. (Permanent but not alarming.) It should not be used for debugging output. Do not rely on the translucent-on-hover behavior to expect the player to keep playing while the warning is visible.
GlkOte.log(msg)
Send a message to the browser's Javascript console, if possible. (If it is not possible, the message is silently dropped.) This currently works on Safari, Opera, and Firefox (only if Firebug is installed).
Log messages are not visible to the user unless he explicitly calls up the console display. This function is intended for debugging only. A final release of a game or interpreter should only call it in nonfatal error cases.
Game.accept
function will be called each time the user does something. (Starting the game counts as "doing something".) It will be given a single argument, an object.
The argument will always have type
and gen
properties. The type
describes what kind of event has happened. The gen
is a generation number; it is copied from the last GlkOte.update
call that the library got.
{ type: 'init', gen: 0, support: ['timer', 'graphics', 'hyperlinks'], metrics: METRICS_OBJECT }
Game initialization. This includes a capabilities list (see "The Application's Life Story") and the window metrics (see "The Metrics Object"). Since this arrives before you do any updates, the generation number will be zero.
{ type: 'line', gen: GENERATION, window: WINDOW_ID, value: STRING, terminator: CHAR }
Line input in the named window. The value
property will be the line which the user typed. (It will not have a terminal newline.)
If line input was entered in the normal way (typically the Enter key), the terminator
property will be absent. If line input was terminated by a special terminator keystroke (see "The Input Update Array"), its name will be in the terminator
property. This will be one of the following strings:
escape, func1, func2, func3, func4, func5, func6, func7, func8, func9, func10, func11, func12
{ type: 'char', gen: GENERATION, window: WINDOW_ID, value: CHAR }
Character input in the named window. The value
property will be the character which the user typed. If this is a printable character (or space), the value will simply be that character (a string of length one). If it was a special key, the value will be one of the following strings:
left, right, up, down, return, delete, escape, tab, pageup, pagedown, home, end, func1, func2, func3, func4, func5, func6, func7, func8, func9, func10, func11, func12
(Not all browsers can trap all of these special keys. If you want to wait for one of them, it is wise to accept printable alternatives. For example, accept N(ext) and P(revious) as alternatives to up
and down
.)
{ type: 'hyperlink', gen: GENERATION, window: WINDOW_ID, value: VALUE }
Hyperlink click in the named window. The value
property will be the target value for the link (passed in when the link was created).
{ type: 'mouse', gen: GENERATION, window: WINDOW_ID, x: NUMBER, y: NUMBER }
Mouse click in the named window. The x
and y
fields are a character position (for a grid window) or a pixel position (for a graphics window).
{ type: 'arrange', gen: GENERATION, metrics: METRICS_OBJECT }
Resize event. The gameport
has changed size, and so the game window sizes must be recomputed. The event provides the new window metrics. (This event may also happen if the display font has changed, or something else which affects the metrics.)
Note that each metrics object is complete; they do not contain incremental changes.
{ type: 'redraw', gen: GENERATION, window: WINDOW_ID }
Redraw event. The contents of a graphics window have been lost and must be redrawn. If the window
field is absent, all graphics windows must be redrawn. The affected windows are already cleared to their current background color.
{ type: 'specialresponse', gen: GENERATION, response: TYPE, value: VALUE }
A special response, responding to a special input request. See "Special Input Requests". The response
field indicates the request type. (The only response
value currently supported is "fileref_prompt"
.)
{ type: 'external', gen: GENERATION, value: VALUE }
An external event, which you triggered by calling GlkOte.extevent
. The value
property will be whatever you passed to GlkOte.extevent
.
{ type: 'debuginput', gen: GENERATION, value: STRING }
A debug command. See "Debug Commands".
{ type: 'refresh', gen: GENERATION }
Browser refresh. You receive this if the library has missed some updates, or if you sent a retry
response. The gen
number in this input is not up to date; it is some old value (perhaps even zero). Your job is to send an update which includes everything which has happened since then.
A pure Javascript game will not get refresh
events, because it will never send retry
responses and will never get out of sync with the library.
arrange
event will usually interrupt line input in the main window. If you have input fields in two windows at once, then an input event in one window will interrupt the other.)
If you plan to leave the input field in place, this is not a problem. Ignore the partial input; the library will leave the input field alone.
However, you might want to cancel that input field and then restart it. (If you want to update that window, you must do this -- see "The Display Model".) To restart input without annoying the user, you have to know what the user has typed so far in that field.
The library supports this by providing a partial input object for any interrupted line input. This may occur in any input event object. (Except of course for init
events -- at the beginning of the game, there are no input events to interrupt.)
A partial input object contains one property for each window with partial input; the property name is the window ID, and the value is whatever the user has typed so far. (Note that even though the object has numeric keys, it is a Javascript object, not a Javascript array.)
For example, if the user has typed "get la" in a line input field in window 2, and then the user resizes the window, you would get the input:
{ type: 'arrange', gen: GENERATION, metrics: METRICS_OBJECT, partial: { 2: 'get la' } }
The input object will only have a partial
property if there is partial input for at least one window. A window only produces partial input data for line input (not character input), and only if the player has actually typed something (the value will not be the empty string). Also, a line input event for a window will not contain partial input for that same window!
GlkOte.update
exactly once in each invocation of your Game.accept
function. (If you don't call GlkOte.update
, the game will stop accepting input. Calling GlkOte.update
more than once is not illegal, but it is inefficient. It just forces the library to do more DOM work than the ultimate result requires.)
GlkOte.update
takes one argument, an object. This can take a few different forms:
{ type: 'pass' }
Do nothing.
{ type: 'error', message: STRING }
A fatal error. This is equivalent to calling GlkOte.error
.
{ type: 'retry' }
This tells the library that you are still working, and it should send a refresh
event in a few seconds to see what you've come up with.
(See "Accepting User Events".)
A pure Javascript game will not use this. RemGlk uses it if the server is running too slowly.
{ type: 'update', gen: GENERATION, windows: WINDOWS_ARRAY, content: CONTENT_ARRAY, input: INPUT_ARRAY, timer: INTERVAL, disable: BOOLEAN, specialinput: OBJ }
This is the most common, and most complex, case. The gen
is a generation number; it must be higher than the generation number of the input event that caused this update.
The windows
, content
, and input
fields are optional. You may include all three, but it is better to include only the ones which are necessary for the update you are doing. It is legal to include none of these fields; but this indicates that the display has not changed, in which case you might as well not bump the generation number, in which case you might as well use type: 'pass'
.
The order of the values in the windows
, content
, and input
arrays is not significant.
The timer
value (if present and not null) sets a repeating timer. See "The Timer Update". If the flag is null, the timer is cleared. If the flag is absent, the timer not affected.
The specialinput
value (if present and not null) indicates that the game should stop and display a special input request. See "Special Input Requests". The game is otherwise disabled, just as if the disable
flag were set.
The disable
flag (if present and true) places the entire game into a disabled state. See "Disabling the UI". If the flag is absent or false, the UI will be enabled (or re-enabled).
windows
property of the update argument is an array listing all the windows which should be visible. If no windows have been created, destroyed, or changed position or size since the last update, you should omit the windows
field. (Omitting it is different from including an empty windows
array; that would cause all windows to be closed.)
Each element in the windows
array has one of the forms:
{ id: WINDOW_ID, type: 'buffer', rock: ROCK_VALUE, left: POS, top: POS, width: SIZE, height: SIZE } { id: WINDOW_ID, type: 'grid', rock: ROCK_VALUE, gridheight: LINES, gridwidth: ROWS, left: POS, top: POS, width: SIZE, height: SIZE } { id: WINDOW_ID, type: 'graphics', rock: ROCK_VALUE, graphwidth: SIZE, graphheight: SIZE, left: POS, top: POS, width: SIZE, height: SIZE }
The window ID is a (positive) integer which will identify the window. Do not reuse window IDs; each window you open should have a brand-new ID number.
The rock value can be any number or string. The only effect of the rock within GlkOte is to set a CSS class for the window div.
(See "The contents of the windowport
".)
The left
, top
, width
, and height
define the window's position within the windowport
. The values are in pixels.
See "Laying Out Windows"
for more than you wanted to know about determining these values.
A grid window must have gridwidth
and gridheight
values as well. These are measured in characters (the number of columns and rows in the character grid). These values should fit within the pixel width
and height
, when all of the grid character metrics are taken into account.
Graphics windows depend on the <canvas>
HTML element. Do not try to open a graphics window in an older browser which does not support <canvas>
.
A graphics window must have graphwidth
and graphheight
values. These are in pixels, and must fit in width
and height
when the metrics (graphicsmarginx/y
) are taken into account. A newly-opened graphics window is cleared to white, and its default fill color is white. If a graphics window is resized, it is cleared to its current default fill color.
input
property is an array listing all the windows which should be awaiting input. If no input fields have been cancelled or posted since the last update, you should omit the input
field. (Omitting it is different from including an empty input
array; that would cancel all input fields.)
Each element in the input
array has one of the following forms:
{ id: WINDOW_ID, hyperlink: BOOLEAN, mouse: BOOLEAN }
This indicates that there will be no line or character input in the given window. The (optional) hyperlink
and mouse
properties indicate whether there will be hyperlink or mouse-click input.
If both flags are false or missing, the window will accept no input at all. This is equivalent to leaving the window out of the input
array entirely.
{ id: WINDOW_ID, gen: GENERATION_NUMBER, type: 'char', hyperlink: BOOLEAN, mouse: BOOLEAN } { id: WINDOW_ID, gen: GENERATION_NUMBER, type: 'line', maxlen: MAX_INPUT_LENGTH, initial: STRING, terminators: KEY_ARRAY, hyperlink: BOOLEAN, mouse: BOOLEAN }
These forms indicate that there will be character or line input in the given window. The (optional) hyperlink
and mouse
properties indicates whether there will be hyperlink or mouse-click input.
Note that buffer windows do not support mouse-click input.
If this is a grid window, you must also include xpos
and ypos
properties, indicating where the char/line input field will begin. (An input field in a buffer window always appears at the end of the buffer's text.)
For line input, you must include the maxlen
property, indicating the maximum number of characters the player may enter. You may include an initial
property, giving a string to preload the input field with. You may also include a terminators
property, giving a list of names of keys that will terminate input. See above for the list of terminator key names. (The initial
and terminators
fields are optional, defaulting to the empty string or array.)
The gen
field must contain the generation number at which this input field was created. If the library does not know about it (i.e., if the field is not already present in the window) then the gen
makes no difference. However, if the field is present, then the gen
is basically distinguishing between two cases:
initial
and terminators
fields are ignored. In this case, you may not update the window's contents.
initial
field.
(The editing state and the initial
and terminators
properties are meaningless for character input. But you must still provide the gen
value, and -- if it is old -- obey the restriction about not updating the window.)
content
property of the update argument is an array listing all the windows which should be visible. If no windows have changed since the last update, you should omit the content
field. (Although an empty content
array is, for a wonder, the same as omitting it entirely.)
Each element in the content
array describes all the changes in one window since the last update.
{ id: WINDOW_ID, lines: [ { line: LINE_NUM, content: LINE_DATA_ARRAY }, { line: LINE_NUM, content: LINE_DATA_ARRAY }, ... ] }
Each element in the lines
array represents one line of the grid to update. (It is, of course, best to update only the lines which have changed.) The entire line is cleared and replaced with the content
of this element.
(To clear a line completely, you may use an empty content
array, or omit the content
field entirely.)
The content
describes the line in terms of runs of characters which have the same style. The format is described below.
{ id: WINDOW_ID, clear: BOOLEAN, text: [ { append: BOOLEAN, flowbreak: BOOLEAN, content: LINE_DATA_ARRAY }, { append: BOOLEAN, flowbreak: BOOLEAN, content: LINE_DATA_ARRAY }, ... ] }
If the clear
property is true, the buffer window is first cleared.
Then, each element in the text
array represents one line (or paragraph) to add to the buffer. If the element's append
property is true, the text is appended to the last line that currently exists in the buffer. If false, the text forms a new line. (If the buffer is empty, append
is ignored and the text becomes the buffer's first line.)
If flowbreak
is true, the paragraph is moved down below any floating (left or right) images.
The content
describes the line in terms of runs of characters which have the same style. The format is described below.
The clear
, append
, and flowbreak
values may be omitted; they default to false. Omitting text
is equivalent to an empty array. Within the text
array, the content
may be omitted if there is no data for the line. Therefore, an empty object {}
is a valid array entry; it indicates a blank line.
(For efficiency, only the first element of the text
array should have append
. Condense elements into complete paragraphs where possible.)
{ id: WINDOW_ID, draw: [ { special: 'setcolor', color: COLOR }, { special: 'fill' }, { special: 'fill', color: COLOR, x: NUMBER, y: NUMBER, width: NUMBER, height: NUMBER }, { special: 'image', image: NUMBER, x: NUMBER, y: NUMBER, width: NUMBER, height: NUMBER, url: STRING, alttext: STRING }, ... ] }
Each element in the draw
array represents a drawing operation, specified by the special
field.
setcolor
sets the (default) fill color. The color
field (required) should be a string containing a valid CSS color. (There are several possible formats; the most familiar looks like "#C0207F"
.) This does not change the window appearance, but it affects later fill
commands and window resizes.
fill
fills a rectangle of the graphics window with a color. If the color
field is given, it should be a valid CSS color; if omitted, the default fill color will be used. If the x, y, width, height
fields are given, they define the fill rectangle; if omitted, the entire graphics window is filled. (All four of these fields must be specified if any is.)
image
draws an image into the window. See below for the description of the image object.
(For efficiency, minimize overdraw where possible. It's a waste of time to have multiple window-filling operations in the same draw
array, or to draw content that is then erased by filling the window with a solid color.)
content
field, which contains an array of runs of characters. Each run in the array has the form
{ style: STYLE, hyperlink: VALUE, text: STRING }
This gives the name of a style, and a string of characters in that style. If the (optional) hyperlink
property is defined, the run is a hyperlink with that target value.
All these runs are concatenated to make the line. For grid windows, if this is short of the grid width, it is padded (on the right) with whitespace in the "normal" style. For buffer windows, no padding is needed.
Some windows can contain special inline entries, such as images in a buffer window. These are described by a "run" object with a special
field (and no text). See below.
You can also describe a text run in the array by giving two separate strings (the style and text values). This is a simpler format for a run; note that it cannot specify a hyperlink or a special entry. You can even mix the two run formats:
[ STYLE1, STRING1, { style: STYLE2, text: STRING2 }, STYLE3, STRING3, ... ]
Yes, parsing this is a bit of a nuisance. You have to examine the type of each entry to decide whether it's an object or (the first of) a pair of strings. Then, if it's an object, you have to look for a special
field. Sorry; the format has evolved over time.
special
value "image"
.
{ special: 'image', image: NUMBER, hyperlink: VALUE, alignment: ALIGNMENT, width: NUMBER, height: NUMBER, url: STRING, alttext: STRING }
In the simplest case, the url
field is a URL which refers to the image resource. (The URL can be absolute or relative to the document. However, absolute is preferred, in case the update stream is recorded and stored on a different server.) If the application uses GiLoad to locate image data, you may supply an image
number instead of a url
.
The width
and height
fields are required. If they do not match the image's native size, it will be resized to the given values.
The alignments are "inlineup"
, "inlinedown"
, "inlinecenter"
, "marginleft"
, "marginright"
. The margin alignments float left and right (in the HTML sense). The others are vertical alignments; the image hangs above, below, or centered relative to its line of text. If the alignment
is missing or unrecognized, the default is "inlineup"
.
The alttext
is recommended. If you do not supply this, GlkOte will create some generic alt text for the image.
The hyperlink
is optional. If defined, the image is a hyperlink with that target value.
An image entry in a graphics content array is similar:
{ special: 'image', image: NUMBER, x: NUMBER, y: NUMBER, width: NUMBER, height: NUMBER, url: STRING }
Here the x
, y
, width
, height
fields are all required. The x
and y
fields give the position of (the top left corner of) the image. There is no alignment
, hyperlink
, or alttext
.
timer
property of the update must be null or a positive number. If a number, it sets a repeating timer; the game will receive a timer
input event every N milliseconds from now on. If the timer
property is null, the timer is cancelled (if it is running).
Note that a positive timer
will restart the repeating timer from zero, even if the previous interval was the same as the new one. To allow the timer to continue running across updates, omit the timer
property entirely.
Also note that older versions of GlkOte did not support the timer
property. Therefore, older game code had to set up its own timeouts and call GlkOte.extevent
to trigger a timer event. This remains a valid approach.
Commands sent from this channel arrive as user events of this form:
{ type: 'debuginput', gen: GENERATION, value: STRING }
When the game generates debug output, include a debugoutput
list in your GlkOte.update
call:
debugoutput: [ TEXT, TEXT, ... ]
Note that the value must be a list of strings, each representing one line of debug output. (It may be an empty list, but do not use a string with no list around it.) Any update may include a debugoutput
list, not just responses to debuginput
.
To do this, include a specialinput
object in your GlkOte.update
call:
specialinput: { type: 'fileref_prompt', filemode: MODE, filetype: TYPE, gameid: VALUE }
The filemode
must be one of the strings read
, write
, readwrite
, writeappend
. These indicate what operation will be done with the file. (This affects whether a selection or creation dialog box is displayed.)
The filetype
must be one of the strings data
, save
, transcript
, command
. These indicate what type of file is being requested. (This affects the prompt displayed in the dialog box.)
The gameid
is a string which identifies what game is being played. This is used to filter the files listed in the dialog box, so that only files appropriate to the game are included. The gameid
does not have to be human-readable; it could be (for example) a base64-encoded dump of the game's header.
The gameid
field is optional. In general, you should only include it for the save
filetype. (Save-game files are game-specific, but data files can be exchanged between games.)
When the user makes a selection, you will get a user event in this form:
{ type: 'specialresponse', gen: GENERATION, response: 'fileref_prompt', value: VALUE }
If the value
is missing or null, the user hit Cancel. Otherwise, the value
will be a fileref object. This can be used to read or write data to HTML browser storage. (See the documentation in the dialog.js library.)
Note that if GlkOte is connected to a server-side library (such as RemGlk), browser storage is not an appropriate solution. In that case, you would not use dialog.js, but an alternative library (not yet implemented). The user would simply be prompted for a filename, and value
would contain this filename as a string.
GlkOte.update
with an update that includes the disable
flag. The display will be updated as you request, but it will also be disabled. All input fields will be disabled (given the HTML disabled
attribute). All input events, including keyboard input and hyperlinks, will be suppressed. Scrolling will work normally.
If the user resizes the window while the UI is disabled, you will not get an arrange
event immediately. The event will arrive after you re-enable the UI.
(You might disable the UI to block game activity while prompting for input outside the GlkOte library. For example, while prompting for a save-game location using the dialog.js library. It is also reasonable to disable the UI when the game is over, on the final game update.)
The UI will remain disabled until the next GlkOte.update
call which lacks the disable
flag.
The easy way to do this is to create a global object (i.e., an object at the top level of your script) called Game
(with a capital G). GlkOte.init
will see this object and take it as the game interface.
If you don't want to clutter your top-level namespace, you can instead pass an object as an argument to GlkOte.init
. The object can be named anything (or even be an anonymous object). If you do this, the library accepts the object you pass, and doesn't look for a global Game
.
The game interface object may have the following properties:
Game.accept()
This is the only property you must provide. Your Game.accept
function receives input events, and responds to them by updating the display.
See "Accepting User Events".
Game.gameport
This string defines the id of the gameport
element -- the HTML div which contains GlkOte's content. The default value is "gameport". You can set this to something else if your HTML document structure requires the game to be built inside a different div.
Game.windowport
This string defines the id of the windowport
element -- the HTML div which contains everything that the player sees. The default value is "windowport". You can set this to something else if your HTML document structure requires the game to be built inside a different div.
Game.loadingpane
This string defines the id of the loadingpane
element -- the HTML div which contains the initial "Loading..." banner and spinner image. The default value is "loadingpane".
Game.errorpane, Game.errorcontent
This string defines the id of the errorpane
and errorcontent
elements -- the HTML divs which display warnings and fatal errors. errorpane
is set display:none
until an error needs to be displayed. The text errorcontent
is then set to the error message. The default values are "errorpane" and "errorcontent".
(It's awkward to use two special element ids here, but that's how I set it up way back when.)
Game.dom_prefix
This string is applied as a prefix to the ids of all divs created by GlkOte, such as window1
, window2
, etc. (It is not applied to the elements mentioned above such as windowport
.) The default is "". You can set this if you already have HTML elements with these ids and want to avoid collisions.
Game.spacing
This value defines the number of pixels of blank space around each window (and also between windows). It is optional, and defaults to zero. See "Spacing, borders, and padding".
Game.inspacing, Game.outspacing, Game.inspacingx, Game.inspacingy, Game.outspacingx, Game.outspacingy
More precise control of the spacing value. See "Spacing, borders, and padding".
Game.detect_external_links, Game.regex_external_links
If detect_external_links
is set, GlkOte will detect URLs in the displayed text, and turn them into external hyperlinks. regex_external_links
allows you to customize the URL detector.
See "External hyperlinks".
Game.localize
If localize
is set, it will be used to provide values for GlkOte's fixed (English) strings. The default values are:
{ glkote_more: "More", glkote_taphere: "Tap here to type", }
You may replace any or all of these.
Note that dialog.js has many more localizable strings, which may be provided in the same localize
object. See that library's source code for a list.
Game.recording_url, Game.recording_handler, Game.recording_format, Game.recording_label
If recording_url
is set, GlkOte will forward every command (input and output) to that URL as an AJAX JSON POST request. To do something else with the command, you can instead set recording_handler
to an arbitrary Javascript function.
By default, the sent object (or recording_handler
argument) looks like:
{ "format": "glkote", "input": {input event object; see above}, "output": {output update object; see above}, "sessionId": "string of digits; identifies game session", "label": "the recording_label parameter, or the game title or URL", "timestamp": timestamp when input received, "outtimestamp": timestamp when output generated }
Timestamps are always from the Unix epoch in milliseconds. (E.g., the result of Javascript `Date.getTime()`.)
(See also "The .glktra File Format", below.)
If recording_format
is set to "simple"
, the "input"
and "output"
fields will be replaced with plain strings ("output"
will contain newlines). In this format, style information is lost, as is the distinction between line and character input, etc.
(Regardless of configuration, the player can disable transcript recording by adding ?feedback=0
to the URL.)
windows
property for GlkOte.update
, you must give the position (left and top coordinates) and the size (width and height). But how do you know what they should be?
It's easy to say "my status window should be as wide as the gameport". It gets a bit harder when you have to take the Game.spacing
into account. When you say "my status window should be tall enough to display one row of characters in its fixed-width font," that's when the headache begins.
The information you need to work this stuff out comes from the metrics object. The library provides this to you at startup time (and also when the window is resized). You still have to do some math to make everything work, but let's take a look at...
metrics.width, metrics.height
The width and height of the gameport
(and also the windowport
). Your game windows should fill all of this area, aside from margins and spacing.
metrics.outspacingx, metrics.outspacingy
The width and height of the space around the windows -- that is, between the windows and the edge of the gameport
. outspacingx
is the width of the left and right edges; outspacingy
is the height of the top and bottom edges.
metrics.inspacingx, metrics.inspacingy
The width and height of the space between adjacent windows. inspacingx
is the width of the vertical space between horizontally adjacent windows. inspacingy
is the height of the horizontal space between vertically adjacent windows.
You define all these spacing values -- they're taken straight from your game interface object. Note that it doesn't matter how you defined them. If you just provided a Game.spacing
property, the metrics will still define outspacingx
, outspacingy
, inspacingx
, and inspacingy
; they'll just all be the same number. If you don't define any spacing at all, all four properties in the metrics will be zero.
metrics.gridcharwidth, metrics.gridcharheight
The size of a single character in the grid window's normal font. (Note that these are not necessarily integers. In particular, gridcharwidth
is a fractional value in many modern browsers.)
metrics.gridmarginx, metrics.gridmarginy
The amount of space consumed by a grid window aside from the characters it contains. gridmarginx
is the total width of the left and right padding, the left and right border, and so on. gridmarginy
is the same for the top and bottom.
The idea is that you can find the total width required for a grid window by computing (columns * gridcharwidth + gridmarginx)
. And the total height is (rows * gridcharheight + gridmarginy)
.
metrics.buffercharwidth, metrics.buffercharheight
The character measurements for buffer windows. Of course, since buffer windows typically have propertional-width fonts, the buffercharwidth
is only an approximation. Even the buffercharheight
can be inaccurate, since you can define buffer styles with different font sizes. (The measurements are made on digits in the normal buffer font style. Digits are fixed-width in most fonts.)
metrics.buffermarginx, metrics.buffermarginy
The extra space consumption for buffer windows.
metrics.graphicsmarginx, metrics.graphicsmarginy
The extra space consumption for graphics windows.
{ left: metrics.outspacingx, top: metrics.outspacingy, width: metrics.width-(2*metrics.outspacingx), height: metrics.height-(2*metrics.outspacingy) }
The outspacing surrounds the window on all four sides, and so the width and height are each diminished by twice the spacing value.
(You don't have to give bottom
or right
values. However, the implicit rules are, for each window:
metrics.width = (width + left + right) metrics.height = (height + top + bottom)
In the one-window case, the left
and right
values are both metrics.outspacingx
; the top
and bottom
values are both metrics.outspacingy
.)
If this were a grid window, how many rows and columns would it have? (We need to supply these as the gridwidth
and gridheight
in the window definition.) We've just computed the window's width and height, but those values include the gridmarginx
and gridmarginy
-- the extra space consumed by the window padding and border. We have to subtract those off before dividing by the character size.
{ <see above>..., gridwidth: Math.floor((width-metrics.gridmarginx) / metrics.gridcharwidth), gridheight: Math.floor((height-metrics.gridmarginy) / metrics.gridcharheight) }
(The gridwidth
and gridheight
have to be integers, so we round down with Math.floor
.)
To create a window that doesn't stretch the full height of the port, we need to do a little more work. Say we want a three-line-height status window. This would look like:
{ left: metrics.outspacingx, top: metrics.outspacingy, width: metrics.width-(2*metrics.outspacingx), height: 3*metrics.gridcharheight + metrics.gridmarginy, gridwidth: Math.floor((width-metrics.gridmarginx) / metrics.gridcharwidth), gridheight: 3 }
The left and width parameters have not changed. The height is now defined as three times the character height, plus the extra space needed by the gridmarginy
. And the gridheight
, of course, is three.
Naturally we want a story window below this three-line status window. Where does it go?
var storytop = (metrics.outspacingy + (3*metrics.gridcharheight+metrics.gridmarginy) + metrics.inspacingy)
The top
coordinate of this window is the sum of everything that lies above it: the outer spacing, plus the full height of the status window, plus the inner spacing. (The inner spacing, remember, is the measurement between the two windows.)
With this in hand, we can define the story window:
{ left: metrics.outspacingx, top: storytop, width: metrics.width-(2*metrics.outspacingx), height: metrics.height-(storytop+metrics.outspacingy) }
The height value comes straight from the rules mentioned above; the implicit bottom
value is metrics.outspacingy
RemGlk contains code to compute all the window sizes, based on the constraint mechanisms of the Glk API. It gets all of the math right, and then passes it on to GlkOte. This means that GlkOte doesn't contain any math cleverness.
One could imagine rewriting that constraint code in Javascript, and including it in GlkOte as a helper function. Pure Javascript games could use this helper (thus getting perfect windows without any effort), while RemGlk would just bypass it. This is probably the right way to do things, but I haven't done it. Yet. Sorry.
glkote.css
file,
included in the GlkOte library. You can either modify this file on your web site, include an additional CSS file in your page, or put CSS styles directly in your page header.
Most of glkote.css
is free for you to play with. However, some elements should be left alone, lest you break GlkOte's assumptions. In general, you can freely modify appearance properties -- color, fonts, text styles and sizes. You must be more careful with layout properties -- position, width, height, margin, and so on. Do not mess with these unless this document says it's safe.
(The glkote-demo2.css
file is an example of a modified stylesheet. You can see its effects in the sample-demo2.html
demo.)
gameport
<div id="gameport"> <div id="windowport"> ...windows... <div id="loadingpane"> <div id="errorpane">
The gameport
is the box that contains all of GlkOte's functionality. You place the gameport
on your page, and GlkOte does all its work inside it. This is the big exception to the rule abot layout properties. You can position gameport
by any means: set its width and height (or max-width, or left and right), apply borders and margins, anything you want. Just make sure that gameport
's size is completely determined by your CSS properties. GlkOte fits its content inside the gameport
; it does not expand the gameport
to fit the content.
The appearance properties of the gameport
are mostly irrelevant, since no text appears directly in it. However, you may wish to customize its background
color. This is the color that appears around windows, and between them, if you set a non-zero window spacing.
(See "Spacing, borders, and padding".)
There are three divs stacked inside gameport
:
The windowport
contains all of the game windows -- everything the player sees in the normal course of play. It is the same size as the gameport
. We will discuss it further in a moment.
The loadingpane
contains the spinny compass and the sign "Loading..." This div is hidden (CSS property display:none
) the first time you call GlkOte.update
, so it won't be bothering you.
The errorpane
is a red-bordered panel which appears at the top of the gameport
when a fatal error occurs. Until that happens, it is hidden, so it won't be bothering you either.
You should not modify the CSS positioning of windowport
, loadingpane
, or errorpane
. You may modify their appearance, if you want.
(All of these unique element ids may be changed with interface options.)
windowport
<div id="windowport"> <div id="window1" class="WindowFrame GridWindow WindowRock_123"> <div id="win1_ln0" class="GridLine"> <div id="win1_ln1" class="GridLine"> <div id="window2" class="WindowFrame BufferWindow WindowRock_456"> <div class="BufferLine"> <div class="BufferLine"> <div class="BufferLine"> <div class="BufferLine">
Each window is a div, absolutely positioned in the windowport
. The id
of the window identifies it, using the integer identifier you created it with.
As you see, each window has three CSS classes. WindowFrame
is a general class which applies to all windows. GridWindow
, BufferWindow
, or GraphicsWindow
identifies the format of the window; you can use these classes to adjust the properties of all windows of a given type. (For example, the default stylesheet ensures that grid windows have a fixed-width font by putting font-family: monospace
in the GridWindow
class definition.)
The WindowRock_...
class identifies the window by the rock value you created it with. (If you didn't give a rock, it gets the class WindowRock_undefined
.) You can use this to adjust the appearance of a single window.
Or, you could use the id
value. Why does the rock value exist? If you're writing your game in pure Javascript, there isn't much reason -- the id
and the rock serve the same function. However, if you're using a library such as RemGlk, you probably have no control over the id
value. That's chosen by the library; it may not be the same from one game to the next; and it will never be reused within a game. (That is, if RemGlk closes a window and then reopens it, the new one will have a different id
.) The rock value is always under your control.
GridLine
and BufferLine
classes, but this doesn't really let you do anything you couldn't do with GridWindow
and BufferWindow
.
Each line div contains a series of spans. Each span has a class indicating its style. Style_normal
for normal text, or Style_header
, Style_emphasized
, or so on -- whatever style you specified when you updated the window. You can adjust the appearance of each style class as you wish.
Since grid windows are grids, you must make sure that all styles in all grid windows are monospaced fonts. Equally importantly, they must all be the same point size. (You cannot have one grid window with a 18-point font, and another with a 14-point font.) It is safest to set the font-family
and font-size
in the GridWindow
class, and not touch them in individual style classes. If you want to change the font size of a buffer window style, use a combined selector to ensure that you don't disturb the grids:
.BufferWindow .Style_header { font-size: 14px; }
Because style classes only apply to spans, not divs, there is currently no way to change the text-align
or text-indent
of a style. This is a bug. (You can still set these properties on an entire window.)
detect_external_links
property on your
game interface object:
Game = { accept: accept, detect_external_links: 'search' };
The value of the property controls the behavior of the URL detection. You have two choices:
detect_external_links: 'search'
Any URL in the output text is detected, as long as it's not split by a newline, space, or style change. Each URL will be displayed as a hyperlink with class External
and target _blank
. (The _blank
ensures that the linked page will open in a new window.)
Note that this just linkifies URLs; you cannot use this feature to apply an external hyperlink to an arbitrary word. Also, this external URL feature is completely separate from Glk's hyperlink feature. Glk hyperlinks are discussed under The Content Update Array. A span of text which is marked as a Glk hyperlink cannot also be an external hyperlink.
The regex employed for the 'search'
detector is this terrifying agglomeration:
\b((?:https?://)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+ (?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\) |[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))
This basically looks for any URL-like string beginning with http:
or https:
, ignoring case. The complications come from being smart about closing punctuation; if a URL appears in a sentence, followed by a period, it will try to exclude the period. This is derived from John Gruber's URL regex (the "web URL only" variant). I made one change: unlike Gruber's, this does not attempt to recognize bare domain names like www.eblong.com
.
If you want to customize the regex, you can assign a new regex object to the regex_external_links
property on your game interface. You can use any regex, as long as it doesn't try to accept the empty string; whatever it accepts will be treated as a URL. (Do not use the 'g'
global-search flag for this regex. GlkOte is able to detect multiple URLs in a line anyway.)
detect_external_links: 'match'
The 'match'
detector is a much simpler and faster test, but it can only detect a URL which is a complete span in the output. Therefore, for a game to take advantage of this feature, you must either print the URL on a line by itself, or you must change the style immediately before and after the URL. (Either way, you must exclude all leading and trailing spaces and punctuation.) In Inform 7, for example, you might write:
say "Please see [fixed letter spacing]http://eblong.com/[variable letter spacing].";
This detector uses the regex /^https?:.*$/i
-- any string beginning with http:
or https:
, ignoring case. Again, you may customize this by setting the regex_external_links
property on your game interface object. Your regex must match at the start of the string (start with ^
); otherwise you will get surprisingly wrong results.
<input type="text">
elements. Line input fields have the Input
and LineInput
CSS classes; character input fields have Input
and CharInput
.
The default stylesheet makes an input field as invisible as possible; no highlighting, border, or background color. This is the usual style for IF.
In a grid window, the input field is a child of the GridWindow
div; it is absolutely positioned to the proper spot in the window. In a buffer window, the input field is contained in the last BufferLine
. (Not in one of the style spans, but in a special InvisibleCursor
span.) A buffer input field may be relatively or absolutely positioned, depending on which I could get to work right in a given browser.
The first rule is, do not set the CSS margin
property on GlkOte windows. The library cannot account for margins; your windows will come out the wrong size.
Rather than using CSS to space your windows apart, you will have to set a spacing
property on your
game interface object.
The property spacing:4
, for example, will leave a four-pixel margin around each window, and also between windows. If you want to control those values separately, you can set outspacing
(the outer margins around the windows) and inspacing
(the margins between adjacent windows). For even finer control, you can set outspacingx
and inspacingx
(the width of left and right margins) and outspacingy
and inspacingy
(the height of top and bottom margins).
The color of these margins, between and around your windows, is the background
color of the gameport
. Or the background
color of the windowport
, if that is set.
A window can have a CSS border
property. You specify the width, color, and style as part of the property value. For example, border: 2px solid red
would put a two-pixel red line around the window. (See the CSS spec for complete details.) If you specify both a spacing and a border, the spacing is measured from the outer edge of the border.
A window can also have a CSS padding
property. This is the distance between the inner edge of the border (if any) and the window content -- the text in the window. (This padding space is the same color as the window background. For graphics windows, that means it will start out white.) The default stylesheet defines a fair bit of padding; you don't want to skimp on it.
Within a window -- that is, within lines of text -- you are free to adjust the spacing however you want. The letter-spacing
and line-height
properties are safe. You can set margin
values on BufferLine
, but don't try it on GridLine
.
By the way, in a buffer window, paragraphs of text are separated by blank lines -- that is, empty paragraphs. (More precisely, a blank line is a paragraph with a single space in it, so that it doesn't collapse.) This is unlike the normal HTML model, in which paragraphs are <p>
tags separated by the CSS margin
property.
(If you think that's ugly, don't ask what I had to do to prevent runs of spaces from collapsing.)
First, we must create a separate gameport div
for each game. Each one needs its own unique element id, and the same is true for all of its parts. See
the sample HTML
for how this might look. In this example we use alpha_
and beta_
prefixes for each div
.
We also need a game configuration object for each game:
var GameA = { accept: SampleDemoAlpha.game_accept, dom_prefix: 'alpha_', dialog_dom_prefix: 'dialogalpha', gameport: 'alpha_gameport', windowport: 'alpha_windowport', errorpane: 'alpha_errorpane', errorcontent: 'alpha_errorcontent', loadingpane: 'alpha_loadingpane', detect_external_links: 'match', spacing: 4 }; var GameB = { accept: SampleDemoBeta.game_accept, dom_prefix: 'beta_', dialog_dom_prefix: 'dialogbeta', gameport: 'beta_gameport', windowport: 'beta_windowport', errorpane: 'beta_errorpane', errorcontent: 'beta_errorcontent', loadingpane: 'beta_loadingpane', detect_external_links: 'match', spacing: 4 };These specify all the
div
ids. They also provide unique dom_prefix
strings, so that when the games create window div
s, there's no collision. Finally, each configuration object must specify the accept
function for its own game. (In this example, each game is represented by a SampleDemo class instance.)
When starting up, you must create separate GlkOte
library instances using the GlkOteClass
constructor. Note that we will not be using the default window.GlkOte
instance at all. The example does this in the init_demo()
function:
var GlkOteA = new GlkOteClass(); var GlkOteB = new GlkOteClass();Then configure each of the game instances with its GlkOte instance and configuration object:
SampleDemoAlpha.game_init(GlkOteA, GameA); SampleDemoBeta.game_init(GlkOteB, GameB);The
game_init()
function takes care of calling GlkOte.init()
. This launches the games, so at this point everything is off and running.
The obvious way to do this is to leverage the recording format. (Indeed, Lectrote implements this by setting up an internal recording_handler
function.) Thus, the serialization format consists of a sequence of JSON stanzas, exactly as described above:
{ "format": "glkote", "input": {input event object; see above}, "output": {output update object; see above}, "sessionId": "string of digits; identifies game session", "label": "the recording_label parameter, or the game title or URL", "timestamp": timestamp when input received, "outtimestamp": timestamp when output generated }
We use .glktra
as the conventional filename suffix for this format. Note that a .glktra
file is not a valid JSON document; it's concatenated JSON documents.
To make parsing easier, we guarantee (and require) that every stanza ends with a newline. Of course JSON permits whitespace, including newlines, within a stanza; that doesn't cause any problems. But there must be at least one newline between stanzas (and at the end of the file).
The first stanza of the file may (optionally) be a metadata stanza:
{ "timestamp": timestamp when transcript began, "metadata": { "title": "Adventure", "author": "Crowther and Woods", ... } }
The fields of the metadata
may include (but are not limited to) the fields in the bibliographic
section of an iFiction record. See the Babel spec for more info. All fields (and the metadata stanza itself) are optional; this information is provided on a best-effort basis.
Note that this metadata stanza is not part of the regular GlkOte spec. The GlkOte library will not send it, and implementations like RemGlk will not accept it. (Probably they should accept and ignore it, for consistency, but this is not yet implemented.)
ifid
, format
, tuid
(if present in iFiction block).
localize
key in the game interface object.
max_buffer_length
config option, defaulting to 800 (lines or paragraphs) instead of the previous 200.
init/init_async
call which GlkOte invokes if present. (electrofs.js has init_async
; dialog.js currently has neither.)
white-space:pre-wrap
) instead of
. This will break on very old versions of IE, but we're not fussed about them any more.
'refresh'
.)
<pre>
tag, to work around a Firefox copy-paste bug.
support
array to the initial ('init'
) update request. (Earlier versions of GlkOte supported graphics and hyperlinks -- see below -- but omitted the support
field.)
ram
array. (For Ink/Lectrote.)
GlkOte.warning
API, to display temporary or non-alarming status messages.
extevent()
and interface()
calls. Added disable
flag to the update()
call.
GlkOte home page -- Glk home page