Nightshade (1985) is the first of Ultimate's Filmation II games. Where Knight Lore, Alien 8 and Pentagram show one room at a time, Nightshade's town scrolls: the knight stays in the middle of the screen and the streets move under him. It keeps the earlier games' tune player, menu, key reading, sprite rows and several sound routines, but the town, how it is drawn, how things move through it, the object records and the quest are all its own. This page is the game's outline: the tape and its protection, a turn of the main loop, the object records and how each is updated, and where everything is in memory.

The tape, and the four checks

The tape's BASIC loader loads a loading screen and then four blocks: the game, 34,816 bytes at $6000; 43 bytes into the printer buffer at LOADER; one byte into the system variable NMIADD at NMIADD; and two into FRAMES at $5C78. Then PRINT USR 23424 runs the routine in the printer buffer. It turns the interrupts off for good, sets bit 7 of the R register, and unscrambles the game block a pair of bytes at a time: RLD swaps a nibble between the two, so a pair loaded as ab cd (a nibble a letter) becomes ac db. Then it moves the block down 512 bytes to ENTRY and jumps there. The build does the same to the tape's block and checks that the result is what the snapshot holds.

Each of the three small blocks is a check the game makes on itself, and a copy that skips any of them does not play. A fourth check is aimed at players rather than copiers. Each was tried in the simulator when this page was built, by undoing what the tape did:

CheckWhereWhat the tape leavesUndone in the simulator
FRAMESSTARTFRAMES' middle byte $63. Interrupts are off from the loader on, so FRAMES never counts againWith the middle byte changed, START's RET went straight back to its caller, after 45 T-states. Left as the tape set it, the game went on into its menu
NMIADDDISPATCH, for every object$E9, JP (HL), in NMIADD. Every table of routines in the game is reached through a jump to itWith NMIADD zero, as the ROM leaves it, the game got as far as its first turn and never came round to a second: after five seconds of the game's time the processor was at $0074
R's bit 7STOCK_BUILDINGSR $BD: bit 7 set by the loader. The refresh counter counts only the low seven bits, so only a program's LD R,A can set itWith bit 7 clear, starting a game from the menu jumped to address 0: the Spectrum starts again from its copyright message. With it set, the game played
The livesNEW_LIFEthe DEC (HL) that takes a life, at $CBDD, which each new life checks is still $35With a NOP there, the obvious infinite-lives poke, the knight's first death jumped to address 0

The number of lives is hidden too: a new game takes it from the opcode of the JR that closes the main loop, at $BEDD: $18 shifted right twice is 6, and the first life takes one of them.

A turn

There is no clock in the game at all. Interrupts are off from the loader on, there is no HALT to wait for the television frame, and nothing counts time but the turns themselves. NEW_GAME sets a game up and falls into MAIN_LOOP, one turn, which goes round for as long as the game lasts:

  1. Every object record is updated, the knight's legs first: its graphic picks a routine from UPDATES, called through DISPATCH with IX on the record, and the random number is stirred after each (NEXT_TURN).
  2. The turn is counted. Unless the ending is playing: every sixteenth turn a find may appear in the knight's cell (SPAWN_FIND); every fourth, a monster round him (SPAWN_MONSTER); every 256th, the creature (SPAWN_CREATURE); a bonus when there is none (PLACE_BONUS); a note of any sound effect playing; and the carried things coloured on the panel (COLOUR_CARRIED).
  3. The town and everything in it are drawn into the buffer (DRAW_CELLS; see how the town is drawn).
  4. The knight is coloured by his hits, a new life started if his record has emptied (NEW_LIFE), and the game ended if the villains are gone (CHECK_QUEST_DONE).
  5. END_OF_TURN: the buffers are copied to the screen and cleared (SHOW_PLAY_AREA), a villain's flash is kept for the next turn's colours and ended, and the pause key is read.

So a turn takes as long as it takes. Timed in the simulator when this page was built, 392 turns of walking (and now and then turning) in 49 cells spread across the town, in T-states:

Part of the turnLeastMeanMost
every record updated24,24137,342100,608
the finds, monsters, creature and bonus; the sound effect's note; the panel's colours5661,2723,200
the town and everything in it drawn into the buffer69,355172,741288,888
the knight coloured; a new life; the end of the quest818818818
the buffers copied to the screen and cleared80,75980,75980,759
the flash ended; the pause key read158158158
the whole turn179,321293,090456,420

