Skip to content

Quixe Without GlkOte

Andrew Plotkin edited this page Jun 3, 2015 · 3 revisions

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.

Designing your display layer

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, and done_executing). See the implementation of @glk in quixe.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.

Quixe's API

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 call Quixe.resume(val), but that's not the way it evolved.)

Your API

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: the display layer

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 the simple argument is true, then all the characters in str 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 your GiDispa.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 your GiDispa.class_obj_from_id() function (see below). arr is an array of undefined values. The function should read bytes (integers in the range 0..255) from your library's notion of permanent storage, and store them in arr. Do not store more bytes than the original arr.length (that is, do not expand the array). Return the number of bytes so read -- this will be a number from 0 to arr.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 a Glk.update() call.)

GiDispa: the dispatch layer

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 and val. This is used by the @save and @restore opcodes to locate your file objects, which are then passed to Glk.glk_put_buffer_stream and Glk.glk_get_buffer_stream.

GiLoad: the loading layer

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.

The @save / @restore procedure

When the VM encounters a @save opcode, it will:

  • Call GiDispa.class_obj_from_id("stream", val), where val 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), where file is the object returned above and arr is the save-state array.
  • Report that the save succeeded.

The @restore opcode is analogous:

  • Call GiDispa.class_obj_from_id("stream", val), where val 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.