Fairlight (The Edge, 1985) is Bo Jangeborg's game, and its engine is his own. It is an isometric flip-screen adventure like Ultimate's Knight Lore, but it works quite differently: a room is not a picture or a set of blocks but a short program of points, lines and textured fills that the game runs each time the knight walks in, and a moving object is put onto the screen straight from four 256-byte buffers and a clean copy of the room, without the screen ever being redrawn as a whole. This page is the outline: the tape, the start-up, a pass of the main loop, the object records, the states objects are in, where everything lies in memory, and what else the tape carries.

The tape and the loader

The tape carries the Alkatraz Protection System's loader. Its BASIC holds a routine that decrypts itself in layers and loads a second, turbo-speed stage, which reads the long block: the loading screen a line at a time in its own order, then the game in pieces, every byte XORed with a key that changes as it goes, with the count of bytes left and with the address it is stored at. The first piece extends the loader's own list of pieces; the last three bytes loaded are the address the loader's last RET goes to, $C47C, and a byte of the checksum (LOADER_TABLE, offset 25). When the checksum holds, the loader clears itself (LOADER_CLEARED) and returns there; when it does not, LOADER_FAILURE wipes memory, asks for the tape to be rewound and resets the machine. The build loads the tape through all of this in SkoolKit's simulator and does the decryption again on the tape's own bytes, checking the result against what the simulator loaded.

The start-up

START is the game's first instruction. It sets the stack two bytes below the end of a stretch of leftover text (SOURCE_AND_STACK), plays the loading tune (LOADING_TUNE) until a key is pressed, points IY at the variables at $FF80, turns interrupts off, and makes its copies:

FromToBytesWhat
OBJECTS_TAPEOBJECTS3,420the object table (and some text after it)
TEMPLATES_TAPETEMPLATES932the object templates and the room patches
OBJECTSMASTER_OBJECTS1,200a master copy of the object table's first 1200 bytes, for new games
$FF80MASTER_VARIABLES61a new game's variables
KNIGHTMASTER_KNIGHT20the knight's record

and jumps to the title (TITLE_SCREEN), a loop the game never leaves: room 79 drawn with the title page's words over it, a key, the master copies put back, the knight put in the start room (TELE, the author's name), the game played inside ROOMST, then room 1 drawn behind GAME OVER, and round again. The places the tables are copied to hold leftover source text on the tape, and the places they are copied from, like the tune itself, become the game's screen buffers as soon as the first room is drawn: the start-up and the tune can run only once.

