§42   Devices and opcodes

This section covers the only opcodes which designers are likely to have occasional need of: those which drive powerful and otherwise inaccessible features of the Z-machine's “hardware”, such as sound, graphics, menus and the mouse. There's no need to be fluent in assembly language to use these opcodes, which work just as well if used as incantations from a unfamiliar tongue.

WARNING
Some of these incantations may not work well if a story file is played on old interpreters which do not adhere to the Z-Machine Standard. Standard interpreters are very widely available, but if seriously worried you can test in an Initialise routine whether your game is running on a good interpreter, as in the following code.

if (standard_interpreter == 0) {
    print "This game must be played on an interpreter obeying the
           Z-Machine Standard.^";
    @quit;
}

The library variable standard_interpreter holds the version number of the standard obeyed, with the upper byte holding the major and the lower byte the minor version number, or else zero if the interpreter isn't standard-compliant. Thus $002 means 0.2 and $100 means 1.0. Any standard interpreter will carry out the opcodes in this chapter correctly, or else provide fair warning that they cannot. (For instance, an interpreter running on a palm-top personal organiser without a loudspeaker cannot provide sound effects.) Here is how to tell whether a standard interpreter can or can't provide the feature you need.

FeatureVersionsAvailable if
auxiliary files5,6,8(true)
coloured text5,6,8((0->1) & 1 ~= 0)
input streams5,6,8(true)
menus6(($10-->0) & 256 ~= 0)
mouse5,6(($10-->0) & 32 ~= 0)
output streams5,6,8(true)
pictures6(($10-->0) & 8 ~= 0)
sounds5,6,8(($10-->0) & 128 ~= 0)
throw/catch stack frames5,6,8(true)
timed keyboard interrupts5,6,8((0->1) & 128 ~= 0)

