Skip to content

Chapter 19

The front end and the keys

Before and between missions the game runs a few screens: a scrolling story, three pictures, a menu of ranks, a briefing, a dialog for saved games, the high scores. By the end of this chapter you will know what each draws, waits for and reads, VBlank by VBlank; how one loop joins them; why the dialogs' font comes from the ROM; how a key becomes a code that the ROM's own routine turns into a character; and what the port made of it, its own keys and the keyboard assist among it.

A timetable of waits

The front end is the game's screens before and between missions, and the loop that joins them. Each screen waits by calling graphics.library's WaitTOF, which returns at the next VBlank, so many times or until the button goes down; one call is a round, one VBlank. So, left alone, the program keeps one timetable, and the port is held to it. A run of the headless original with no input, on PAL:

VBlank What happens
1 song 2; the story scroller
3 to 2,915 53 lines of the story, one every 56 VBlanks
3,791 the scroller ends; song 2 fades
3,895 song 1; the first picture, 60 rounds in a palette of its own, 120 in its colours
4,078 the title, 300 rounds
4,379 the credits, 600 rounds
4,980 the rank selection; song 1 fades
5,084 song 4; from 5,087 the menu, which gives up when its count passes 1,800
6,888 the menu gives up and asks for the demo; the music stops
6,992 the briefing, 240 rounds
after 7,237 step S, the inner loop; from 7,235 the briefing's fade, the setup, main's first tick

A change of song waits for the song's fade, 104 VBlanks in which the screen stands still (chapter 18). The display's fades hold no wait, so the headless original runs them in no time; the port gives a step two VBlanks, the comparisons none (chapter 7). With the taps of fire the tests use, step S comes after 444 VBlanks, 312 of them the three songs' fades.

The figures

The timetable in seconds, derived
A line of the story, 56 VBlanks; the pictures' waits, 60 to 600 rounds 1.1 s; 1.2 to 12 s
The rank selection, 1,800 rounds; the briefing, 240 36 s; 4.8 s
A change of song, 104 VBlanks; a fade in the port, 32 2.1 s; 0.64 s
The front end left alone, fades at no time, 7,237 VBlanks; with fire, 444 145 s; 8.9 s

The story scroller

The story scroller comes first, behind song 2; the crack's text screen before it is left out (chapter 1). Its screen is two views of 640 by 200, one shown, each a single viewport of one bitplane. Its window, the rows the screen shows, is 230 rows from wherever the plane's start stands, text visible down to row 196. Every step, four VBlanks, the start moves a row down, so the text rises with nothing copied; a line is 14 rows, so one comes every 14 steps. After 210 steps the start returns to the top, and the copper list reloads the plane's address at the ring's row 210, so the window's lower rows show the top again: a ring of 210 rows, which is how a window of 230 rows shows a bitmap of 200.

Each new line is drawn where the window's row 196 will show it: after a wrap at the ring's row 196, otherwise 14 rows above the plane's start, before the drawing's first row, which graphics.library does not clip here. So drawing and display reach past the bitmap, the display to row 258: the port's graphics layer must not clip either, and its view block is 640 by 260 bytes (chapter 11).

The processor draws each line in the game's font into a one-bit stencil of where the ink goes, and graphics.library's BltTemplate stamps it in the current colour, justified to 615 pixels unless it ends a paragraph. A grey ramp of colour 1, 0x111 a row, fades the text in over the visible text's bottom 16 rows and out over its top 16; the port gives each grey a palette. Fire ends the scroller, or some 210 steps after its last line and a wind-down of 16 rounds of two VBlanks; it reads no key.

A line dim at the top, a white justified paragraph, its short last line dim at the bottom.

The story scroller at VBlank 780 left alone: the ramps fade the text out at the top and in at the bottom.

The title sequence