The tune is two voices on the beeper, each a square wave kept in its own copy of the port byte and sent to port $FE in turn, round a loop whose two ways take the same 96 T-states so that neither voice's pitch disturbs the other's. Each voice has 418 notes, the last 3 of them rests in both; a note lasts 18 passes of 256 turns of the loop, 442,368 T-states, about 0.126 seconds. Played to its end in the simulator with no key pressed, both voices fell silent after 415 notes and 52.6 seconds (the ROM's KEY-SCAN between notes adds a little to each), and the routine went on scanning the keyboard in silence.

Interrupts are off

The tune's routine turns interrupts on as it returns (LOADING_TUNE, at $C018), and the start-up turns them off three instructions later ($C487) -- for good: nothing turns them on again, and the game calls no ROM routine that would. It has to run that way. The sprites begin at $5B00 and lie over the ROM's system variables, which the ROM's interrupt routine writes to fifty times a second: FRAMES, at $5C78, is inside the sprite at SPRITE5C64, and the keyboard variables from $5C00 inside SPRITE5BE8. When this page was built, a second of play in the simulator with interrupts offered every frame left the interrupt flip-flop clear and FRAMES as it was ($00, $00, $C2). Ville Krumlinde's disassembly reads the IM 1 in his snapshot as the ROM's routine running fifty times a second; this disassembly's own earlier notes said the same. Neither is so.

So nothing paces the game: there is no HALT and no wait for the television's frame. A pass of the main loop takes as long as its work takes.

A pass of the main loop

ROOMST enters a room: it moves the records of the things the knight carries up behind his own, draws the room (DRAW_CURRENT_ROOM, which also makes the records of its objects), copies the bare room to the clean copy, draws the doors and still things into it (DRAW_STILL_THINGS), copies it again, and falls into the main loop, MAIN_LOOP, which runs inside it until the game ends. One pass is:

  1. the keys that are not the knight's own: 9 turns the Kempston joystick on or off; SPACE with SYMBOL SHIFT pauses; SYMBOL SHIFT with 0 ends the game; 6 or 7 uses the thing in the chosen place; 1 to 5 choose a place. After a room's first pass its colours are put on here, all at once;
  2. LIFE printed again if it has changed (START_PASS, the author's ST3);
  3. the message line: BLOCKED, LOCKED or TOO HEAVY, for ten passes (SHOW_MESSAGE);
  4. every record from the knight's (the seventh) to the last in use, each updated by the author's CHE3D (CHE3D): the knight's controls, every creature's move, every fall, every collision and every redraw happen inside it.

The game returns from ROOMST when LIFE reaches 00, or on SYMBOL SHIFT and 0; a door, or the thing that carries the knight off, goes back into it with the stack as it was. Timed in the simulator when this page was built, 24 passes in each of a few rooms, standing still and walking, in T-states:

RoomThe knightRecords in useLeastMeanMostPasses a second
room 29standing16241,615261,934281,28013.4
room 29walking (Q, A)16239,415327,932733,04310.7
room 2standing26412,097465,244511,0577.5
room 2walking (Q, A)26414,124542,512951,1066.5
room 20standing28557,347558,370581,2566.3
room 20walking (Q, A)28514,414643,4471,049,0585.4
room 71standing37401,398401,410401,4958.7
room 71walking (Q, A)37342,997465,499895,4497.5

So the game runs at 5 to 13 passes a second, slower where more is moving. The slowest passes are the walking ones in which the knight turns round (every sixth here): turning mirrors all his frames in memory, which costs more than a whole pass (turning round). One pass in room 2, traced part by part and record by record, the knight walking:

Part of the passT-states
LIFE printed, if it changed32
the message line98
every record from the knight's (CHE3D)372,169
the keys between passes425
the whole pass372,724
RecordWhatSpriteT-states in CHE3D
7the knight$933E94,064
8a thing lying (state 0)$819812,912
9a thing lying (state 0)$819812,644
10a thing lying (state 0)$819812,752
11a thing lying (state 0)$819810,491
12a thing lying (state 0)$7D0012,645
13a thing lying (state 0)$7D0012,626
14a thing lying (state 0)$7E5412,519
15a thing lying (state 0)$7E5412,574
16a thing lying (state 0)$7E5412,166
17a thing lying (state 0)$7E5412,194
18a thing lying (state 0)$82468,499
19the troll (state 7)$8BB8143,033
20a door91
21a door91
22no sprite: an invisible box91
23no sprite: an invisible box91
24no sprite: an invisible box91
25no sprite: an invisible box91
26no sprite: an invisible box91

13 of the 20 records took any time: CHE3D returns at once for doors, invisible boxes and still things, which never move and are part of the clean copy. A thing lying on the floor is not still -- it can be pushed, carried or knocked -- and even when it does not move it costs thousands of T-states a pass: gravity asks it to fall every time, and the step is tested against every other record before the floor stops it. A creature or the knight costs its update, its collision tests, and the redraw of its rectangle of screen with everything that overlaps it.

Room 2 at the end of the pass traced

Room 2 at the end of the pass in the table: the knight, walking with Y held, and the troll beside him, with the barrels behind and the table and stools to the right.

The object records

Everything the game moves, draws or collides with is a record of twenty bytes, numbered from 1 at FIXED_RECORDS. The first six have no sprite and are the room itself: its floor, its ceiling and its four walls, huge boxes that each room's patch code moves into place. The seventh is the knight (KNIGHT); after him come the things he carries, then the room's objects, made from the object table and from the room's own drawing (RECORDS). The fields:

OffsetWhat
+0, +1where the sprite's top left corner is on the screen: x, and y counted up from the bottom
+2, +3the sprite's width in pixels and height in rows
+4, +5the sprite: its image, then its mask; 0 for a record nothing draws
+6, +7, +8where it is in the room: +6 and +8 along the floor, +7 the height of its top
+9, +10, +11its size: along +6, down from the top, along +8. The box is a corner and three sizes
+12its kind: the low nibble what it is (1 a door, 4-11 things that do something), bit 4 hurts at a touch, bit 5 can be carried, bit 6 can be killed, bit 7 a fighter
+13the direction it goes in (the bits are on the movement page)
+14its state: the low nibble its behaviour, bit 4 carried along one pass, bits 5 and 6 which way it faces, bit 7 in the air
+15a countdown: to a new course, the end of a jump's rise or a bounce
+160 for a still object, which is never updated; otherwise 16 plus its weight, and bit 5 while it is carried or gone
+17its animation: the frame, the run's last frame, going back
+18its course: a chaser's heading, or the direction it keeps in the air
+19its number in the object table, for the things; 0 for anything else

While a record is being updated, CHE3D copies its first 19 bytes to $FFE4 on, so the code reaches field n as IY+$64+n; the author's leftover source calls that copy T and the variables V, as in (T+13) and (V+3). The same bytes serve the room drawing as its points and mode while a room is drawn, and the sprite drawing as the rectangle it rebuilds: each use sets what it reads first.

States

The low nibble of +14 is an object's state, and CHE3D and its continuation CREATURE_UPDATE are a dispatch on it. Each object type's template (TEMPLATES) gives the state it starts in; read from the templates when this page was built:

StateWhoHandled atWhat it does
0things lyingCHE3Donly falls; a thing just dropped or knocked makes its last movement once more
3type 2I30bounces about: keeps its movement, turning round off whatever it meets
4type 48I4strikes when the knight is within reach in front of it
5the knight in a jumpI5eight passes going up
6, 10types 26, 33GUARD_MOVESguards: patrol, and chase within 30
7type 13I6the troll: chases
8the knightKNIGHT_UPDATEthe controls
9type 27I9rises out of the floor, then chases (or goes for a decoy)
11type 45WRAITH_MOVESthe wraith: chases
12, 14types 40, 51I12hang in the air and flicker
13type 52I6the ghost: wanders, and steals things it touches
15type 56I6stands still until a thing of kind 11 lies in the room, then becomes a wraith

Before any state, CHE3D passes over records with no sprite, doors, things carried or gone, and still things; on a room's first pass it only draws each object; and an object in the air carries on the way it was going. Every case ends in the same place, ANIMATE_AND_MOVE: the frame, gravity, the step, the collisions and the redraw. State 2 is in no template, so the one piece of code that would set one up never runs. The creatures are on their own page.

Where everything is

Memory on the tape and in play

$5B00 to $FFFF, a row a kilobyte and four bytes a pixel: on the left as the tape leaves it at START, on the right once the game runs. Key: code sprites, the font and the textures rooms, parts, the object table and the templates object records and variables the loading tune the clean copy and the compositor's pages leftovers: source text, symbols, the loader unused or unidentified.

FromToBytesWhat
SPRITE5B00$617B1,660sprites (and the system variables under them)
SOURCE_AND_STACK$639B544leftover source text; the stack grows down from its top
MASTER_OBJECTS$689C1,281leftover text on the tape; the master copy of the object table, the variables and the knight's record once the game starts
ZEROS_BEFORE_ROOMS$68AF19zeros
ROOM1$758B3,292the 81 rooms
PART1$7CCA1,855the 56 parts rooms are drawn from
BYTES_AFTER_PARTS$7CFF5353 bytes not identified
SPRITE7D00$A8FB11,260sprites
SOURCE_BEFORE_OBJECTS$A92340leftover source text
OBJECTS$B67F3,420leftover text on the tape; the object table once the start-up has copied it
SOURCE_BEFORE_TITLE$B6856leftover source text
TITLE_PAGE_TEXT$B733174the title page's words
TEMPLATES$BAD7932leftover text on the tape; the templates and patches once copied
FONT$BC17320the font
FIXED_RECORDS$BCA3140the six fixed records and the knight's
RECORDS$BFFF860leftover text on the tape; the room's object records in play
LOADING_TUNE$C0AF176the loading tune's player; the clean copy of the room in play
TUNE_VOICES$C47B972the loading tune's notes
START$C4B962the start-up
SOURCE_AFTER_START$C4DF38leftover text
OBJECTS_TAPE$D1343,157the object table as the tape has it
SOURCE_AFTER_OBJECTS$D2EF443leftover text
TEMPLATES_TAPE$D693932the templates as the tape has them
SOURCE_AFTER_TEMPLATES$DABF1,068leftover text; the clean copy ends at $D7FF, and the compositor's four pages run from $D800 to $DBFF
LOADER_TABLE$DBFF320the loader's table and its cleared code
$DC00$DD38313the end of the cleared loader, and its failure routine
SYMBOL_TABLE$DFF1697the assembler's symbol table
MESS2$E0A3178the closing lines, the message line, symbol scraps
TEXTURES$E3E3832the 26 textures
COMPOSITE_TO_SCREEN$FF697,046the code
UDG_LEFTOVERS$FF7F22bits of the ROM's user-defined graphics
OBJECT_COUNT$FFFF128the variables

The code is about 7 KB, most of it from SHOW_MESSAGE up. Two things make the map unusual. The start-up moves the level data it needs from where the tape has it to where the code reads it, and the places it leaves become buffers: from the first room on, $C000-$D7FF is the clean copy of the room (RESTOR) and $D800-$DBFF the four pages the compositor works in (COMPOSITE_TO_SCREEN). And about 9 KB of what the tape loads is not the game at all.

What else the tape carries

The tape was saved from the development machine's memory, and much of that memory was the tools' rather than the game's. Pieces of the game's own source text lie in the gaps between the game's bytes, and where the start-up copies tables over them. Each line is a carriage return, a two-byte line number and the text; read by their line numbers only:

FromToLinesLine numbers
SOURCE_AND_STACK$639B436250-6670
SOURCE_BEFORE_OBJECTS$B685803130-3920
TEMPLATES$BAD7223970-4180
RECORDS$BFFF214300-4510
$C436$C47B24920-4940
SOURCE_AFTER_START$C4DF212860-12870
SOURCE_AFTER_OBJECTS$D2EF3215380-15690
SOURCE_AFTER_TEMPLATES$DABF8316370-17190

(and 22 more lines, 6680-6890, at MASTER_OBJECTS on the tape, before the start-up copies the master copy over them). The lines near the object table are its own DEFB lines; others are the source of the pick-up, the key reading, the room's set-up and the use of a carried thing, which is how many of the author's labels are known -- matched instruction for instruction to the code they became. They also give his two variable bases, V for $FF80 and T for $FFE4.

Part of the assembler's symbol table survives too (SYMBOL_TABLE): 64 whole symbols, each a node of a tree with a name and a value, and the value of every one an instruction of this very release's code. 20 of them name the first instruction of a routine in this listing, and the listing uses his names for those: WAIT, INPUT, IN31, ATTRI, RESTOR, MIMAN, MIWRAI, MITRO, MW, FACING, DECLI1, DE1, CHE3D, ZZ1, I6, BUT, BUT2, EEN, ZOOMIN, ROOMST. The rest name places inside routines, and are kept as their labels. The title routine's own name is lost but for its last letter: the loader's bytes cut the first node short.

Beside Krumlinde's reading

Ville Krumlinde's disassembly came first, and many facts and some names here are his. It was made from a snapshot taken in play, with the start-up long gone, which is why his entry point and his stack are reconstructions; this listing starts from the tape, at START. Where this page differs from his: interrupts are off, not on; the six records at FIXED_RECORDS are the room's box, not spare records; SAVE_OBJECT_POSITIONS copies the records' places into the object table, not the other way; and the compositor reads all four of its pages (how a moving object is drawn).

Confirmed and inferred