-
Notifications
You must be signed in to change notification settings - Fork 33
Quixe Without GlkOte
In theory, one can use Quixe (the quixe.js
library) without the rest of the Glk machinery (glkote.js
, glkapi.js
, gi_dispa.js
, and so on). That is, you can build a Javascript interpreter which uses the Glulx VM but a different display layer and loading mechanism.
In practice, this is slightly messy, because Quixe grew up as a GlkOte plugin and I never tried to use it in any other context.
The Glulx VM requires a display library to handle all its input and output. As shipped, Quixe uses the Glk display library, which is accessed through the @glk
opcode.
To replace this, you'll need to add a custom opcode which does the same things:
- Transform arguments from Glulx VM values (32-bit integers) into a form suitable for your display layer.
- Invoke a feature of your display layer.
- Possibly block (setting
resumefuncop
,resumevalue
,pc
, anddone_executing
). See the implementation of@glk
inquixe.js
. - Tranform any return values back into Glulx values.
Alternatively, you could continue to rely on the @glk
opcode and gi_dispa.js
library, but rip out all the existing Glk functions and install new ones. I have not thought through this plan, but it would permit more code re-use.
The Quixe library offers has just a few entry points. Your Javascript application must load the game, call Quixe.prepare
, and then call Quixe.init
.
Quixe.prepare(game, options)
Set up the game state.
game
must be a Glulx game file, not blorbed, encoded as an array of integers 0..255.options
is an (optional) group of execution options.
Quixe.init()
Begin running the game. The game will continue running until it exits or blocks awaiting input.
Quixe.resume()
Continue running the game after input is accepted. The game will continue running until it exits or blocks awaiting input.
Quixe.ReadByte(addr), Quixe.WriteByte(addr, val), Quixe.ReadWord(addr), Quixe.WriteWord(addr, val), Quixe.ReadStructField(addr, fieldnum), Quixe.WriteStructField(addr, fieldnum, val)
Utility functions for the part of your system which transfers Glulx values into and out of the VM. Note that, for these functions only, an
addr
of 0xFFFFFFFF means "write to/from the VM stack".
Quixe.SetResumeStore(val)
If the game is blocked on an operation which returns a value, call this before
Quixe.resume()
to pass the value in. (Yes, it would have been cleaner to callQuixe.resume(val)
, but that's not the way it evolved.)
Quixe assumes the existence of three global library objects, which it uses for all access to the outside world. (Quixe never touches the page DOM itself.) If you are writing a new Quixe-based interpreter, you'll have to provide your own objects with matching names and APIs.
(The required names are Glk-centric, for which I apologize. I may one day go through and rename everything to be generic-looking. But this is not a priority.)
The requirements below look messy, but there's really only a few pieces that you need to put in place. Basically, your display library has to be able to (1) handle text printing; (2) handle reading and writing to permanent storage, for save/load operations; (3) display errors.
We assume that your display library has the notion of a "current output destination". That is, game code will set output to some target (a stream, a channel, whatever you call it) and then print a bunch of text. Naive Inform print statements (say phrases, in I7) all funnel into the current output destination.
Some of these API fields exist only to support the @glk
opcode. If you are writing your own interpreter, your game presumably never calls this opcode. Thus you can omit these fields. On the other hand, you will probably need something analogous to support your own opcode.
Glk.fatal_error(msg)
Display the argument (string) prominently for the user, and then disable the display layer. (Cancel input events, etc.) The display layer should not call back into Quixe after this.
Glk.glk_put_jstring(str, simple)
Send the
str
argument (string) to the current output destination. This is used by opcodes such@streamstr
and@streamnum
. If thesimple
argument is true, then all the characters instr
are Latin-1 (in the range 0..255). Otherwise,str
might include higher Unicode.simple
is only a hint; you can ignore it if you don't care.
Glk.glk_put_char(val)
Send a character with the given numeric value to the current output destination.
val
will be an integer from 0 to 255. This is used by the@streamchar
opcode. This could be implemented as{ Glk.glk_put_jstring(String.fromCharCode(val), true); }
Glk.glk_put_char_uni(val)
Send a character with the given numeric value to the current output destination.
val
will be an integer. This is used by the@streamunichar
opcode. This could be implemented as{ Glk.glk_put_jstring(String.fromCharCode(val)); }
Glk.DidNotReturn
This should be a unique object; the contents do not matter. It is used by the
@glk
opcode to indicate that a Glk call did not return. If your game does not use the@glk
opcode, skip this.
Glk.call_may_not_return(val)
Return whether the Glk function number
val
is a blocking function. If your game does not use the@glk
opcode, skip this.
Glk.glk_put_buffer_stream(file, arr)
Write a byte array to a writable stream.
file
is an object returned by yourGiDispa.class_obj_from_id()
function (see below).arr
is an array of integers in the range 0..255. This is used by the@save
opcode, to write a save-game file to your library's notion of permanent storage.
Glk.glk_get_buffer_stream(file, arr)
Read a byte array from a readable stream.
file
is an object returned by yourGiDispa.class_obj_from_id()
function (see below).arr
is an array ofundefined
values. The function should read bytes (integers in the range 0..255) from your library's notion of permanent storage, and store them inarr
. Do not store more bytes than the originalarr.length
(that is, do not expand the array). Return the number of bytes so read -- this will be a number from 0 toarr.length
. If an error occurs, return -1. This is used by the@restore
opcode.
Glk.update()
Quixe calls this to signal that it is about to block and wait for input. Your library should set up DOM callbacks to accept input. When it arrives, call
Quixe.resume()
.
Glk.glk_exit()
Quixe calls this to signal that the VM has exited. Your library should take note and remember not to call
Quixe.resume()
again. (Glk.glk_exit()
will be followed by aGlk.update()
call.)
GiDispa.get_function(val)
Return a callable function for Glk function number
val
. The returned function is called with one argument, an array of Glulx VM values. If your game does not use the@glk
opcode, skip this.
GiDispa.class_obj_from_id(class, val)
Return the object used by your library for the given
class
andval
. This is used by the@save
and@restore
opcodes to locate your file objects, which are then passed toGlk.glk_put_buffer_stream
andGlk.glk_get_buffer_stream
.
GiLoad.find_data_chunk(num)
Locate a data chunk with the given number (from a blorb file, for example). This is only called if the
debug_info_data_chunk
execution option is set.
When the VM encounters a @save
opcode, it will:
- Call
GiDispa.class_obj_from_id("stream", val)
, whereval
is the value passed to the@save
opcode. - If this returns false/null/undef, the save fails.
- Construct a save-state file. This is encoded as an array of bytes (0..255).
- Call
Glk.glk_put_buffer_stream(file, arr)
, wherefile
is the object returned above andarr
is the save-state array. - Report that the save succeeded.
The @restore
opcode is analogous:
- Call
GiDispa.class_obj_from_id("stream", val)
, whereval
is the value passed to the@restore
opcode. - If this returns false/null/undef, the restore fails.
- Repeatedly call
Glk.glk_get_buffer_stream(file, arr)
, using a fixed-size buffer, until the call returns zero. - Try loading the resulting save-state into the VM.
- Report success or failure.