The title sequence runs the scroller and then three pictures behind song 1: chapter 1's picture of the crack, the title and the credits, on two views of 320 by 200 with five planes. Each picture is decoded into the view not on show, its colours set aside and the view's own black, while the previous one is still up; then the views swap and it fades in, so no picture is seen being decoded. The first fades in to a fixed palette of the executable, waits 60 rounds, then fades to its own colours and waits 120. Fire ends a wait and skips the rest of the sequence at once.

The rank selection

The rank selection is the menu of chapter 17's seven ranks and the load item below them, behind song 4. Its picture is the third the crack touched, three lines of the crack group above the heading (chapter 1). Its highlight, one of eight records of a shape container, is drawn by chapter 12's shape_draw_xor, which inverts the screen's bits where the shape has them, so drawing it again takes it away. A move draws the old one away and the new one in, then copies the shown view into the other, so that the next move finds the same pixels to erase in either.

A metal frame: three lines at the top, a heading, eight items, the second under a blue bar.

The rank selection after one pull of the stick: the blue bar is the highlight, drawn in exclusive-or on the second item; the three lines at the top are the crack group's.

The menu's reader, menu_input, takes the cursor keys and Return from the key buffer of waiting keys, told below, then polls the stick and the button. Look at its count of rounds against 0x708, 1,800, beyond which it returns 1000, the demo's request; in the port CO_WAIT stands for WaitTOF:

; re/Wings.lst 0x01819C-0x018222: menu_input [C], a part of 0x018194-0x018227
loc_01819c:
01819c  4eac82fc             jsr        -$7d04(a4)                             ; -> key_available
0181a0  4a40                 tst.w      d0
0181a2  6736                 beq.b      $181da
0181a4  4eac8302             jsr        -$7cfe(a4)                             ; -> key_get
0181a8  3b40fffc             move.w     d0, -$4(a5)
0181ac  0c6d004cfffc         cmpi.w     #$4c, -$4(a5)
0181b2  6606                 bne.b      $181ba
0181b4  70ff                 moveq      #$ff, d0
loc_0181b6:
0181b6  4e5d                 unlk       a5
0181b8  4e75                 rts
loc_0181ba:
0181ba  0c6d004dfffc         cmpi.w     #$4d, -$4(a5)
0181c0  6604                 bne.b      $181c6
0181c2  7001                 moveq      #$1, d0
0181c4  60f0                 bra.b      $181b6
loc_0181c6:
0181c6  0c6d0044fffc         cmpi.w     #$44, -$4(a5)
0181cc  6708                 beq.b      $181d6
0181ce  0c6d0043fffc         cmpi.w     #$43, -$4(a5)
0181d4  6604                 bne.b      $181da
loc_0181d6:
0181d6  7000                 moveq      #$0, d0
0181d8  60dc                 bra.b      $181b6
loc_0181da:
0181da  4eac82c6             jsr        -$7d3a(a4)                             ; -> poll_joy_dir8
0181de  3b40fffe             move.w     d0, -$2(a5)
0181e2  0c6d0001fffe         cmpi.w     #$1, -$2(a5)
0181e8  6604                 bne.b      $181ee
0181ea  70ff                 moveq      #$ff, d0
0181ec  60c8                 bra.b      $181b6
loc_0181ee:
0181ee  0c6d0005fffe         cmpi.w     #$5, -$2(a5)
0181f4  6604                 bne.b      $181fa
0181f6  7001                 moveq      #$1, d0
0181f8  60bc                 bra.b      $181b6
loc_0181fa:
0181fa  4eac82c0             jsr        -$7d40(a4)                             ; -> poll_fire
0181fe  4a40                 tst.w      d0
018200  6704                 beq.b      $18206
018202  7000                 moveq      #$0, d0
018204  60b0                 bra.b      $181b6
loc_018206:
018206  4eac8452             jsr        -$7bae(a4)                             ; -> gfx_WaitTOF
01820a  526dfffa             addq.w     #$1, -$6(a5)
01820e  4a6d0008             tst.w      $8(a5)
018212  670e                 beq.b      $18222
018214  0c6d0708fffa         cmpi.w     #$708, -$6(a5)
01821a  6f06                 ble.b      $18222
01821c  303c03e8             move.w     #$3e8, d0
018220  6094                 bra.b      $181b6
loc_018222:
018222  6000ff78             bra.w      $1819c
/* src/fade.c, lines 70-92, a part of wof_menu_input (lines 58-93) */
for (;;) {
    if (wof_key_available()) {
        uint16_t key = (uint16_t)(wof_key_get() & 0xFFFFu);

        if (key == 0x4C) { wof_f.menu_result = -1; CO_RETURN(c); }
        if (key == 0x4D) { wof_f.menu_result =  1; CO_RETURN(c); }
        if (key == 0x44 || key == 0x43) { wof_f.menu_result = 0; CO_RETURN(c); }
    }
    {
        int dir = wof_poll_joy_dir8();

        if (dir == 1) { wof_f.menu_result = -1; CO_RETURN(c); }
        if (dir == 5) { wof_f.menu_result =  1; CO_RETURN(c); }
    }
    if (wof_poll_fire()) { wof_f.menu_result = 0; CO_RETURN(c); }
    CO_WAIT(c);
    wof_f.menu_rounds++;
    if (wof_f.menu_timeout && (int16_t)wof_f.menu_rounds > 0x708) {
        wof_f.menu_result = 1000;
        CO_RETURN(c);
    }
}
CO_END(c);