The mean turn is 84 milliseconds, about 12 turns a second; the quickest ran at 20 a second and the slowest at 7.7. The drawing is most of it, and it depends on how much of the town is built up round the knight: the game slows down among the buildings and speeds up in the open. Copying the buffers to the screen and clearing them costs exactly the same 80,759 every turn. Knight Lore, Alien 8 and Pentagram pad a quiet turn out with a delay (Alien 8); Nightshade has none.

The object records

Everything that moves is one of 23 records of 16 bytes from KNIGHT: half the size of the earlier games' records, since nothing in the town has a height or falls. Each kind of thing has its own records, and the order matters: a thrown object only ever looks for the villain four records on from its own, so the pairing of objects and villains is the order of the records (the quest).

RecordsFromWhat
0KNIGHTthe knight's legshis whole update: the controls, turning, walking, throwing
1KNIGHT_TOPthe knight's topfollows the legs; its own poses
2-3ANTIBODIESantibodies in flightthrown by the knight
4-7FINDSfinds in buildingsand, when a villain dies, its sparkles
8BONUSa bonusa faster walk or the hits back
9-12OBJECTSthe four objectslying, carried (the record then empty) or thrown
13-16VILLAINSthe four villainseach killed only by the object in the same place among the objects
17-22MONSTERSmonstersor the creature

The fields, as the update routines use them:

OffsetFieldWhat it is
+0graphicpicks the update routine (UPDATES) and the sprite (GRAPHICS); 0 is an empty record
+1, +2Uthe high byte the town's column, the low byte the place across the cell
+3, +4Vthe same: the high byte the row
+5speedunits a turn along the facing; a find keeps the type of cell it came from here
+6facingbits 6-7: $00 +V, $40 +U, $80 -V, $C0 -U; the low bits a count -- the knight's turn delay, a monster's turns to its next change of course
+7flagsbit 0 stopped by a wall this turn, bit 1 drawn this turn, bit 5 heads for the knight, bit 6 mirrored, bit 7 upside down (never set)
+8, +9half-sizeshow far it reaches from its centre along U and along V
+A, +Bstepthis turn's move along U and V, signed
+C, +Ddrawing offsetadded to where its centre projects, for the sprite's bottom left corner
+E, +Fwhere drawnthe pixel x and y it was drawn at

One turn of real play, the knight walked about a built-up part of the town from cell (9,23) and the turn with the most records in use out of 240 traced: every record, in the order the main loop takes them, with its graphic, the routine that graphic picks, and the T-states that routine took. An empty record costs the jump to NO_UPDATE, which returns at once. The sounds are played inside the updates, and nothing else happens while they play: record 22, a monster appearing on the screen, spent 35,741 T-states on its note (APPEAR_SOUND).

RecordAddressGraphicUpdate routineT-states
0$BC8E29UPDATE_KNIGHT1,701
1$BC9E45TOP_FOLLOWS517
2$BCAE0NO_UPDATE125
3$BCBE0NO_UPDATE125
4$BCCE0NO_UPDATE125
5$BCDE0NO_UPDATE125
6$BCEE0NO_UPDATE125
7$BCFE0NO_UPDATE125
8$BD0E3HITS_BONUS799
9$BD1E7OBJECT_LYING416
10$BD2E6OBJECT_LYING416
11$BD3E5OBJECT_LYING416
12$BD4E4OBJECT_LYING416
13$BD5E108VILLAIN_WANDER4,029
14$BD6E104VILLAIN_WANDER3,989
15$BD7E100VILLAIN_WANDER2,502
16$BD8E96VILLAIN_WANDER4,029
17$BD9E122MONSTER112_UPDATE4,590
18$BDAE72WANDERING_MONSTER4,125
19$BDBE72WANDERING_MONSTER4,331
20$BDCE122MONSTER112_UPDATE4,591
21$BDDE120MONSTER112_UPDATE2,851
22$BDEE128APPEARING_UPDATE35,741
The screen after the turn traced

The screen after the turn in the table: 17 of the 23 records in use. The play area is the framed part; the panel below and down the left side is drawn straight onto the screen.

Graphics and their routines

