The Game Author's Guide to Glulx Inform Inform version 6.21(G0.35) http://www.eblong.com/zarf/glulxe/ *** What Is This For? If you are an Inform game author -- reasonably familiar with the Inform language, and compiling Z-code games -- and you want to get into that Glulx thing, then you should read this document first. This probably isn't the *only* document you should read. This doesn't attempt to cover graphics, sound, or any of the other fancy tricks that Glk makes possible. This guide tries to answer the immediate question: "I have an Inform game, and I want to compile it for Glulx instead of for the Z-machine. What do I do?" And, as a bonus, it touches on "What neat tricks are available in Glulx Inform that aren't available on the Z-machine?" Just a touch, mind you. For some authors, this document may be all you need. If your only desire is to write an Inform game which won't fit in the Z-machine, you can read this, compile it for Glulx, and you'll be fine. * What Is This Glulx Thing? The long form, with rationalizations and justifications and circles and lines and arrows, is on the Glulx web page. (See URL above.) The short form is, it's a virtual machine. Just same way the Z-machine is a virtual machine. You can write an Inform game, compile it for the Z-machine, and run it with a Z-machine interpreter; or you can compile it for Glulx, and run it with a Glulx interpreter. The difference is, the Z-machine has many limitations in its design, and the Glulx virtual machine is free of them. Well, most of them. * So It's Just That Simple, Right? The Glulx Inform compiler is designed to compile the same language as standard Inform. If your source code compiles under Inform 6.21, it should compile under Glulx Inform, with almost no changes. The rest of this document is about the difference between "almost no changes" and "no changes at all". *** What Every Game Author Should Know * The Overview To get started writing a Glulx Inform game, you need: The Glulx (or bi-platform) Inform compiler The bi-platform Inform library The Glulxe interpreter for your machine This is, of course, parallel to what you'd need to write a Z-code game -- compiler, library, Z-machine interpreter. The compiler is on the Glulx web page. It works exactly the same as the original Inform compiler (Z-code Inform). The only difference is what kind of file it generates. (At the moment, I have two compilers up there. One generates only Glulx files; the other can generate either Glulx or Z-code, depending on whether you use the -G switch. You might as well use the bi-platform compiler.) The bi-platform library is also on the web page. This is a bi-platform version of Graham's library 6/10 -- that is, you can use it with *either* Inform compiler, compiling for either Glulx or Z-code. Under Z-code, it is exactly as the same as the 6/10 library. Under Glulx, it does the same job, but various pieces are implemented differently, accounting for differences between the two VMs. The interpreter is found guess where. You can get it as source code, or as a Mac, DOS, or Windows executable. (If you get the source code, you'll also need a Glk library to compile it with. If you get an executable, the Glk library is built in.) Writing a game with these tools is nearly the same as writing one with the Z-code Inform tools. However, you may need to pay attention to some details: If you want to be (still) able to compile with Graham's Z-code Inform compiler, you'll need to put a small block of compatibility code at the beginning of your program. If you get the length of a property, with the expression (obj.#prop)/2, you should change it to (obj.#prop)/WORDSIZE. If you write your own DrawStatusLine() procedure, using Z-code assembly, you will have to rewrite it using Glk calls. (See the bi-platform library for sample code.) If you use the Inform "style" statement, you will have to rewrite it using a Glk call. A few new library functions are available, only when compiling for Glulx. The following sections explain these in more detail. Feel free to skip anything you don't need to use. (The Advent.inf source file compiles for Glulx without any changes whatsoever.) * Writing Bi-Platform Code The ideal, of course, is to write Inform source code that will compile for either platform. Not everyone will want to do this -- if your game is too large to fit in the Z-machine, there's no reason to make the source code bi-platform. But in the interests of clarity, this document will assume that you are going for both VMs. The compiler predefines two constants which are useful. When compiling for the Z-machine, it (effectively) defines: Constant TARGET_ZCODE; Constant WORDSIZE 2; When compiling for Glulx, it defines: Constant TARGET_GLULX; Constant WORDSIZE 4; This means you can use #ifdef TARGET_ZCODE or #ifdef TARGET_GLULX to define pieces of code appropriate to a single platform. (The adapted Inform library uses this strategy all over.) (The WORDSIZE constant defines the number of bytes in a word. See the next section.) There is one temporary snag: Graham's released Inform 6.21 compiler (for Z-code) does not actually define these constants yet. Future versions eventually will. In the meantime, to make use of the adapted 6/10 library with Graham's compiler, you must stick the following code at the top of your source code (before any Includes:) ! This is necessary to compile with Graham's current Inform 6.21 compiler. #ifndef WORDSIZE Constant TARGET_ZCODE; Constant WORDSIZE 2; #endif; This code is *not* necessary when compiling a Z-code game with my bi-platform compiler. The bi-platform compiler defines WORDSIZE and the appropriate TARGET_ constant, in both modes. * Word Size The most basic difference between Glulx and the Z-machine is that words are four bytes long instead of two. All variables are 32-bit values, the stack contains 32-bit values, property entries are 32-bit values. And so on. In most Inform programming, you don't need to worry about this change at all. For example, if you have an array Array list --> 10; ...then Z-code Inform will allocate ten words -- that is, twenty bytes -- and you can access these values as list-->0 through list-->9. If you compile the same code under Glulx Inform, the compiler will again allocate ten words -- forty bytes -- and you can still access them as list-->0 through list-->9. Everything will work the same, except that the values can contain values greater than 65535. Table arrays also refer to four-byte values, and the first four-byte word is the length of the array. String and -> arrays, and the -> notation, still refer to single bytes. You should not have to modify your code here, either. There is one important case where you *will* have to modify your code: the .# operator. The expression obj.#prop returns the length of the property in bytes. Since properties almost always contain words, rather than bytes, it is very common to have code like: len = (obj.#prop) / 2; for (i=0 : ii; In Glulx Inform programs, it is necessary to divide by 4 instead of by 2. To make this more readable, you can use the constant WORDSIZE. So you should replace the above code with: len = (obj.#prop) / WORDSIZE; for (i=0 : ii; This will compile and run correctly in both VMs. In general, you should search through your code for the .# operator; for each occurence, find the relevant "2" and change it to "WORDSIZE". * Missing Statements A few Inform statements are specific to the Z-machine. In Glulx code, they should be replaced with Glulx assembly or Glk calls. These statements are almost never used by game code; they are primarily used in the library, and the adapted 6/10 library correctly modifies them. If you try to use any of these statements in Glulx, you will get a compilation error. style: This statement should be replaced with Glk calls to change the output style. See the section "Text Styles". It is possible that future versions of the Glulx compiler will implement these statements with "best-guess" Glk style calls. However, Glk is capable of much subtler distinctions than the simple bold/italic/fixed-width set of the Z-machine. It is worth putting in explicit Glk calls. save, restore: These are more complicated procedures in Glulx than in Z-code. They cannot be implemented without involving library variables and functions. If you want to do this sort of thing, modify or copy the library SaveSub and RestoreSub routines. read: Similarly, reading a line of text in Glulx involves the library; the compiler cannot generate stand-alone code to do it. See the library KeyboardPrimitive routine. * Thingie References In the Z-machine, strings and functions are "packed" addresses; dictionary words are normal addresses; and game objects are represented as sequential numbers from 1 to #top_object. These ranges overlap. So a string, a dict word, and an object could conceivably all be represented by the same value. In Glulx, all those things are represented by normal addresses. So two different things will always have different values. Furthermore, the first byte found at the address is an identifier value, which tells you what kind of thing the address contains: E0...FF: Strings. E0: Unencoded, null-terminated strings. E1: Compressed strings. E2...FF: Reserved for future expansion of string types. C0...DF: Functions. C0: Stack-argument functions. C1: Local-argument functions. C2...DF: Reserved for future expansion of function types. 80...BF: Reserved for future use by the VM. 40...7F: Reserved for use by the Inform library. 70: Objects (and classes). 60: Dictionary words. 01...3F: Available for use by game code. 00: Reserved to mean "no object". Now that you have seen this system, you can forget about it. The metaclass and ZRegion functions figure out object types, and return the same values they do under Z-code Inform. One small detail: in Z-code Inform, you can print a compressed string from anywhere in RAM with the line: print (address) word; Nineteen times out of twenty, this is used to print dictionary words. (A dict word value is a RAM address, and a Z-code dict word begins with a compressed string.) This is still legal in Glulx Inform, but *only* to print dictionary words. (Glulx dict words are not compressed, and they begin with the byte 60 instead of E0 or E1.) If you want to print a generic string, use the usual print (string) method. Another detail: since objects are no longer sequential integers, you cannot write a loop like for (obj=rock : obj0) { 5: ! evtype_Arrange DrawStatusLine(); 2: ! evtype_CharInput if (gg_event-->1 == gg_mainwin) done = true; } } #endif; ! TARGET_ Note that gg_mainwin is the library variable which contains the story window ID, and gg_event is a four-word array which the library uses to catch Glk events. It is used in KeyboardPrimitive, but you can use it too. In fact, a fancier version of this code is in the library, as KeyCharPrimitive(). See below. As you've probably noticed, this is not extremely easy to read. If you plan to make a lot of Glk calls, you might want Katre's "infglk.h" library header. That defines wrapper functions and constants. So, for example, you could call glk_set_style(style_Preformatted); and get the same effect as glk($0086, 2); ! set_style The library does not use this trick, for the sake of simplicity, but you're free to. * DrawStatusLine() Explained The biggest hurdle for Inform authors switching to Glulx is writing a DrawStatusLine() procedure. In Z-code Inform, this is a big hunk of assembly, and everybody just rearranges it until it's right -- perhaps with some peering at the Z-machine specification. Under Glulx, it's a big hunk of Glk calls, but you can rearrange it the same way. Here's the complete DrawStatusLine() procedure from the bi-platform library. (Really, of course, the library contains two versions -- Glulx and Z-code -- with #ifdefs to compile the right one.) [ DrawStatusLine width height posa posb; ! If we have no status window, we must not try to redraw it. if (gg_statuswin == 0) return; ! If there is no player location, we shouldn't try either. if (location == nothing || parent(player) == nothing) return; glk($002F, gg_statuswin); ! set_window StatusLineHeight(gg_statuswin_size); glk($0025, gg_statuswin, gg_arguments, gg_arguments+4); ! window_get_size width = gg_arguments-->0; height = gg_arguments-->1; posa = width-26; posb = width-13; glk($002A, gg_statuswin); ! window_clear glk($002B, gg_statuswin, 1, 0); ! window_move_cursor if (location == thedark) { print (name) location; } else { FindVisibilityLevels(); if (visibility_ceiling == location) print (name) location; else print (The) visibility_ceiling; } if (width > 66) { glk($002B, gg_statuswin, posa-1, 0); ! window_move_cursor print (string) SCORE__TX, sline1; glk($002B, gg_statuswin, posb-1, 0); ! window_move_cursor print (string) MOVES__TX, sline2; } if (width > 53 && width <= 66) { glk($002B, gg_statuswin, posb-1, 0); ! window_move_cursor print sline1, "/", sline2; } glk($002F, gg_mainwin); ! set_window ]; Let's go through it one line at a time. [ DrawStatusLine width height posa posb; A normal Inform function, with four local variables. ! If we have no status window, we must not try to redraw it. if (gg_statuswin == 0) return; gg_statuswin is the library global variable which stores the status window reference. Some libraries don't support a status window; if there isn't one, gg_statuswin contains zero. If so, there's nothing to be done here, so return immediately. ! If there is no player location, we shouldn't try either. if (location == nothing || parent(player) == nothing) return; This case won't occur in most games, but it's safest to check anyhow. (One way it might occur is if you put a YesOrNo() question in your Initialise() procedure.) glk($002F, gg_statuswin); ! set_window Set the current output window. All subsequent printing will go to the status window. Again, note the general form of Glk calls. The first argument of glk() says which Glk call in being invoked; $002F is glk_set_window(). The rest of the arguments follow. StatusLineHeight(gg_statuswin_size); StatusLineHeight() is a library function which simply sets the status line height to the given value. It's smart enough to remember the current size, and leave the window alone if no change is needed. (The gg_statuswin_size global variable is 1 by default, and the library never changes it.) glk($0025, gg_statuswin, gg_arguments, gg_arguments+4); ! window_get_size width = gg_arguments-->0; height = gg_arguments-->1; This calls glk_window_get_size(), to measure the size of the status window. The last two arguments should be memory addresses; the width and height are written there. gg_arguments is a library array which is available for just this purpose. It is eight words long, but in this case we only need the first two positions. (The second position is given as gg_arguments+4, because a word is four bytes long in Glulx Inform.) Once the call is made, we pull the two values out into local variables. posa = width-26; posb = width-13; The horizontal positions of the score and turn indicators. This line appears in both the Z-code and Glulx versions of DrawStatusLine(). glk($002A, gg_statuswin); ! window_clear Call glk_window_clear() to clear the current window. glk($002B, gg_statuswin, 1, 0); ! window_move_cursor Move the cursor to position (1, 0) -- that is, the second character of the first line. The top left position in Glk windows is numbered (0, 0). This is different from Z-code windows, where the top left position is (1, 1). The other difference, just as important, is that glk_window_move_cursor() takes coordinates in the order (X, Y). The Z-code @set_cursor opcode uses (Y, X). if (location == thedark) { print (name) location; } else { FindVisibilityLevels(); if (visibility_ceiling == location) print (name) location; else print (The) visibility_ceiling; } This is identical to the code in the Z-code version. if (width > 66) { glk($002B, gg_statuswin, posa-1, 0); ! window_move_cursor print (string) SCORE__TX, sline1; glk($002B, gg_statuswin, posb-1, 0); ! window_move_cursor print (string) MOVES__TX, sline2; } if (width > 53 && width <= 66) { glk($002B, gg_statuswin, posb-1, 0); ! window_move_cursor print sline1, "/", sline2; } Print the score and moves. This code works the same as in the Z-code version. glk($002F, gg_mainwin); ! set_window ]; Set the current window back to the story window, and return. The only different you might note is that the Z-code version tests ((0->1)&2 == 0). This is a Z-machine header bit, which in V3 games is used to tell the interpreter to draw a status line with "Time" instead of "Score / Moves". The Z-code DrawStatusLine() routine emulates this behavior. However, V3 games are rare, and Glulx has no header bits in any case. So the Glulx version of the routine drops the whole idea. If you want to show the time, Replace DrawStatusLine and write your own code. The explanation here should be enough to get you started. Again, if you don't want to spend your life looking up constants in tables, you can use the "infglk.h" library header. * If You Write an Inform Library Extension Not all game authors will want to write bi-platform Inform source. Many will know they want to create a Glulx game, and will code only for at. However, a library extension *should* be bi-platform, because it may be used in both Z-code and Glulx games. It may also be used both with Graham's (Z-code-only) compiler, and with my compilers. Therefore, you should probably include the portability hack at the beginning of the library's source: ! This is necessary to compile with Graham's current Inform 6.21 compiler. #ifndef WORDSIZE Constant TARGET_ZCODE; Constant WORDSIZE 2; #endif; You can then use WORDSIZE in your code, and put in #ifdef TARGET_... conditionals, if necessary. Note: use this chunk of code exactly as given. Don't leave out the TARGET_ZCODE line, even if your library extension has no use for it. The reason for this is left as an exercise. *** Things Available Only in Glulx * New Library Functions KeyCharPrimitive(); This waits for a single key to be struck, and returns the character (0 to 255, or one of the Glk special key codes.) You can also call this with one or two (optional) arguments, as follows: KeyCharPrimitive(win, nostat); If win is nonzero, the character input request goes to that Glk window (instead of gg_mainwin, the default.) If nostat is nonzero, a window rearrangement event is returned immediately as value 80000000 (instead of the default behavior, which is to call DrawStatusLine() and keep waiting.) StatusLineHeight(); This changes the height of the status line, as you might expect. The standard DrawStatusLine() calls this every turn, which isn't a bad thing, since StatusLineHeight() is smart. If you replace DrawStatusLine(), maintain this convention. (The library menu routines fiddle with the status line, and it's up to DrawStatusLine() to reset it after the menus are over.) DecimalNumber(num); This prints num as a decimal number. It is in fact identical to print num; ...and that's how it's implemented. But it may be useful in conjunction with the following function: PrintAnything(obj, ...); This will print any thingie known to the library. It handles strings, functions (with optional arguments), objects, object properties (with optional arguments), and dictionary words. Calling: Is equivalent to: ------- ---------------- PrintAnything() PrintAnything(0) PrintAnything("string"); print (string) "string"; PrintAnything('word') print (address) 'word'; PrintAnything(obj) print (name) obj; PrintAnything(obj, prop) obj.prop(); PrintAnything(obj, prop, args...) obj.prop(args...); PrintAnything(func) func(); PrintAnything(func, args...) func(args...); Extra arguments after a string or dict word are safely ignored. The (first) argument you pass in is always interpreted as a thingie reference, not as an integer. This is why none of the forms shown above print out an integer. However, you can get the same effect by calling PrintAnything(DecimalNumber, num); ...which is where the DecimalNumber() function comes in handy. You can also, of course, use other library functions, and do tricks like PrintAnything(EnglishNumber, num); PrintAnything(DefArt, obj); None of this may seem very useful. After all, there are already ways to print all those things. But PrintAnything() is vital in implementing the following function: PrintAnyToArray(array, arraylen, obj, ...); This works the same way, except that instead of printing to the screen, the output is diverted to the given array. The first two arguments must be the array address and its maximum length. Up to that many characters will be written into the array; any extras will be silently discarded. This means that you do not have to worry about array overruns. The PrintAnyToArray() function returns the number of characters generated. (This may be greater than the length of the array. It represents the entire text that was output, not the limited number written into the array.) It is safe to nest PrintAnyToArray() calls. That is, you can call PrintAnyToArray(func), where func() is a function which itself calls PrintAnyToArray(). (However, if they try to write to the *same array*, chaos will ensue.) It is legal for arraylen to be zero (in which case array is ignored, and may be zero as well.) This discards *all* of the output, and simply returns the number of characters generated. You can use this to find the length of anything -- even a function call. * New Entry Points An entry point is a function which you can provide in your code, or leave out; the library will call it if it's present, ignore it if not. (See the Inform DM.) The bi-platform library has some entry points which aid in writing more complicated interfaces -- games with sound, graphics, extra windows, and other fancy Glk tricks. If you're just writing a standard Infocom-style game, you can ignore this section. HandleGlkEvent(ev, context, abortres) This entry point is called every time a Glk event occurs. The event could indicate nearly anything: a line of input from the player, a window resize or redraw event, a clock tick, a mouse click, or so on. The library handles all the events necessary for a normal Infocom-style game. You only need to supply a HandleGlkEvent() function if you want to add extra functionality. The ev argument is a four-word array which describes the event. ev-->0 is the type of the event; ev-->1 is the window involved (if relevant); and ev-->2 and ev-->3 are extra information. The context argument is 0 if the event occurred during line input (normal commands, YesOrNo(), or some other use of the KeyboardPrimitive() library function); 1 indicates that the event occurred during character input (any use of the KeyCharPrimitive() library function). The abortres argument is only used if you want to cancel player input and force a particular result; see below. If you return 2 from HandleGlkEvent(), player input will immediately be aborted. Some additional code is also required: * If this was character input (context == 1), you must call the Glk cancel_char_event function, and then set abortres-->0 to the character you want returned. Then return 2; KeyCharPrimitive() will end and return the character, as if the player had hit it. * If this was line input (context == 0), you must call the Glk cancel_line_event function. (You can pass an array argument to see what the player had typed so far.) Then, fill in the length of the input to be returned in abortres-->0. If this is nonzero, write the input characters sequentially into the array starting at abortres->WORDSIZE, up to (but not including) abortres->(WORDSIZE+len). Do not exceed 256 characters. Then return 2; KeyboardPrimitive() will end and return the line. If you return -1 from HandleGlkEvent(), player input will continue even after a keystroke (for character input) or after the enter key (for line input). (I don't know why this is useful, but it might be.) You must re-request input by calling request_char_input or request_line_input. Any other return value from HandleGlkEvent() (a normal return, rfalse, or rtrue) will not affect the course of player input. InitGlkWindow(winrock) This entry point is called by the library when it sets up the standard windows: the story window, the status window, and (if you use quote boxes) the quote box window. The story and status windows are created when the game starts (before Initialise()). The quote window is created and destroyed as necessary. This is called in five phases: * The library calls InitGlkWindow(0). This occurs at the very beginning of execution, even before Initialise(). You can set up any situation you want. (However, remember that the story and status windows might already exist -- for example, if the player has just typed "restart".) This is a good time to set gg_statuswin_size to a value other than 1. Return 0 to proceed with the standard library window setup, or 1 if you've created all the windows yourself. * The library calls InitGlkWindow(GG_MAINWIN_ROCK), before creating the story window. This is a good time to set up style hints for the story window. Return 0 to let the library create the window; return 1 if you have yourself created a window and stored it in gg_mainwin. * The library calls InitGlkWindow(GG_STATUSWIN_ROCK), before creating the status window. Again, return 0 to let the library do it; return 1 if you have created a window and stored it in gg_statuswin. * The library calls InitGlkWindow(1). This is the end of window setup; you can take this opportunity to open other windows. (Or you can do that in your Initialise() routine. It doesn't matter much.) * The library calls InitGlkWindow(GG_QUOTEWIN_ROCK), before creating the quote box window. This does not occur during game initialization; the quote box window is created during the game, whenever you print a quote, and destroyed one turn later. As usual, return 1 to indicate that you've created a window in gg_quotewin. (The desired number of lines for the window can be found in gg_arguments-->0.) However you handle window initialization, remember that the library requires a gg_mainwin. If you don't create one, and don't allow the library to do so, the game will shut down. Contrariwise, the status window and quote windows are optional; the library can get along without them. IdentifyGlkObject(phase, type, ref, rock) This entry point is called by the library to let you know what Glk objects exist. You must supply this function if you create any windows, filerefs, file streams, or sound channels beyond the standard library ones. (This is necessary because after a restore, restart, or undo command, your global variables containing Glk objects will be wrong.) This is called in three phases: * The library calls IdentifyGlkObject() with phase==0. You should set all your Glk object references to zero. * The library calls IdentifyGlkObject() with phase==1. This occurs once for each window, stream, and fileref that the library doesn't recognize. (The library handles the two standard windows, and the files and streams that have to do with saving, transcripts, and command records. You only have to deal with objects that you create.) You should set whatever reference is appropriate to the object. For each object: type will be 0, 1, 2 for windows, streams, filerefs respectively; ref will be the object reference; and rock will be the object's rock, by which you can recognize it. * The library calls IdentifyGlkObject() with phase==2. This occurs once, after all the other calls, and gives you a chance to recognize objects that aren't windows, streams, or filerefs. If you don't create any such objects, you can ignore that bit. But you should also take the opportunity to update all your Glk objects to the game state that was just started or restored. (For example, redraw graphics, or set the right background sounds playing.) * Stack-Argument Functions By default, function arguments work the same in Glulx as they do in Z-code. When you call a function, the arguments that you pass are written into the function's local variables, in order. If you pass too many arguments, the extras are discarded; too few, and the unused local variables are filled with zeroes. However, the Glulx VM supports a second style of function. You can define a function of this type by naming the first function argument "_vararg_count". For example: [ StackFunc _vararg_count ix pos len; ! ...code... ]; If you do this, the function arguments are *not* written into the local variables. They are pushed onto the stack; you must use Glulx assembly to pull them off. All the local variables are initialized to zero, except for _vararg_count, which (as you might expect) contains the number of arguments that were passed in. Note that _vararg_count is a normal local variable, aside from its useful initial value. You can assign to it, increment or decrement it, use it in expressions, and so on. Stack-argument functions are most useful if you want a function with variable arguments, or if you want to write a function wrapper that passes its arguments on to another function. (Alert readers may notice that this isn't exactly the structure that the Glulx VM spec describes. A C0-type function is started with an extra value on the stack, which specifies the number of arguments below it on the stack. However, this is hidden from you by the compiler. When you write a _vararg_count function, the compiler automatically generates code to pop this value from the stack and store it in the _vararg_count local variable.) * Limited Length print_to_array Inform's String metaclass has a built-in print_to_array method. str = "This is a string."; len = str.print_to_array(arr); ...will write the contents of the string into the array, starting at byte 2. The first word (arr-->0) will contain the number of characters written, as will len. This works identically in Glulx Inform, except of course that the string starts at byte 4, so that there's room for the four-byte arr-->0. However, there is also an extended form in Glulx. len = str.print_to_array(arr, 80); ...will write no more than 76 characters into the array. If arr is an 80-byte array, you can be sure it will not be overrun. (Do not try this with the second argument less than 4.) The value written into arr-->0, and the value returned, are *not* limited to the number of characters written. They represent the number of characters in the complete string. This means that len = str.print_to_array(arr, 4); ...is an ugly but perfectly legal way to find the length of a string. (And in this case, arr need only be four bytes long.) * Better Printing Variables Z-code Inform supports 32 printing variables, @00 to @31, which you can include in strings and then set with the statement. string num "value"; In Glulx, this limit is raised to 64. (Eventually, this will be a Inform command-line option. It could easily be raised to 100, or even higher if Inform's string syntax was changed to allow three digits after the @ sign.) Furthermore, in Glulx you can set these variables to any string or function value. If you use a function, the function will be called with no arguments and the result discarded; you should print your desired output inside the function. A string value will simply be printed. In Glulx, unlike Z-code, a printing variable string can itself contain @.. codes, allowing recursion. You can nest this as deeply as you want. However, it is obviously a bad idea to cause an infinite recursion. For example, string 3 "This is a @03!"; print "What is @03?"; ...will certainly crash the interpreter. *** Tricks You Probably Don't Need If you want to define your own thingie classes, you can. (This is usually not necessary -- it's easier to represent your thingies as Inform game objects -- but it's possible.) Define any block of memory whose first byte is in the range 01 to 3F. You can then pass its address around like any other reference. Library functions such as metaclass and PrintAnything will not understand it, but if you get a reference, you can reliably tell the difference between your thingies and library thingies by examining the first byte.