After a move it waits up to nine rounds for the stick and the button to rest, so that one push moves one item. The attract demo it asks for is not on this disk, so a game starts at the rank under the cursor. The rank is stored twice, as the rank chosen and the rank played, which promotions raise and a high-score row carries (chapter 17).

The briefing

The briefing comes before every mission, on two views of 640 by 147 with three planes: the shape rank from world.shp as its background, the disk's Rank.iff never opened, and on it, in the game's font, the rank's name, the mission number and chapter 17's two counts. It sets no pen, graphics.library's colour number to draw with, so its text takes the one InitRastPort leaves when the game sets up its screens: 0xFF cut to three planes, colour 7, which a test holds. After 240 rounds or on fire the mission begins; Control with R goes back to the rank selection.

A framed panel on black: the rank and the mission, two orange lines, the islands and the ships.

The first mission's briefing, VBlank 7,100, left alone.

The load and save dialog

The load and save dialog lists the saved games and loads one, or saves the game under a typed name. It takes the back view alone, 320 by 200 with four planes, and draws itself with graphics.library in the system font, topaz 8, choosing none. Topaz 8 is in the Kickstart ROM, not on the disk, so the port's build reads it from the ROM image whoever builds supplies (chapter 2), finding the glyphs themselves, since the system fills in a ROM font's header only when it starts. Six slots and two buttons, one to leave and one to end the program, come from a table, every line of their boxes parallel to an axis; the selection is a rectangle filled in the complement mode, which inverts what is there, so a second fill undoes it.

Six outlined slots, the first holding a saved game and highlighted, two buttons below.

The dialog in load mode with this disk's one saved game; every pixel is graphics.library's.

The list is the game's directory as dos.library's ExNext hands out its entries. Look at the test of each name, w, o, f in either case, a full stop and one character more, and at the port's side, which computes the same list:

; re/Wings.lst 0x018A7E-0x018AFA: dialog_file_list [C], a part of 0x018A06-0x018B95
loc_018a7e:
018a7e  0c6d0006fffe         cmpi.w     #$6, -$2(a5)
018a84  6c0000c8             bge.w      $18b4e
018a88  2f2dfff4             move.l     -$c(a5), -(a7)
018a8c  2f2dfff8             move.l     -$8(a5), -(a7)
018a90  4eac83da             jsr        -$7c26(a4)                             ; -> os_dos_ex_next
018a94  504f                 addq.w     #$8, a7
018a96  4a40                 tst.w      d0
018a98  670000b4             beq.w      $18b4e
018a9c  206dfff4             movea.l    -$c(a5), a0
018aa0  10280008             move.b     $8(a0), d0
018aa4  4880                 ext.w      d0
018aa6  3f00                 move.w     d0, -(a7)
018aa8  4eac8398             jsr        -$7c68(a4)                             ; -> tolower
018aac  544f                 addq.w     #$2, a7
018aae  b07c0077             cmp.w      #$77, d0
018ab2  66000096             bne.w      $18b4a
018ab6  206dfff4             movea.l    -$c(a5), a0
018aba  10280009             move.b     $9(a0), d0
018abe  4880                 ext.w      d0
018ac0  3f00                 move.w     d0, -(a7)
018ac2  4eac8398             jsr        -$7c68(a4)                             ; -> tolower
018ac6  544f                 addq.w     #$2, a7
018ac8  b07c006f             cmp.w      #$6f, d0
018acc  667c                 bne.b      $18b4a
018ace  206dfff4             movea.l    -$c(a5), a0
018ad2  1028000a             move.b     $a(a0), d0
018ad6  4880                 ext.w      d0
018ad8  3f00                 move.w     d0, -(a7)
018ada  4eac8398             jsr        -$7c68(a4)                             ; -> tolower
018ade  544f                 addq.w     #$2, a7
018ae0  b07c0066             cmp.w      #$66, d0
018ae4  6664                 bne.b      $18b4a
018ae6  206dfff4             movea.l    -$c(a5), a0
018aea  0c28002e000b         cmpi.b     #$2e, $b(a0)
018af0  6658                 bne.b      $18b4a
018af2  206dfff4             movea.l    -$c(a5), a0
018af6  4a28000c             tst.b      $c(a0)
018afa  674e                 beq.b      $18b4a
/* src/fs.c, lines 493-539 */
/* The n-th name of the game's directory that begins with `wof.`, in ExNext order.  A file
 * the run has written comes before a file of the disk in the same chain, because it is
 * newer; two written files are ordered newest first for the same reason. */
const char *wof_fs_dir_entry(uint32_t index)
{
    const char *best;
    uint32_t    best_key;
    uint32_t    seen = 0;
    uint32_t    last_key = 0;
    const char *last = 0;

    for (;;) {
        best = 0;
        best_key = 0;
        for (uint32_t i = 0; i < FS_WRITE_MAX; i++) {
            if (!fs_written[i].used || !begins_with_wof(fs_written[i].name))
                continue;

            uint32_t key = ((uint32_t)name_hash(fs_written[i].name) << 24)
                         | (0xFFFFFFu - fs_written[i].written);

            if ((!last || key > last_key) && (!best || key < best_key)) {
                best = fs_written[i].name;
                best_key = key;
            }
        }
        for (uint32_t i = 0; i < fs_files; i++) {
            const char *name = (const char *)entry_of(i);

            if (!begins_with_wof(name) || is_deleted(name) || written_find(name))
                continue;

            uint32_t key = ((uint32_t)name_hash(name) << 24) | 0xFFFFFFu;

            if ((!last || key > last_key) && (!best || key < best_key)) {
                best = name;
                best_key = key;
            }
        }
        if (!best)
            return 0;
        if (seen++ == index)
            return best;
        last = best;
        last_key = best_key;
    }
}

There is no sorting: the list keeps the order of the directory's 72 hash chains (chapter 3), six names at most, and would list a directory so named. In save mode the cursor goes straight into the line editor, told below, on a slot's name, and saving over an edited name deletes the old file, so editing a name renames the save. A typed : or / becomes a space, and wof. goes in front. After a load, chapter 17's setup of the loaded mission follows.

The high scores and the name entry

The high-score screen ends a game, behind song 0, with chapter 17's high-score file in memory only while it runs, and two viewports, a picture above a slab. The ten rows go on the slab in the game's font three times: a pixel up and left in pen 0, a pixel down and right in pen 0, then in place in pen 15, white outlined in black. Before it runs the name entry, shown only when the score beats the tenth row's: the dialog's screen and the line editor, with room for 16 characters.