For instance, if coloured text is essential (for instance if red and black letters have to look different because it's a vital clue to some puzzle), you may want to add a test like the following to your Initialise routine:

if ((0->1) & 1 == 0)
    print "*** This game is best appreciated on an interpreter
           capable of displaying colours, unlike the present
           one. Proceed at your own risk! ***^";

· · · · ·

Text flows in and out of the Z-machine continuously: the player's commands flow in, responses flow out. Commands can come in from two different “input streams”, only one of which is selected at any given time: stream 0 is the keyboard and stream 1 is a file on the host computer. The stream is selected with:

@input_stream number

The Inform debugging verb “replay” basically does no more than switch input to stream 1.

There are four output streams for text, numbered 1 to 4. These are: (1) the screen, (2) the transcript file, (3) an array in memory and (4) a file of commands on the host computer. These can be active in any combination, except that at all times either stream 1 or stream 3 is active and not both. Inform uses stream 3 when the message print_to_array is sent to a string, and streams 2 and 4 in response to commands typed by the player: “script on” switches stream 2 on, “script on” switches it off; “recording on” and “off” switch stream 4 on and off. The relevant opcode is:

@output_stream number arr

If number is 0 this does nothing. +n switches stream n on, -n switches it off. The arr operand is omitted except for stream 3, when it's a table array holding the text printed: that is, arr-->0 contains the number of characters printed and the text printed is stored as ZSCII characters in arr->2, arr->3, …

As the designer, you cannot choose the filename of the file of commands used by input stream 1 or output stream 4. Whoever is playing the story file will choose this: perhaps after being prompted by the interpreter, perhaps through a configuration setting on that interpreter.

▲▲ EXERCISE 122
Implement an Inform version of the standard ‘C’ routine printf, taking the form

printf(format, arg1, ...)

to print out the format string but with escape sequences like %d replaced by the arguments (printed in various ways). For example,

printf("The score is %e out of %e.", score, MAX_SCORE);

should print something like “The score is five out of ten.”

In Version 6 story files, only, @output_stream can take an optional third operand when output stream 3 is being enabled. That is:

@output_stream 3 arr width

If width is positive, the text streamed into the array will be word-wrapped as if it were on a screen width characters wide; if width is negative, then as if on a screen -width pixels wide. The text going into arr is in the form of a sequence of lines, each consisting of a word containing the number of characters and then the ZSCII characters themselves in bytes. The sequence of lines finishes with a zero word. Such an array is exactly what is printed out by the opcode @print_form arr.

· · · · ·

The Z-machine has two kinds of “screen model”, or specification for what can and can't be done to the contents of the screen. Version 6 has an advanced graphical model, whereas other versions have much simpler textual arrangements. Early versions of the Z-machine are generally less capable here, so this section will document only the Version 5 and Version 6 models. (Versions 7 and 8 have the same model as Version 5.)

The version 5 screen model. The screen is divided into an upper window, normally used for a status line and sometimes also for quotations or menus, and a lower window, used for ordinary text. At any given time the upper window has a height H, which is a whole number of lines: and H can be zero, making the upper window invisible. (The story file can vary H from time to time and many do.) When text in the upper and lower windows occupy the same screen area, it's the upper window text that's visible. This often happens when quotation boxes are displayed.

@split_window H

Splits off an upper-level window of the given number of lines H in height from the main screen. Be warned that the upper window doesn't scroll, so you need to make H large enough for all the text you need to fit at once.

@set_window window

Selects which window text is to be printed into: (0) the lower one or (1) the upper one. Printing on the upper window overlies printing on the lower, is always done in a fixed-pitch font and does not appear in a printed transcript of the game.

@set_cursor line column

Places the cursor inside the upper window, where (1, 1) is the top left character.

@buffer_mode flag

This turns on (flag==true) or off (flag==false) word-breaking for the current window: that is, the practice of printing new-lines only at the ends of words, so that text is neatly formatted.

@erase_window window

Blanks out window 0 (lower), window 1 (upper) or the whole screen (if window=-1).

Using fixed-pitch measurements, the screen has dimensions X characters across by Y characters down, where X and Y are stored in bytes $21 and $20 of the header respectively. It's sometimes useful to know this when formatting tables:

print "My screen has ", 0->$20, " rows and ", 0->$21, " columns.^";

Be warned: it might be 80 × 210 or then again it might be 7 × 40. Text printing has a given foreground and background colour at all times. The standard stock of colours is:

0current colour5yellow
1default colour6blue
2black7magenta
3red8cyan
4green9white

@set_colour foreground background

If coloured text is available, this opcode sets text to be foreground against background. (But bear in mind that not all interpreters can display coloured text, and not all players enjoy reading it.) Even in a monochrome game, text can be set to print in “reverse colours”: background on foreground rather than vice versa. Status lines are almost always printed in reverse-colour, but this is only a convention and is not required by the Z-machine. Reverse is one of five possible text styles: roman, bold, underline (which many interpreters will render with italic), reverse and fixed-pitch. (Inform's style statement chooses between these.)

EXERCISE 123
Design a title page for ‘Ruins’, displaying a more or less apposite quotation and waiting for a key to be pressed. (For this last part, see below.)

EXERCISE 124
Change the status line so that it has the usual score/moves appearance except when a variable invisible_status is set to true, when it's invisible.

EXERCISE 125
Alter the ‘Advent’ example game to display the number of treasures found instead of the score and turns on the status line.

EXERCISE 126
(From code by Joachim Baumann.) Put a compass rose on the status line, displaying the directions in which the room can be left.

▲▲ EXERCISE 127
(Cf. ‘Trinity’.) Make the status line consist only of the name of the current location, centred in the top line of the screen.

The version 6 screen model. We are now in the realm of graphics, and the screen is considered to be a grid of pixels: coordinates are usually given in the form (y,x), with (1,1) at the top left. y and x are measured in units known, helpfully enough, as “units”. The interpreter decides how large “1 unit” is, and it's not safe to assume that 1 unit equals 1 pixel. All you can tell is what the screen dimensions are, in units:

print "The screen measures ", $22-->0, " units across and ",
      $22-->1, " units down.^";

There are eight windows, numbered 0 to 7, which text and pictures can currently be printing to: what actually appears on the screen is whatever shows through the boundaries of the window at the time the printing or plotting happens. Window number −3 means “the current one”. Windows have no visible borders and usually lie on top of each other. Subsequent movements of the window do not move what was printed and there is no sense in which characters or graphics “belong” to any particular window once printed. Each window has a position (in units), a size (in units), a cursor position within it (in units, relative to its own origin), a number of flags called “attributes” and a number of variables called “properties”. If you move a window so that the cursor is left outside, the interpreter automatically moves the cursor back to the window's new top left. If you only move the cursor, it's your responsibility to make sure it doesn't leave the window.

The attributes are (0) “wrapping”, (1) “scrolling”, (2) “copy text to output stream 2 if active” and (3) “buffer printing”. Wrapping means that when text reaches the right-hand edge it continues from the left of the next line down. Scrolling means scrolling the window upwards when text printing reaches the bottom right corner, to make room for more. Output stream 2 is the transcript file, so the question here is whether you want text in the given window to appear in a transcript: for instance, for a status line the answer is probably “no”, but for normal conversation it would be “yes”. Finally, buffering is a more sophisticated form of wrapping, which breaks lines of text in between words, but which (roughly speaking) means that no line is printed until complete. Note that ordinary printing in the lower window has all four of these attributes.

@window_style window attrs operation

Changes window attributes. attrs is a bitmap in which bit 0 means “wrapping”, bit 1 means “scrolling”, etc. operation is 0 to set to these settings, 1 to set only those attributes which you specify in the bitmap, 2 to clear only those and 3 to reverse them. For instance,

@window_style 2 $$1011 0

sets window 2 to have wrapping, scrolling and buffering but not to be copied to output stream 2, and

@window_style 1 $$1000 2

clears the buffer printing attribute of window 1.

Windows have 16 properties, numbered as follows:

0y coordinate8newline interrupt routine
1x coordinate9interrupt countdown
2y size10text style
3x size11colour data
4y cursor12font number
5x cursor13font size
6left margin size14attributes
7right margin size15line count

The x and y values are all in units, but the margin sizes are in pixels. The font size data is 256*h+w, where h is the height and w the width in pixels. The colour data is 256*b+f, where f and b are foreground and background colour numbers. The text style is a bitmap set by the Inform style statement: bit 0 means Roman, 1 is reverse video, 2 is bold, 3 is italic, 4 is fixed-pitch. The current value of any property can be read with:

@get_wind_prop window prop -> r

Those few window properties which are not italicised in the table (and only those few) can be set using:

@put_wind_prop window prop value

Most window properties, the ones with italicised text in the table above, are set using specially-provided opcodes:

@move_window window y x

Moves to the given position on screen. Nothing visible happens, but all future plotting to the given window will happen in the new place.

@window_size window y x

Changes window size in pixels. Again, nothing visible happens.

@set_colour foreground background window

Sets the foreground and background colours for the given window.

@set_cursor line column window

Moves the window's cursor to this position, in units, relative to (1,1) in the top left of the window. If this would lie outside the margin positions, the cursor moves to the left margin of its current line. In addition, @set_cursor -1 turns the cursor off, and @set_cursor -2 turns it back on again.

@get_cursor arr

Writes the cursor row of the current window into arr-->0 and the column into arr-->1, in units.

@set_font font -> r

(This opcode is available in Versions 5 and 8 as well as 6.) Selects the numbered font for the current window, and stores a positive number into r (actually, the previous font number) if available, or zero if not available. Font support is only minimal, for the sake of portability. The possible font numbers are: 1 (the normal font), 3 (a character graphics font: see §16 of The Z-Machine Standards Document), 4 (a fixed-pitch Courier-like font). Owing to a historical accident there is no font 2.

@set_margins left right window

Sets margin widths, in pixels, for the given window. If the cursor is overtaken in the process and now lies outside these margins, it is moved back to the left margin of the current line.

“Interrupt countdowns” are a fancy system to allow text to flow gracefully around obstructions such as pictures. If property 9 is set to a non-zero value, then it'll be decremented on each new-line, and when it hits zero the routine in property 8 will be called. This routine should not attempt to print any text, and is usually used to change margin settings.

EXERCISE 128
(Version 6 games only.) Set up wavy margins, which advance inwards for a while and then back outwards, over and over, so that the game's text ends up looking like a concertina.

Here are two useful tricks with windows:

@erase_window window

Erases the window's whole area to its background colour.

@scroll_window window pixels

Scrolls the given window by the given number of pixels. A negative value scrolls backwards, i.e., moving the body of the window down rather than up. Blank (background colour) pixels are plotted onto the new lines. This can be done to any window and is not related to the “scrolling” attribute of a window.

Finally, Version 6 story files (but no others) are normally able to display images, or “pictures”. To the Z-machine these are referred to only by number, and a story file does not “know” how pictures are provided to it, or in what format they are stored. For the mechanics of how to attach resources like pictures to a story file, see §43. Fouropcodes are concerned with pictures:

@draw_picture pn y x

Draws picture number pn so that its top left corner appears at coordinates (y, x) in units on the screen. If y or x are omitted, the coordinate of the cursor in the current window is used.

@erase_picture pn y x

Exactly as @draw_picture, but erases the corresponding screen area to the current background colour.

@picture_data pn arr ?Label

Asks for information about picture number pn. If the given picture exists, a branch occurs to the given Label, and the height and width in pixels of the image are written to the given array arr, with arr-->0 being the height and arr-->1 the width. If the given picture doesn't exist, no branch occurs and nothing is written, except that if pn is zero, then arr-->0 is the number of pictures available to the story file and arr-->1 the “release number” of the collection of pictures. (Or of the Blorb file attached to the story file, if that's how pictures have been provided to it.)

@picture_table tarr

Given a table array tarr of picture numbers, this warns the Z-machine that the story file will want to plot these pictures often, soon and in quick succession. Providing such a warning is optional and enables some interpreters to plot more quickly, because they can cache images in memory somewhere.

· · · · ·

Sound effects are available to story files of any Version from 5 upwards. Once again, to the Z-machine these are referred to only by number, and a story file does not “know” how sounds are provided to it, or in what format they are stored. For the mechanics of how to attach resources like sound effects to a story file, see §43. There is only one sound opcode, but it does a good deal. The simplest form is:

@sound_effect number

which emits a high-pitched bleep if number is 1 and a low-pitched bleep if 2. No other values are allowed.

@sound_effect number effect volrep routine

The given effect happens to the given sound number, which must be 3 or higher and correspond to a sound effect provided to the story file by the designer. Volume is measured from 1 (quiet) to 8 (loud), with the special value 255 meaning “loudest possible”, and you can also specify between 0 and 254 repeats, or 255 to mean “repeat forever”. These two parameters are combined in volrep:

volrep = 256*repeats + volume;

The effect can be: 1 (prepare), 2 (start), 3 (stop), 4 (finish with). You may want to “warn” the Z-machine that a sound effect will soon be needed by using the “prepare” effect, but this is optional: similarly you may want to warn it that you've finished with the sound effect for the time being, but this too is optional. “Start” and “stop” are self-explanatory except to say that sound effects can be playing in the background while the player gets on with play: i.e., the Z-machine doesn't simply halt until the sound is complete. The “stop” effect makes the sound cease at once, even if there is more still to be played. Otherwise, unless set to repeat indefinitely, it will end by itself in due course. If a routine has been provided (this operand is optional, and takes effect only on effect 2), this routine will then be called as an interrupt. Such routines generally do something like play the sound again but at a different volume level, giving a fading-away effect.

· · · · ·

In addition to reading entire lines of text from the keyboard, which games normally do once per turn, you can read a single press of a key. Moreover, on most interpreters you can set either kind of keyboard-reading to wait for at most a certain time before giving up.

@aread text parse time function -> result

This opcode reads a line of text from the keyboard, writing it into the text string array and ‘tokenising’ it into a word stream, with details stored in the parse string array (unless this is zero, in which case no tokenisation happens). (See §2.5 for the format of text and parse.) While it is doing this it calls function() every time tenths of a second: the process ends if ever this function returns true. The value written into result is the “terminating character” which finished the input, or else 0 if a time-out ended the input.

@read_char 1 time function -> result

Results in the ZSCII value of a single keypress. Once again, function(time) is called every time tenths of a second and may stop this process early. (The first operand is always 1, meaning “from the keyboard”.)

@tokenise text parse dictionary

This takes the text in the text buffer (in the format produced by @aread) and tokenises it, i.e., breaks it up into words and finds their addresses in the given dictionary. The result is written into the parse buffer in the usual way.

@encode_text zscii-text length from coded-text

Translates a ZSCII word to the internal, Z-encoded, text format suitable for use in a @tokenise dictionary. The text begins at from in the zscii-text and is length characters long, which should contain the right length value (though in fact the interpreter translates the word as far as a zero terminator). The result is 6 bytes long and usually represents between 1 and 9 letters.

It's also possible to specify which ZSCII character codes are “terminating characters”, meaning that they terminate a line of input. Normally, the return key is the only terminating character, but others can be added, and this is how games like ‘Beyond Zork’ make function keys act as shorthand commands. For instance, the following directive makes ZSCII 132 (cursor right) and 136 (function key f4) terminating:

Zcharacter terminating 132 136;

The legal values to include are those for the cursor, function and keypad keys, plus mouse and menu clicks (see Table 2 for values). The special value 255 makes all of these characters terminating. (For other uses of Zcharacter, see §36.)

EXERCISE 129
Write a “press any key to continue” routine.

EXERCISE 130
And another routine which determines if any key is being held down, returning either its ZSCII code or zero to indicate that no key is being held down.

EXERCISE 131
Write a game in which a player taking more than ten seconds to consider a command is hurried along.

EXERCISE 132
And if thirty seconds are taken, make the player's mind up for her.

EXERCISE 133
Design an hourglass fixed to a pivot on one room's wall, which (when turned upright) runs sand through in real time, turning itself over automatically every forty seconds.

· · · · ·

Besides the keyboard, Version 6 normally supports the use of a mouse. In theory this can have any number of buttons, but since some widely-used computers have single-button mice (e.g., the Apple Macintosh) it's safest not to rely on more than one.

The mouse must be attached to one of the eight windows for it to register properly. (Initially, it isn't attached to any window and so is entirely inert.) To attach it to the window numbered wnum, use the opcode:

@mouse_window wnum

Once attached, a click within the window will register as if it were a key-press to @read_char with ZSCII value 254, unless it is a second click in quick succession to a previous click in the same position, in which case it has ZSCII value 253. Thus, a double-clicking registers twice, once as click (254) and then as double-click (253).

At any time, the mouse position, measured in units, and the state of its buttons, i.e., pressed or not pressed, can be read off with the opcode:

@read_mouse mouse_array

places (x, y) coordinates of the click in mouse_array-->0 and mouse_array-->1 and the state of the buttons as a bitmap in mouse_array-->2, with bit 0 representing the rightmost button, bit 1 the next and so on. In practice, it's safest simply to test whether this value is zero (no click) or not (click). The array mouse_array should have room for 4 entries, however: the fourth relates to menus (see below).

EXERCISE 134
Write a test program to wait for mouse clicks and then print out the state of the mouse.

The mouse also allows access to menus, on some interpreters, though in Version 6 only. The model here is of a Mac OS-style desktop, with one or more menus added to the menu bar at the top of the screen: games are free to add or remove menus at any time. They are added with:

@make_menu number mtable ?IfAbleTo;

Such menus are numbered from 3 upwards. mtable is a table array of menu entries, each of which must be itself a string array giving the text of that option. IfAbleTo is a label, to which the interpreter will jump if the menu is successfully created. If mtable is zero, the opcode instead removes an already-existing menu. During play, the selection of a menu item by the player is signalled to the Z-machine as a key-press with ZSCII value 252, and the game receiving this can then look up which item on which menu was selected by looking at entry -->3 in an array given to read_mouse. The value in this entry will be the menu number times 256, plus the item number, where items are numbered from 0. If the game has 252 listed as a “terminating character” (see above), then menu selection can take the place of typing a command.

EXERCISE 135
Provide a game with a menu of common commands like “inventory” and “look” to save on typing.

· · · · ·

The Z-machine can also load and save “auxiliary files” to or from the host machine. These should have names adhering to the “8 + 3” convention, that is, one to eight alphanumeric characters optionally followed by a full stop and one to three further alphanumeric characters. Where no such extension is given, it is assumed to be .AUX. Designers are asked to avoid using the extensions .INF, .H, .SAV or .Z5 or similar, to prevent confusion. Note that auxiliary files from different games may be sharing a common directory on the host machine, so that a filename should be as distinctive as possible. The two opcodes are:

@save buffer length filename -> R

Saves the byte array buffer (of size length) to a file, whose (default) name is given in the filename (a string array). Afterwards, R holds true on success, false on failure.

@restore buffer length filename -> R

Loads in the byte array buffer (of size length) from a file, whose (default) name is given in the filename (a string array). Afterwards, R holds the number of bytes successfully read.

EXERCISE 136
How might this assist a “role-playing game campaign” with several scenarios, each implemented as a separate Inform game but sharing a player-character who takes objects and experience from one scenario to the next?

EXERCISE 137
Design catacombs in which the ghosts of former, dead players from previous games linger.

· · · · ·

Finally, the Z-machine supports a very simple form of exception-handling like that of the C language's long jump feature. This is very occasionally useful to get the program out of large recursive tangles in a hurry.

@catch -> result

The opposite of @throw, @catch preserves the “stack frame” of the current routine in the variable result: roughly speaking, the stack frame is the current position of which routine is being run and which ones have called it so far.

@throw value stack-frame

This causes the program to execute a return with value, but as if it were returning from the routine which was running when the stack-frame was “caught”, that is, set up by a corresponding @catch opcode. Note that you can only @throw back to a routine which is still running, i.e., to which control will eventually return anyway.

▲▲ EXERCISE 138
Use @throw and @catch to make an exception handler for actions, so that any action subroutine getting into recursive trouble can throw an exception and escape.

REFERENCES
The assembly-language connoisseur will appreciate ‘Freefall’ by Andrew Plotkin and ‘Robots’ by Torbjörn Andersson, although the present lack of on-line hints make these difficult games to win.   Gevan Dutton has made an amazing port of the classic character-graphic maze adventure ‘Rogue’ to Inform, taking place entirely in the upper window.   Similarly, ‘Zugzwang’ by Magnus Olsson plots up a chess position.   The function library "text_functions.h", by Patrick Kellum, offers text styling and colouring. These routines are entirely written in assembler. Similar facilities are available from Chris Klimas's "style.h" and L. Ross Raszewski's "utility.h". Jason Penney's "V6Lib.h" is a coherent extension to the Inform library for Version 6 games (only), offering support for multiple text windows, images and sounds by means of class definitions and high-level Inform code.   More modestly, but applicably to Version 5 and 8 games, L. Ross Raszewski's "sound.h" function library handles sound effects.