The graphic number is the whole of a record's kind: UPDATES gives each of the 158 graphics its update routine, and a thing changes what it is by changing its graphic -- a monster appearing counts up through 128-131 and then becomes a monster, anything destroyed becomes graphic 12, the start of a puff. The graphics come in runs of four frames or directions, and a run shares a routine:

GraphicsUpdate routine
0-1NO_UPDATE
2SPEED_BONUS
3HITS_BONUS
4-7OBJECT_LYING
8-11OBJECT_FLIGHT
12-15VANISHING
16-21UPDATE_KNIGHT
22UPDATE_TOP
23NO_UPDATE
24-29UPDATE_KNIGHT
30UPDATE_TOP
31NO_UPDATE
32-47TOP_FOLLOWS
48-63FIND_WANDER
64-79WANDERING_MONSTER
80-95ANTIBODY_FLIGHT
96-111VILLAIN_WANDER
112-127MONSTER112_UPDATE
128-131APPEARING_UPDATE
132-135VILLAIN_DYING
136-139CREATURE_UPDATE
140-143SPARKLE_FLY
144-151ENDING_VILLAIN
152ENDING_PIT_FRONT
153-157ENDING_PICTURE

Every table of routines is reached the same way: DISPATCH looks the address up and jumps not to it but to NMIADD, where the tape's JP (HL) makes the jump. Besides UPDATES there are five small tables: what an antibody does to a monster (MONSTER_HIT_TABLE), the depth sort's outcomes (DEPTH_TABLE), and three by the knight's or a walker's facing -- coming to a stop (COAST_TABLE), turning towards the knight (TURN_TO_KNIGHT_TABLE) and stopping at the walls (MOVE_TABLE).

Where everything is

FromToBytesWhat
PRINTER_BUFFER$5BFF256the printer buffer, with the loader's routine at LOADER
SYSTEM_VARIABLES$5CCA203the system variables, two of them set by the tape: FRAMES and NMIADD
BASIC_PROGRAM$5DFF309what is left of the BASIC loader, and the game's stack, which grows down into it
ENTRY$5E034the entry
TOWN$62031,024the town map: 32 rows of 32 cells, a byte each
DRAW_ORDER$62A3160the drawing order: 32 records of five steps
BUILDING_TABLE$6575722the building table, the box table and the boxes, and the edge picture under each tile
BUILDING14$6CB51,856the building definitions: two faces of eight columns of two tiles each
PANEL_CHARS$7016865the panel's frame, the font, the tile and graphic tables, the edge pictures
SPRITE152$A25612,864the sprites, with the carried things' pictures and the panel's icons among them
TILE0$BB096,323the tiles the buildings are drawn from
BORDER_CHARS$BBA9160the border's characters
RANDOM$BC8D228the variables
$BC8E$BDFD368the 23 object records
START$E5C310,182the code, with the notes and tunes, the menu, the text and the record templates in it
BUFFER$F0432,688the play area's pixel buffer: 112 lines of 24 bytes
ATTR_BUFFER$F193336its attribute buffer: 14 rows of 24
ATTR_SPILL$F1FF108a byte the colour fill spills into, and unused bytes
MIRROR_TABLES$F7FF1,536tables for drawing mirrored at an even pixel, built at a new game
UNUSED_F800$F8FF256unused
REVERSE_TABLE$F9FF256every byte reversed
SHIFT_TABLES$FFFF1,536tables for drawing shifted by 2, 4 and 6 pixels

The game's code runs from START to $E5C3; below it are the level data and the graphics, above it the buffers and the tables it builds at every new game (MAKE_TABLES). The stack is set once, at START, below the game, and is never set again: every game over leaves two bytes on it and every ending four, since those are jumped into from routines that were called. After 156 game overs in one sitting it has crept down over NMIADD and the game crashes (measured by stage 2 of this disassembly; see START).

Beside the earlier games

Knight Lore, Alien 8, PentagramNightshade
The worldrooms of blocks, one at a timea scrolling town of 32 by 32 cells (TOWN)
Heightseverything has a Z, and fallsnone
Object records40, 56 and 54 records of 32 bytes (Knight Lore, Alien 8)23 of 16
Drawingonly what changed, redrawn in rectanglesthe whole play area every turn
Pacinga delay pads out a quiet turnnone: a turn takes what it takes
DispatchJP (HL) in the codeJP (HL) in NMIADD, loaded from the tape

Confirmed and inferred