The outer loop

The outer loop is main's loop that runs, after the title sequence, the rank selection, the briefing, a mission and the high-score screen, again and again. A mission is the inner loop, one pass a round, with chapter 17's briefings between missions inside it.

Arrows from the title sequence to the rank selection, its dialog, the briefing, a mission, its end and the high-score screen, and back.

The outer loop. The dialog here is the load item's; in flight Control-G and Control-L open it too (chapter 17).

How a key reaches the game

The keyboard never reaches the logic through the input byte. A key sends a raw key code, a number from 0 to 127 that names the key's place, not its letter, bit 7 set when the key goes up rather than down; so a German keyboard gives the same codes as an American one. With it travels the qualifier, a word of bits for the keys held: the two Shifts, Caps Lock, Control (0x0008), the Alt and the Amiga keys.

The system's input.device passes every event down a chain of handlers by priority. The game's own sits at 127, the highest, so it sees each key first: it drops a key going up, appends the code and the qualifier to the key buffer, ten of each with a count, and clears the event's class, so that the system's own handlers do not act on the game's keys as well. key_get takes the oldest key and shifts the rest down, with an off-by-one. Look at the loop's ble: it runs while the index is at most the new count, so with a full buffer its last turn copies one entry from past the end of each array. Beside it, how the port reads that entry:

; re/Wings.lst 0x0207F2-0x02082C: key_get [asm], a part of 0x0207E4-0x020847
loc_0207f2:
0207f2  4eac8404             jsr        -$7bfc(a4)                             ; -> os_disable
0207f6  7000                 moveq      #$0, d0
0207f8  41ecbca4             lea.l      -$435c(a4), a0                         ; key_qualifier_buffer
0207fc  3010                 move.w     (a0), d0
0207fe  4840                 swap       d0
020800  41ecbc9a             lea.l      -$4366(a4), a0                         ; key_buffer
020804  8010                 or.b       (a0), d0
020806  2f00                 move.l     d0, -(a7)
020808  536cbcb8             subq.w     #$1, -$4348(a4)                        ; key_count
02080c  4241                 clr.w      d1
02080e  4242                 clr.w      d2
020810  41ecbc9a             lea.l      -$4366(a4), a0                         ; key_buffer
020814  43ecbca4             lea.l      -$435c(a4), a1                         ; key_qualifier_buffer
loc_020818:
020818  11b010011000         move.b     $1(a0, d1.w), (a0, d1.w)
02081e  33b120022000         move.w     $2(a1, d2.w), (a1, d2.w)
020824  5241                 addq.w     #$1, d1
020826  5442                 addq.w     #$2, d2
020828  b26cbcb8             cmp.w      -$4348(a4), d1                         ; key_count
02082c  6fea                 ble.b      $20818
/* src/keys.c, lines 64-75 */
/* The original's shift loop runs from index 0 up to and including the new key_count, so
 * with a full buffer its last round copies one entry past the end of each array.  On the
 * machine those two reads land on the next globals: key_buffer + 10 is the first byte of
 * key_qualifier_buffer, which big-endian is the high byte of its first word, and
 * key_qualifier_buffer + 20 is key_count itself.  The port keeps the values rather than the
 * adjacency, so that the quirk survives a struct whose order is its own. */
static uint8_t key_code_at(uint16_t i)
{
    if (i < 10)
        return wof_g.key_buffer[i];
    return (uint8_t)(wof_g.key_qualifier_buffer[0] >> 8);
}

On the machine the byte past the ten codes is the high byte of a qualifier word, and the word past the ten qualifiers is the count. Nothing reads those two places before the buffer fills again, but they are state, compared with the original's after every step, so the port leaves the same values there. It stores them as two fields of their own, so that its buffer need not be laid out as the original's memory was.

The command readers test only Control; the line editor also tests the two Shifts and right Amiga. A qualifier mask that could demand a qualifier on every key is set only in a case a game started from the command line never meets; the port keeps it, dead in play, for the test that runs its other path.

