Section 33: Descending into assembly language

Contents Back Forward | ### 33. Descending into assembly language
| Some dirty tricks require bypassing all of Inform's higher levels to program the Z-machine directly with assembly language. There is an element of danger in this, in that some combinations of unusual opcodes might look ugly on some incomplete or wrongly-written interpreters: so if you're doing anything complicated, test it as widely as possible.
| The best-researched and most reliable interpreters available by far are Mark Howell's Zip and Stefan Jokisch's Frotz: they are also faster than their only serious rival, the InfoTaskForce, a historically important work which is fairly thorough (and should give little trouble in practice) but which was written when the format was a little less well understood. In some ports, ITF gets rarer screen effects wrong, and it lacks an "undo'' feature, so the Inform "undo'' verb won't work under ITF. (The other two publically-available interpreters are pinfocom and zterp, but these are unable to run Advanced games. In the last resort, sometimes it's possible to use one of Infocom's own supplied interpreters with a different game from that it came with; but only sometimes, as they may have inconvenient filenames 'wired into them'.) Interpreters conforming to the Z-Machine Standard, usually but not always derived from Frotz or Zip, are reliable and widely available. But remember that one source of unportability is inevitable. Your game may be running on a screen which is anything from a 64 characters by 9 pocket organiser LCD display, up to a 132 by 48 window on a 21-inch monitor. Anyone wanting to really push the outer limits (say, by implementing Space Invaders or NetHack) will need to refer to The Z-Machine Standards Document. This is much more detailed (the definition of `aread` alone runs for two pages) and covers the whole range of assembly language. However, this section does document all those features which can't be better obtained with higher-level code. Lines of assembly language must begin with an `@` character and then the name of the "opcode'' (i.e., assembly language statement). A number of arguments, or "operands'' follow (how many depends on the opcode): these may be any Inform constants, local or global variables or the stack pointer `sp`, but may not be compound expressions. `sp` does not behave like a variable: writing a value to it pushes that value onto the stack, whereas reading the value of it (for instance, by giving it as an operand) pulls the top value off the stack. Don't use `sp` unless you have to. After the operands, some opcodes require a variable (or `sp`) to write a result into. The opcodes documented in this section are as follows:
    @split_window    lines
    @set_window      window
    @set_cursor      line column
    @buffer_mode     flag
    @erase_window    window
    @set_colour      foreground background
    @aread           text parse time function <result>
    @read_char       1 time function <result>
    @tokenise        text parse dictionary
    @encode_text     ascii-text length from coded-text
    @output_stream   number table
    @input_stream    number
    @catch           <result>
    @throw           value stack-frame
    @save            buffer length filename <result>
    @restore         buffer length filename <result>
    @split_window    lines
Splits off an upper-level window of the given number of lines in height from the main screen. This upper window usually holds the status line and can be resized at any time: nothing visible happens until the window is printed to. Warning: make the upper window tall enough to include all the lines you want to write to it, as it should not be allowed to scroll.
    @set_window      window
The text part of the screen (the lower window) is "window 0'', the status line (the upper one) is window 1; this opcode selects which one text is to be printed into. Each window has a "cursor position'' at which text is being printed, though it can only be set for the upper window. 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. Note that before printing to the upper window, it is wise to use `@buffer_mode` to turn off word-breaking.
    @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=1`) or off (`flag=0`) 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). It is wise to turn off word-breaking while printing to the upper window.
    @erase_window    window
This opcode is unfortunately incorrectly implemented on some interpreters and so it can't safely be used to erase individual windows. However, it can be used with `window=-1`, and then clears the entire screen. Don't do this in reverse video mode, as a bad interpreter may (incorrectly) wipe the entire screen in reversed colours.
    @set_colour      foreground background
If coloured text is available, set text to be foreground-against-background. The colour numbers are borrowed from the IBM PC:
    2 = black,  3 = red,      4 = green,  5 = yellow,
    6 = blue,   7 = magenta,  8 = cyan,   9 = white
    0 = the current setting,  1 = the default.
On many machines coloured text is not available: the opcode will then do nothing.
    @aread           text parse time function <result>
The keyboard can be read in remarkably flexible ways. 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 the end of Section 27 for the format of `text` and `parse`.) While it is doing this, it calls `function(time)` every `time` tenths of a second while the user is thinking: the process ends if ever this function returns true. `<result>` is to be a variable, but the value written in it is only meaningful if you're using a "terminating characters table''. Thus (by `Replace`ing the `Keyboard` routine in the library files) you could, say, move around all the characters every ten seconds of real time. Warning: not every interpreter supports this real-time feature, and most of those that do count in seconds instead of tenths of seconds.
    @read_char       1 time function <result>
results in the ASCII value of a single keypress. Once again, the `function` is called every `time` tenths of a second and may stop this process early. Function keys return special values from 129 onwards, in the order: cursor up, down, left, right, function key f1, ..., f12, keypad digit 0, ..., 9. The first operand must be 1 (used by Infocom as a device number to identify 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, finds their addresses in the dictionary) into the `parse` buffer in the usual way but using the given `dictionary` instead of the game's usual one. (See the Z-Machine Standards Document for the dictionary format.)
    @encode_text     ascii-text length from coded-text
Translates an ASCII word to the internal (Z-encoded) text format suitable for use in a `@tokenise` dictionary. The text begins at `from` in the `ascii-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 0 terminator). The result is 6 bytes long and usually represents between 1 and 9 letters.
    @output_stream   number table