To act on a letter, a reader calls key_to_char, which hands the key to console.device's RawKeyConvert with the system's default keymap, the table that turns a code and its qualifier into characters, and keeps the result only when exactly one character came back. So 0x13 gives r, R with Shift and the control character 0x12 with Control, and a cursor key gives a sequence of two bytes, so none.

The keymap is the system's, in the ROM, and the headless original runs the ROM's own routine on it (chapter 6). The port's core runs no 68000 code, so its build runs that routine once under the emulator, as chapter 5's oracle runs a routine, over every code under the sixteen combinations of the four bits the port can send, both Shifts, Caps Lock and Control: a table of 2,048 bytes, 1,050 of them characters. Writing the routine anew would mean its rules for every kind of key, for a game that never asks for more than one character; running it makes the ROM itself the reference.

The readers and the commands

Five readers look at the buffer: menu_input in the menus, the line editor text_input, the briefing and ingame_keys, once a pass in flight, take keys from it, and wait_input_release only asks whether one waits. The story, the pictures and the high scores read no key. ingame_keys runs before the inner loop's test of the pause, so its commands work paused too and Escape can end the pause; it and the briefing convert a key without its qualifier and test the Control bit apart:

State Keys
Rank selection cursor up 0x4C and down 0x4D move the highlight, wrapping round; Return or keypad Enter chooses
Briefing Control and R: back to the rank selection
In flight and paused Escape 0x45, the pause; with Control: R restart, S the music (chapter 18), F the vertical flip, G save on the carrier, L load, not while a demo plays, C delete the high-score file

Control with B leads into the game's own crash reporter, which the port lacks. The cheat sequence, the plain keys C, O, L, I and N, unlocks debug keys that change the lives, the fuel and the weapons. The manual's Control-D and Control-C are told in chapter 17. A restart keeps the flip and a loaded game sets it from the file; the port puts its remembered preference back after a load (chapter 1).

The line editor, text_input, edits the name entry's name and the dialog's file names; its caret is a complement fill, drawn again to go:

Key What it does
Return or Enter, or fire accept and leave
cursor left, right a character; with either Shift, to the line's start or end
cursor up or down, or the stick leave, to the slot above or below
Backspace, Delete delete before, or under, the caret
right Amiga and X clear the line
any other key converted with its qualifier and inserted, if there is room

There is no filter: any single byte the conversion gives goes in.

What the port made of it

The key path is kept routine by routine, key_to_char over the table; dropped are the handler's mouse half, whose byte nothing reads, the chain of events, with no input.device behind it, and key_get's wait on an empty buffer, which no live caller reaches.

The key layer

In front of the buffer sits the port's key layer, in a file of its own because it is policy decided with the owner, not a port. A browser keeps Control with R, F and L for itself, so each command gets a plain letter, rewritten into what the readers expect:

Key Becomes When Why that letter, that rule
P Escape always Escape works too
V Control-F, the flip always F is fullscreen
G Control-G, save always its own letter
L Control-L, load always its own letter
M Control-S, the music always S is the stick pulled back
R Control-R, restart paused, or in the briefing a stray press would end a campaign
C Control-C, clear the high scores paused a stray press would wipe the high scores

Otherwise a key passes as it came, and inside the line editor every key does. Look at the editor's test around the switch:

/* src/portkeys.c, lines 44-88 */
/* The shell's entry.  The state that decides what happens here - the line editor, the
 * pause, the briefing - lives in the core, which is why this layer does (SPEC 6.1). */
void wof_port_key(uint8_t code, uint16_t qualifier)
{
    uint16_t with_control = (uint16_t)(qualifier | IEQUALIFIER_CONTROL);

    /* The keyboard assist steps the weapon menu with the stick alone; the cursor keys would
     * reach it a second time through last_key (src/assist.c). */
    if (wof_assist_swallows(code))
        return;
    if (!wof_f.editing) {
        switch (code) {
        case RAW_P:                             /* KeyP pauses and continues */
            wof_key(RAW_ESCAPE, qualifier);
            return;
        case RAW_V:                             /* KeyV flips the vertical control; KeyF is */
            wof_key(RAW_F, with_control);       /* the shell's fullscreen key */
            return;
        case RAW_G:                             /* KeyG saves, on the carrier only */
            wof_key(RAW_G, with_control);
            return;
        case RAW_L:                             /* KeyL loads */
            wof_key(RAW_L, with_control);
            return;
        case RAW_M:                             /* KeyM switches the music: KeyS is the stick */
            wof_key(RAW_S, with_control);
            return;
        case RAW_R:                             /* KeyR restarts, paused or in the briefing */
            if (wof_g.pause_flag || wof_f.briefing) {
                wof_key(RAW_R, with_control);
                return;
            }
            break;
        case RAW_C:                             /* KeyC clears the high scores, paused only */
            if (wof_g.pause_flag) {
                wof_key(RAW_C, with_control);
                return;
            }
            break;
        default:
            break;
        }
    }
    wof_key(code, qualifier);
}

The cost: P, V, G, L and M never reach a reader as plain letters outside the editor, and the cheat, whose third key loads, cannot be typed; nothing in the manual is lost, for a plain letter does nothing in the original. The shell (chapter 23) takes H and F for its help screen and fullscreen, and maps KeyboardEvent.code, a place too, to the raw code, leaving out the function keys and Help, so as not to swallow reload or the developer tools; the key left of 1, its diagnostics key; and right Amiga. The port's development keys behind the diagnostics overlay set a score for the name entry, open either dialog, record the following games as the demo, choose PAL or NTSC and step the stereo width.

Two columns: on the Amiga a key, the handler, the buffer, the readers and the ROM's conversion; in the port a key, the shell's map, the key layer, the same buffer and readers, and the table.

From a key to a character: the buffer and its readers are the original's on both sides; the shell's map, the key layer and the table are the port's.

The keyboard assist

Chapter 1 told what the keyboard assist does; here is how. The weapon menu in the hold steps on the input byte, then waits for two more input samples that carry input, so a step costs three input samples, twelve VBlanks, and for 15 ticks after it opens it is not run; a stick is held through that, a key tapped. So the assist turns a press of forward or back in the menu into a push, the stick held for exactly twelve VBlanks, one step at any phase. Two presses made meanwhile are remembered; one in the first 15 ticks waits for the VBlank on which the menu will listen, worked out from its countdown and the queued input samples. The push is never flipped, so up on the key is up in the menu. Elsewhere a direction that goes down arms itself, and the next input sample carries it even after the key is up: a tap shorter than four VBlanks reaches exactly one tick. The menu's steps from twelve taps forward, 20 VBlanks apart, a blank cell a case the test does not run:

Each tap The original With the assist
1 VBlank 1 12
2 VBlanks 2 12
3 or 5 VBlanks 12
4, 8 or 12 VBlanks 4, 8 or 12
held for 240 VBlanks 20 20

In the original the steps grow with the taps, since longer taps cover more of the input samples a step costs. Only the input sample sees the assist; nothing of it is a registered value or writes one, the front end's polls and the button's latches see the stick and the button as they are, and the rank menu, which polls every VBlank, is left alone. The core starts with the assist off, as every comparison runs; the page switches it on.

The waits, the drawing and the files

The front end blocks, and a page cannot: it must hand control back to the browser between frames, or no frame is drawn and no key arrives. So every routine that waits is a coroutine, code that stops at a wait and goes on there at the next call (chapter 22). One CO_WAIT is one VBlank, because the headless original delivers a VBlank where the program waits and the shell calls the core once a VBlank; the port's start runs to its first wait, so the music's first call falls on VBlank 1 in both. Of the four waits, wait_frames_or_fire counts the rounds asked for, ending on fire, wait_input_release at most nine, menu_input past 1,800, and wait_vblank none if its VBlank has come.