Text can be output to a variety of different 'streams', possibly simultaneously. If `number` is 0 this does nothing. $+n$ switches stream n on, $-n$ switches it off. The output streams are: 1 (the screen), 2 (the game transcript), 3 (memory) and 4 (script of player's commands). The `table` can be omitted except for stream 3, when it's a `table` array holding the text printed; printing to this stream is never word-broken, whatever the state of `@buffer_mode`.
    @input_stream    number
Switches the 'input stream' (the source of the player's commands). 0 is the keyboard, and 1 a command file (the idea is that a list of commands produced by `output_stream 4` can be fed back in again).
    @catch           <result>
The opposite of `throw`, `catch` preserves the "stack frame'' of the current routine: meaning, roughly, 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 (see `catch`). Any routines which were called in the mean time and haven't returned yet (because each one called the next) are forgotten about. This is useful to get the program out of large recursive tangles in a hurry.
    @save            buffer length filename <result>
Saves the byte array `buffer` (of size `length`) to a file, whose (default) name is given in the `filename` (a `string` array). Afterwards, `result` holds 1 on success, 0 on failure.
    @restore          buffer length filename <result>
Loads in the byte array `buffer` (of size `length`) from a file, whose (default) name is given in the `filename` (a `string` array). Afterwards, `result` holds the number of bytes successfully read.
| WARNING: Some of these features may not work well on obsolete interpreters which do not adhere to the Z-Machine Standard. Standard interpreters are widely available, but if seriously worried you can test whether your game is running on a good interpreter:
        if (standard_interpreter == 0)
        {   print "This game must be played on an interpreter obeying the
                   Z-Machine Standard.^";
            @quit;
        }
| EXERCISE 90: (link to the answer)
| In a role-playing game campaign, you might want several scenarios, each implemented as a separate Inform game. How could the character from one be saved and loaded into another?
| EXERCISE 91: (link to the answer)
| Design a title page for 'Ruins', displaying a more or less apposite quotation and waiting for a key to be pressed.
| EXERCISE 92: (link to the answer)
| Change the status line so that it has the usual score/moves appearance except when a variable `invisible_status` is set, when it's invisible.
| EXERCISE 93: (link to the answer)
| Alter the 'Advent' example game to display the number of treasures found instead of the score and turns on the status line.
| EXERCISE 94: (link to the answer)
| (From code by Joachim Baumann.) Put a compass rose on the status line, displaying the directions in which the room can be left.
| EXERCISE 95: (link to the answer)
| (Cf. 'Trinity'.) Make the status line consist only of the name of the current location, centred in the top line of the screen.
| EXERCISE 96: (link to the answer)
| 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.''
| REFERENCES: The assembly-language connoisseur will appreciate 'Freefall' by Andrew Plotkin and 'Robots' by Torbjorn Andersson although the present lack of on-line hints make these difficult games to win.

Contents / Back / Forward
Chapter I / Chapter II / Chapter III / Chapter IV / Chapter V / Chapter VI / Appendix

Mechanically translated to HTML from third edition as revised 16 May 1997. Copyright © Graham Nelson 1993, 1994, 1995, 1996, 1997: all rights reserved.