graphics.library becomes seven calls on indexed pixels, Move, Text, RectFill, Draw, SetAPen, SetBPen and SetDrMd, with BltTemplate behind the text routines, without clipping, as on the machine. Draw refuses a sloped line, which would need the blitter's line mode, whose pixels the port has not established; the front end draws only the sides of boxes.

What the game writes goes into the file system's overlay, in front of the read-only disk, kept out of the core's state so that loading a state does not un-write a saved game. Its order comes from the names alone: the name's hash modulo 72 gives the chain, chains are walked upward, and in a chain the player's files come first, newest first; a real Kickstart 1.3 doing the same is documented, not confirmed. The dialog's button that ends the program reloads the page (chapter 23).

How it is held

The keys are held under the headless original by the 34 run descriptions of tests/runs/, chapter 1's 21 in flight and paused among them: the front end left alone and with fire, one in the scroller, which reads no key, and 31 in the states that read keys, each command with its key and without. Here the flip, with Control and F, F alone, twice, and no key:

# tests/test_frontend.py, lines 248-255
def test_control_f_flips_the_vertical_control(flight):
    """The command the owner needs.  Control and the F key toggle the byte read_joy_bits reads;
    the same key without Control does nothing."""
    flipped, plain, twice = run('flight-control-f'), run('flight-plain-f'), run('flight-control-f-twice')
    assert byte(flight, OPT_INVERT_VERTICAL) == 0
    assert byte(flipped, OPT_INVERT_VERTICAL) == 0xFF
    assert byte(plain, OPT_INVERT_VERTICAL) == 0, 'the F key alone flipped the control'
    assert byte(twice, OPT_INVERT_VERTICAL) == 0, 'the flip did not toggle back'

The port replays both front-end runs from the program's start, given only their input, the fades at zero: the same files, notes of the music and drawing calls, with places and texts, at the same VBlanks. The headless original draws nothing, so a screen's look rests on those calls, which observers record without changing the run, and on the owner's eyes. Under the oracle run the off-by-one, the qualifier mask, the table against the ROM's routine over all 2,048 combinations, the justified template and the fades. The line editor runs against the original's own text_input under the headless original, over fixed and random keys. A test holds the layer's five letters, the assist's table is a test of the port alone, and the page tests drive the keys in two browsers (chapter 24).

For the developer

Routines: title_sequence 0x018022, story_screen 0x017E80 (its strings at 0x017494, 2,152 bytes), rank_select 0x018262, mission_briefing 0x018590, load_save_dialog 0x018B96, high_score_screen 0x019856, input_handler 0x02075A, key_get 0x0207E4, key_to_char 0x020700, text_input 0x016086. Qualifier bits: 0x0001, 0x0002 the Shifts, 0x0004 Caps Lock, 0x0008 Control, 0x0010 left Alt, 0x0080 right Amiga. Raw codes: Return 0x44, Enter 0x43, cursor keys 0x4C to 0x4F, Backspace 0x41, Delete 0x46, X 0x32; R 0x13, S 0x21, F 0x23, G 0x24, L 0x28, C 0x33. The qualifier mask is left Alt when task_setup (0x0125C6) finds a trap handler not dos's own. A coroutine's local that outlives a wait lives in its context, since the resume skips its setting, and no wait sits inside the routine's own switch (src/coro.h). A run description's key is a decimal raw code, or one with its qualifier (re/notes/keys.md).

What comes next

The chapter in one sentence: the front end is a timetable of waits in VBlanks, and a key a raw code and qualifier in a buffer of ten that the ROM's routine, run once at the port's build, turns into a character, with the port's own keys and assist in front. Chapter 20 gathers the original's quirks, four met here: whether a real machine lists the directory in the port's order; what nothing reads, the fades' second argument, the editor's fifth, the rank selection's return value and the right mouse button's byte; the buffer's off-by-one; the manual's Control-D.

Further reading