{
 "chunk": "0400",
 "unit": "game/scratchAndMessages0400",
 "unitLabels": {
  "game/scratchAndMessages0400": {}
 },
 "dataTypes": {
  "0500": [
   "byte",
   8
  ],
  "0508": [
   "byte",
   8
  ],
  "0510": [
   "byte",
   8
  ],
  "0518": [
   "byte",
   8
  ],
  "0520": [
   "byte",
   4
  ],
  "0524": [
   "byte",
   8
  ],
  "052C": [
   "byte",
   8
  ],
  "0534": [
   "byte",
   8
  ],
  "053C": [
   "byte",
   8
  ],
  "0544": [
   "byte",
   4
  ],
  "0612": [
   "byte",
   8
  ],
  "061A": [
   "byte",
   8
  ],
  "0622": [
   "byte",
   8
  ],
  "062A": [
   "byte",
   8
  ],
  "0632": [
   "byte",
   4
  ],
  "0770": [
   "byte",
   14
  ],
  "077E": [
   "byte",
   26
  ],
  "0798": [
   "byte",
   1
  ],
  "0799": [
   "byte",
   6
  ],
  "079F": [
   "byte",
   20
  ],
  "07B3": [
   "byte",
   11
  ],
  "07BE": [
   "byte",
   1
  ],
  "07BF": [
   "byte",
   20
  ],
  "07D3": [
   "byte",
   8
  ],
  "07DB": [
   "byte",
   4
  ],
  "07DF": [
   "byte",
   4
  ],
  "07E3": [
   "byte",
   2
  ],
  "07E5": [
   "byte",
   1
  ],
  "07E6": [
   "byte",
   4
  ],
  "07EA": [
   "byte",
   9
  ],
  "07F3": [
   "byte",
   13
  ]
 },
 "notes": {
  "0400": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "checksumXorConstant: 1 byte, $B0. The constant verifyProgramChecksum folds into the finished sum at",
    "$04A0 before storing it in programChecksum ($0B7E). It is deliberately OUTSIDE the checked range,",
    "which starts at $0402, so this byte alone shifts the reported build signature without changing the",
    "sum itself.",
    "The whole $0400-$07FF page is a copy of $6F00-$72FF (track 27 sectors 13-16), made by the boot",
    "loader's four-page block move at $C1B3-$C1C3 just before it drops into the checksum below.",
    "Once a battle starts the first half of the page is pure scratch and this byte becomes an array",
    "element - see the page map in the block comment at $04AE."
   ],
   "line": "$B0 = the checksum EOR constant; at run time this is also element 0 of the $0400 scratch arrays"
  },
  "0401": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "mapNameSeedTable: 32 entries x 3 bytes = 96 bytes ($0401-$0460), the manual look-up protection's",
    "question bank, used only by the copy-protection routine at $09A6.",
    "$09C2 executes 'LDA #seed' whose immediate operand at $09C3 is a SID noise byte the boot loader",
    "poked in; AND #$1F picks entry 0-31 and entry*3 indexes this table ($09CC/$09D3/$09DA).  The three",
    "bytes are stored as the 24-bit map seed mapSeed/mapSeed1/mapSeed2 ($0B95-$0B97), generateBattlefieldMap",
    "draws that battlefield, and the player has to type the map's name from the manual; enterMapName packs",
    "what was typed back into a 24-bit number (zp_5A-zp_5C) and $09FC-$0A0F compares it with the very same",
    "three bytes.  So there is no separate answer list: a map name IS the seed, written in base 32 with the",
    "32-character alphabet at $890C.  Three wrong answers and $0A1E wipes all of memory.",
    "Note that the checksum below starts at $0402, so the first byte of entry 0 is not covered by it.",
    "None of this data survives into the game: the labels inside the table ($0410, $0420, $0430, $0432,",
    "$0450) are there because other code uses those addresses as scratch arrays later on."
   ]
  },
  "0410": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "Map-name entry 5 of the table above starts here.  At run time $0410+g is the group summariser's 'worst",
    "movement",
    "cost any member of group g has to pay' (g = 0-15): cleared at $48ED, raised at $4942 from",
    "terrainMoveCostTable[terrain class under the member]."
   ]
  },
  "0420": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "(This address falls inside map-name entry 10.)  At run time $0420+g holds the lowest movement rate found in group g (g = 0-15): preset to $FF at",
    "$48F2 and lowered at $4960, so the whole group ends up moving at the speed of its slowest member."
   ]
  },
  "0430": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "(The last byte of map-name entry 15.)  At run time $0430+g is the copy of groupStatusTable[g] saved at $48E2 before the group table is",
    "rebuilt; only bits 6-7 of the old value are folded back into the new one at $4978."
   ]
  },
  "0432": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "(Map-name entry 16 starts here.)  At run time $0432+n is the squared-distance list that runs parallel to the unit-index list at $0400+n:",
    "collectUnitsInRadius ($37AA) fills both ($3806/$380E), and scanVisibleEnemies penalises an entry in",
    "place at $364F when the target is not in the unit's facing direction."
   ]
  },
  "0450": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "At run time $0450+d is the row half of the map generator's flood-fill stack: $0400+d = column,",
    "$0450+d = row, $04A0+d = the direction bits still to try, d = 0..$4F (80 deep, $71C6-$72EF)."
   ]
  },
  "0461": {
   "unit": "game/scratchAndMessages0400",
   "routine": [
    "verifyProgramChecksum - boot-time integrity check, and the address the boot loader falls into rather than calls.  While loading, the loader rebuilt the stack (the RTS opcode $60 read from bootDone is pushed by the patched PHA at $C1B1, $04 was stored at $01FE by $C26C), so the RTS at $C218 pops the return address $0460 and execution resumes here at $0461.  It sums $0402-$87FF - everything from the protection table up to the last byte of whichever $6F00-$87FF overlay is resident - with a carry-chained ADC/EOR/ROL word loop, parks the raw sum in $8800 (the first byte past the checked range) and stores sum EOR $B0 in programChecksum $0B7E, then enters the game.  $0B7E is never tested locally: it is sent to the opponent in the settings packet ($ED89 in the modem driver) so both machines can notice they are running different builds.",
    "In:  memory $0402-$87FF; $09C3 (the manual-check seed byte, zeroed for the duration so the sum does not depend on it); $0400 = $B0; a stack rigged to return to $0460",
    "Out: $8800 = the raw sum ($69 on this disk image), $0B7E = sum EOR $B0 ($D9 here), $09C3 restored, zp_19/zp_1A/zp_1B clobbered, the two self-modified operand high bytes at $047C and $0484 left pointing at page $88; ends with JMP $0800 (Y = 2, S = $FE)",
    "Called from: nobody - it is entered by the boot loader's obfuscated RTS.  Recomputing the sum from the extracted image gives $69/$D9, while the byte mastered at $0B7E on this d64 is $DB and overlay A preloads the same $DB at $79CC, so this disk image has been altered somewhere in $0402-$87FF."
   ],
   "line": "read the manual-check seed byte - it is the immediate operand of the 'LDA #' at $09C2, poked in by the boot loader from SID noise"
  },
  "0464": {
   "unit": "game/scratchAndMessages0400",
   "line": "park the seed in zp_19; it is zeroed below so that the sum is the same on every boot, and put back at $04A6  (the address $0464 itself becomes a scratch array later - see the page map at $04AE)"
  },
  "0466": {
   "unit": "game/scratchAndMessages0400",
   "line": "read the SID voice-3 noise oscillator $D41B - free random bits"
  },
  "0469": {
   "unit": "game/scratchAndMessages0400",
   "line": "keep bits 0-6"
  },
  "046B": {
   "unit": "game/scratchAndMessages0400",
   "line": "store it in zp_1A and never look at it again: no code in this routine reads zp_1A and every later user of it writes before reading, so these three instructions look like dead leftovers, an old random-seed grab"
  },
  "046D": {
   "unit": "game/scratchAndMessages0400",
   "line": "132 pages to fold in: $04 up to and including $87, i.e. everything up to the end of the resident $6F00-$87FF overlay - at boot time that is the comcen-screens overlay the loader has just installed"
  },
  "046F": {
   "unit": "game/scratchAndMessages0400",
   "line": "A = 0: the running sum starts empty (the same zero also clears the carry counter and the seed byte)"
  },
  "0471": {
   "unit": "game/scratchAndMessages0400",
   "line": "zp_1B counts how many times the ADC carried out; the count is added back into the sum at $049B"
  },
  "0473": {
   "unit": "game/scratchAndMessages0400",
   "line": "Y = 0"
  },
  "0474": {
   "unit": "game/scratchAndMessages0400",
   "line": "Y = 1 ..."
  },
  "0475": {
   "unit": "game/scratchAndMessages0400",
   "line": "... Y = 2: skip $0400 (the EOR constant) and $0401 (the first protection byte) so the sum starts at $0402"
  },
  "0476": {
   "unit": "game/scratchAndMessages0400",
   "line": "zero the seed byte inside the instruction at $09C2, otherwise every boot would checksum differently"
  },
  "0479": {
   "unit": "game/scratchAndMessages0400",
   "line": "clear the carry ONCE, outside the loop: the carry out of each word is deliberately carried into the next one"
  },
  "047A": {
   "unit": "game/scratchAndMessages0400",
   "line": "add the even byte of the word; the operand high byte at $047C is bumped one page per outer pass, so this reads $0402, $0404 ... $87FE"
  },
  "047D": {
   "unit": "game/scratchAndMessages0400",
   "line": "branch if this addition did not carry out"
  },
  "047F": {
   "unit": "game/scratchAndMessages0400",
   "line": "it did: count the carry instead of losing it"
  },
  "0481": {
   "unit": "game/scratchAndMessages0400",
   "line": "step to the odd byte of the word"
  },
  "0482": {
   "unit": "game/scratchAndMessages0400",
   "line": "mix the odd byte in with EOR (so byte order matters); its operand high byte at $0484 is bumped together with the other one"
  },
  "0485": {
   "unit": "game/scratchAndMessages0400",
   "line": "save the ADC carry - the rotate below must not disturb it"
  },
  "0486": {
   "unit": "game/scratchAndMessages0400",
   "line": "start of a hand-made ROL A ..."
  },
  "0487": {
   "unit": "game/scratchAndMessages0400",
   "line": "... branch if bit 7 was clear"
  },
  "0489": {
   "unit": "game/scratchAndMessages0400",
   "line": "... bit 7 fell out of the top, so feed it back into bit 0"
  },
  "048B": {
   "unit": "game/scratchAndMessages0400",
   "line": "restore the ADC carry for the next word  (the address $048C is one of the trainer's accumulator arrays later - see $04AE)"
  },
  "048C": {
   "unit": "game/scratchAndMessages0400",
   "line": "on to the next word"
  },
  "048D": {
   "unit": "game/scratchAndMessages0400",
   "line": "128 words = one page: keep going until Y wraps back to 0"
  },
  "048F": {
   "unit": "game/scratchAndMessages0400",
   "line": "self-modifying: bump the high byte of the ADC operand (the byte at $047C) to the next page"
  },
  "0492": {
   "unit": "game/scratchAndMessages0400",
   "line": "self-modifying: bump the high byte of the EOR operand (the byte at $0484) the same way"
  },
  "0495": {
   "unit": "game/scratchAndMessages0400",
   "line": "one page done"
  },
  "0496": {
   "unit": "game/scratchAndMessages0400",
   "line": "loop over all 132 pages; both operands are left pointing at page $88 when this falls through"
  },
  "0498": {
   "unit": "game/scratchAndMessages0400",
   "line": "Y = 2 on entry to the game - mainStart never reads Y, so this looks like a leftover"
  },
  "049A": {
   "unit": "game/scratchAndMessages0400",
   "line": "clear carry for the final add"
  },
  "049B": {
   "unit": "game/scratchAndMessages0400",
   "line": "add the carry count back in, so no carry out of the chain is lost"
  },
  "049D": {
   "unit": "game/scratchAndMessages0400",
   "line": "park the raw sum in $8800 - the byte immediately after the checked range.  Nothing ever reads it back and the modem status strings are loaded over $8800 later, so this store is only a convenient scratch byte"
  },
  "04A0": {
   "unit": "game/scratchAndMessages0400",
   "line": "fold in the $B0 at $0400, the one byte of this page that the sum deliberately skipped  (this address is also the trainer's fourth accumulator array and the map generator's fill-stack direction array later)"
  },
  "04A3": {
   "unit": "game/scratchAndMessages0400",
   "line": "$0B7E = the build signature handed to the opponent during the settings handshake; $D9 on this image, $DB on an unmodified one"
  },
  "04A6": {
   "unit": "game/scratchAndMessages0400",
   "line": "take the seed byte back out of zp_19 ..."
  },
  "04A8": {
   "unit": "game/scratchAndMessages0400",
   "line": "... and restore the immediate operand at $09C3 so the manual check still picks a random map"
  },
  "04AB": {
   "unit": "game/scratchAndMessages0400",
   "line": "into the game proper: $0800 is a JMP to mainStart ($0A6C), which sets its own stack pointer to $FD"
  },
  "04AE": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "scratchPageTail: 82 bytes of $00 filler on disk ($04AE-$04FF) - everything after the checksum routine.",
    "Nothing in $0400-$04FF is read as data by the running game: in a solo game the trainer's playbook",
    "(track 29 sectors 6-8) is loaded straight over $0200-$04FF, and in every game the page is scratch.",
    "The tenants of the page, all of which are only valid inside the routine that uses them, are:",
    "  $0400+n / $0432+n / $0464+n   unit scan lists (index, squared distance, direction) built by",
    "                                collectUnitsInRadius $37AA and scanVisibleEnemies $35FE",
    "  $0400+unit / $0464+sp         a per-unit deferral counter (a unit whose count is non-zero is put",
    "                                off) and the stack of units it is waiting on, while finishRound",
    "                                rebuilds the next round's process order ($4665-$4750)",
    "  $0400+g / $0410+g / $0420+g / $0430+g   per-group (0-15) member count, worst move cost, lowest move",
    "                                rate and saved status byte ($48DD-$4986)",
    "  $0400+d / $0450+d / $04A0+d   the map generator's flood-fill stack: column, row, remaining",
    "                                directions, up to 80 deep ($71C6-$72EF)",
    "  $0464+i / $0478+i / $048C+i / $04A0+i   the solo trainer's four 12-entry 16-bit accumulators - these",
    "                                are the high bytes, the low bytes live at $88D2/$88DE/$88EA/$88F6",
    "                                (all cleared together at $ED93)",
    "  $04C8-$04E7                   the 32-byte tile compose buffer described below"
   ]
  },
  "04C8": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "Tile compose buffer, 32 bytes ($04C8-$04E7): one battlefield cell drawn at 16x16 pixels, i.e. the 2x2",
    "character cells of the multicolour bitmap that the tactical viewport uses.  Layout: bytes $00-$07 =",
    "top-left character, $08-$0F = top-right, $10-$17 = bottom-left, $18-$1F = bottom-right.",
    "drawViewport builds a cell here: the 4x4-pixel terrain tile is expanded, each pixel doubled in both",
    "directions ($27C2-$27E5 - the high nibble of each source byte becomes the left character, the low",
    "nibble the right one, and every row is written to two consecutive scan lines), the unit glyph is ORed",
    "on top ($28B5-$28BC), and the finished 32 bytes are blitted into the bitmap by drawViewport ($2700,",
    "the full 7x5 redraw) and drawViewportCell ($5D54, a single cell), both of which step the bitmap",
    "pointer on to the next character row after the first 16 bytes."
   ]
  },
  "04C9": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "+$01: the odd scan lines of the top-left character; each expanded terrain row is stored at both $04C8+x and $04C9+x so that it is two pixels tall."
   ]
  },
  "04D0": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "+$08: first scan line of the top-right character (the low nibble of the same source byte)."
   ]
  },
  "04D1": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "+$09: its odd scan lines, written together with $04D0+x."
   ]
  },
  "04DF": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "+$17: last scan line of the bottom-left character."
   ]
  },
  "04E7": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "+$1F: last scan line of the bottom-right character.  $2861-$286C stores $FF into $04C8, $04D0, $04DF",
    "and $04E7 when the unit's stun/dug-in byte has bit 7 set, which draws a solid 16-pixel bar across the",
    "top and the bottom of the cell - the dug-in marker."
   ]
  },
  "0500": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "messagePtrLoTable: 36 bytes, low half of the status-message pointer table, indexed by message id",
    "$00-$23 (the high halves are at $0524 and the sound effect for each id at $0612).  showMessageString",
    "is called with A/Y taken from the two tables at $C536 (message id 0, the chat line) and $C5E5 (the id",
    "pulled off the 8-entry message ring at $9249); the printed message sits on screen for $3C = 60 frames.",
    "Ids $1E, $20 and $21 are $FFFF placeholders on disk and are filled in at run time by setMessageSlot",
    "($C924/$C928, X = id, A/Y = string) - for instance overlay A assembles its rules line from the",
    "template at $0636, copies it to $0332 and registers that copy as id $1E ($8714-$8724).",
    "The 36 messages: 00 the chat line ($0548), 01 WORKING..., 02 END OF FILM., 03 GAME ABORTED.,",
    "04 OPPONENT READY., 05 UNIT RECYCLED., 06/07/08 three SPOTTED RECYCLER! variants, 09 NOT A GAME FILM!,",
    "0A SETUP UNITS. PRESS F 1 WHEN DONE., 0B WAITING FOR OPPONENT..., 0C COMCEN STUNNED!,",
    "0D SPOTTED ENEMY COMCEN!, 0E SPY UNDER ATTACK!, 0F SPOTTED ENEMY SPY!, 10 WAITING FOR CONNECTION...,",
    "11 WE LOST ON POINTS!, 12 WE WON ON POINTS!, 13 IT WAS A DRAW., 14 WE WERE KNOCKED OUT!,",
    "15 WE WON BY A KNOCKOUT!, 16 GAME OVER., 17 PICK UP PHONE THEN PRESS SPACE.,",
    "18 PRESS SPACE, WAIT, HANGUP PHONE., 19 TIMEOUT. RUN/STOP TO RESUME., 1A OPPONENT PICKING GAME/MAP.,",
    "1B RESUMED, 1C THE BATTLE BEGINS!, 1D LAST QTR WARNING!, 1E/20/21 run-time slots, 1F OPPONENT READY",
    "FOR NEW GAME., 22 PHONE TROUBLE., 23 TRY AGAIN."
   ],
   "line": "ids $00-$07: $0548 chat line, $056C WORKING..., $8707 END OF FILM., $0576 GAME ABORTED., $8858 OPPONENT READY., $8713 UNIT RECYCLED., $8721 and $872A SPOTTED RECYCLER! variants"
  },
  "0508": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $08-$0F: $8731 SPOTTED RECYCLER!, $0583 NOT A GAME FILM!, $0593 SETUP UNITS..., $8867 WAITING FOR OPPONENT..., $8742 COMCEN STUNNED!, $8751 SPOTTED ENEMY COMCEN!, $8766 SPY UNDER ATTACK!, $8777 SPOTTED ENEMY SPY!"
  },
  "0510": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $10-$17: $887E WAITING FOR CONNECTION..., $8789 WE LOST ON POINTS!, $879B WE WON ON POINTS!, $87AC IT WAS A DRAW., $87BA WE WERE KNOCKED OUT!, $87CE WE WON BY A KNOCKOUT!, $87E3 GAME OVER., $8897 PICK UP PHONE THEN PRESS SPACE."
  },
  "0518": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $18-$1F: $88B6 PRESS SPACE WAIT HANGUP PHONE., $05B4 TIMEOUT..., $88D6 OPPONENT PICKING GAME/MAP., $05D0 RESUMED, $05D7 THE BATTLE BEGINS!, $05E9 LAST QTR WARNING!, $FFFF run-time slot, $88F0 OPPONENT READY FOR NEW GAME."
  },
  "0520": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $20-$23: two more $FFFF run-time slots, then $05FA PHONE TROUBLE. and $0608 TRY AGAIN."
  },
  "0524": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "messagePtrHiTable: 36 bytes, the high halves of messagePtrLoTable, same indexing.  The three page",
    "values show where each message lives: $05/$06 = in this file, $87 = in the resident $6F00-$87FF",
    "overlay (so those ids only print correctly while overlay B is in memory), $88 = in the message block",
    "loaded from track 29 sectors 9-10, and $FF = an unfilled run-time slot."
   ],
   "line": "high bytes for ids $00-$07"
  },
  "052C": {
   "unit": "game/scratchAndMessages0400",
   "line": "high bytes for ids $08-$0F"
  },
  "0534": {
   "unit": "game/scratchAndMessages0400",
   "line": "high bytes for ids $10-$17"
  },
  "053C": {
   "unit": "game/scratchAndMessages0400",
   "line": "high bytes for ids $18-$1F - $FF at id $1E marks the empty run-time slot"
  },
  "0544": {
   "unit": "game/scratchAndMessages0400",
   "line": "high bytes for ids $20-$23 - two more empty slots, then this file's last two strings"
  },
  "0548": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "chatMessageLine: message id $00, the 36-byte chat line - exactly the width of the message panel.",
    "$0548 is a leading marker glyph: $1E while there is text to show (set at $13FD when an incoming line",
    "has been unpacked and at $C67E), $9E - the same glyph with bit 7 set, so the string ends immediately -",
    "to blank the line ($C62A).  $C5FC tests it and treats a negative value as 'no chat line'.",
    "On disk the whole line is 35 spaces followed by $A0 (space with the terminator bit)."
   ],
   "line": "marker glyph: $1E = there is a line to print, $9E = terminator, print nothing"
  },
  "0549": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "The 35 text bytes of the chat line, rebuilt character by character at $13EF from the raw receive",
    "buffer incomingChatBuffer ($9253): the newest character is stored with bit 7 set (it terminates the",
    "string) and the earlier ones are copied back over it as the line grows.  $0BC5 stores $9E here to",
    "clear the line at the start of a game."
   ]
  },
  "056C": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgWorking: message id $01, 'WORKING...', 10 bytes with the terminator on the final '.'; sound $80 = silent.  Used as the sticky message (persistentMessageId) while a disk load or an overlay swap is in progress ($2988, $7989, $C853)."
   ]
  },
  "0576": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgGameAborted: message id $03, 'GAME ABORTED.', 13 bytes, sound $03.  Queued at $525F when the opponent or the player abandons the game."
   ]
  },
  "0582": {
   "unit": "game/scratchAndMessages0400",
   "line": "the terminating '.' ($AE).  It is broken out of the string above because the data-type map is keyed by address alone and the 1541 fast loader has a data block at its own $0582"
  },
  "0583": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgNotAGameFilm: message id $09, 'NOT A GAME FILM!', 16 bytes, sound $03.  Queued at $81F9 when the block just loaded from a save slot is not a game film."
   ]
  },
  "0593": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgSetupUnitsPressF1WhenDone: message id $0A, 'SETUP UNITS. PRESS F 1 WHEN DONE.', 33 bytes, silent ($80).  Queued at $1FF2 when the pre-battle setup phase starts; F1 (screen 0) ends it."
   ]
  },
  "05B4": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgTimeoutRunStopToResume: message id $19, 'TIMEOUT. RUN/STOP TO RESUME.', 28 bytes, sound $19.  Shown as a sticky message ($5287, $C72B) when the opponent has not answered for too long; RUN/STOP restarts the exchange."
   ]
  },
  "05D0": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgResumed: message id $1B, 'RESUMED', 7 bytes, sound $19.  Queued at $52A6 when the link comes back after a timeout or a pause."
   ]
  },
  "05D7": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgTheBattleBegins: message id $1C, 'THE BATTLE BEGINS!', 18 bytes, sound $03.  Queued at $0B72 when the setup phase ends and the clock starts."
   ]
  },
  "05E2": {
   "unit": "game/scratchAndMessages0400",
   "line": "'BEGINS!' - the tail of the string above, broken out because the 1541 fast loader has a data block at its own $05E2 and data types are keyed by address alone"
  },
  "05E9": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgLastQtrWarning: message id $1D, 'LAST QTR WARNING!', 17 bytes, sound $19.  Queued at $4621 when the game clock enters the last of its four quarters."
   ]
  },
  "05F4": {
   "unit": "game/scratchAndMessages0400",
   "line": "'RNING!' - the tail of the string above, broken out the same way by the fast loader's hiddenSignature at its own $05F4"
  },
  "05FA": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgPhoneTrouble: message id $22, 'PHONE TROUBLE.', 14 bytes, sound $15.  Shown at $5A50 when the modem link fails."
   ]
  },
  "0608": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgTryAgain: message id $23, 'TRY AGAIN.', 10 bytes, sound $03.  Shown at $5A32, usually right after PHONE TROUBLE."
   ]
  },
  "0612": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "messageSoundTable: 36 bytes, the sound effect played when message id X is put on screen.  $C543-$C55E",
    "loads it with Y = the message id and passes it to playSound, except when bit 7 is set ($80 = silent).",
    "The sounds group the messages: $03 neutral notice, $04 spotted/alert, $06 the two phone prompts,",
    "$0D we won, $0E we lost, $15 phone trouble, $19 clock/status (GAME OVER, TIMEOUT, RESUMED, LAST QTR)."
   ],
   "line": "ids $00-$07: chat and WORKING... silent; END OF FILM / GAME ABORTED / OPPONENT READY $03; UNIT RECYCLED $04; the recycler sightings $03"
  },
  "061A": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $08-$0F: SPOTTED RECYCLER $04, NOT A GAME FILM $03, SETUP UNITS silent, WAITING FOR OPPONENT $03, then $04 for COMCEN STUNNED, ENEMY COMCEN, SPY UNDER ATTACK and ENEMY SPY"
  },
  "0622": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $10-$17: WAITING FOR CONNECTION silent, lost $0E, won $0D, draw silent, knocked out $0E, knockout win $0D, GAME OVER $19, PICK UP PHONE $06"
  },
  "062A": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $18-$1F: PRESS SPACE $06, TIMEOUT $19, OPPONENT PICKING $03, RESUMED $19, THE BATTLE BEGINS $03, LAST QTR WARNING $19, run-time slot $03, OPPONENT READY FOR NEW GAME silent"
  },
  "0632": {
   "unit": "game/scratchAndMessages0400",
   "line": "ids $20-$23: run-time slot silent, run-time slot $03, PHONE TROUBLE $15, TRY AGAIN $03"
  },
  "0636": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "optionSummaryTemplate: 23 bytes, ' MENUS XXXX DAMAGE XXX.' with the terminator on the '.'.  It is a",
    "fill-in-the-blanks rules line, not a constant string: appendOnOffWord ($872E in overlay A) copies one",
    "of the words from optionWordTable over the XXXX field at offset 7 and the XXX field at offset $13,",
    "writes '.'|$80 straight after the last word it substituted and adds ',' / ', ' padding, then $8714",
    "copies the finished line to $0332 and $8722 registers that copy as message id $1E.",
    "Offsets: 0 space, 1-6 'MENUS ', 7-10 the OFF/ON field, 11 space, 12-17 'DAMAGE', 18 space,",
    "19-21 the FULL/HALF/NONE field, 22 '.'."
   ]
  },
  "064D": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "optionWordTable: 17 bytes, five bit-7 terminated words at fixed offsets: +$00 'OFF' ($064D), +$03 'ON'",
    "($0650), +$05 'FULL' ($0652), +$09 'HALF' ($0656), +$0D 'NONE' ($065A).",
    "Only the first two are used.  appendOnOffWord ($8734) is the single reader of the table in the whole",
    "program and it enters with Y = 0 (OFF) or Y = 3 (ON), for both fields of optionSummaryTemplate, and",
    "$8632 prints 'OFF' and then - through printStringAtPointer, which carries on from where the previous",
    "string ended - the 'ON' behind it, as the two items of a menu.  FULL, HALF and NONE have no reader at",
    "all: they look like leftovers from a damage option that ended up being just on/off.",
    "(XREF.txt shows this address as eaCopyrightMessage; that is the 1541 drive's own $064D, a different",
    "address space entirely.)"
   ]
  },
  "065E": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgClicks: 10 bytes, '   CLICKS:' - the header of the score line drawn under the map.  Clicks are",
    "Modem Wars' score unit.  $C6BC prints it (A = $5E, Y = $06), then the game clock, then the next two",
    "strings in memory through printStringAtPointer, each followed by a 16-bit score from",
    "sideScoreLo/sideScoreHi ($92A2/$92A4)."
   ]
  },
  "0668": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgUs: 7 bytes, '    US:'.  Printed straight after the clock by the printStringAtPointer at $C6CB,",
    "which continues from wherever the previous string ended - which is why these three labels have to stay",
    "adjacent and in this order.  The number that follows it comes from sideScore indexed by playerSide EOR",
    "1 ($C6CE), so the score arrays look as if they are indexed by the side that conceded the points."
   ]
  },
  "066F": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgThem: 8 bytes, '   THEM:', printed by the same routine at $C6D6 and followed by the score indexed by playerSide itself."
   ]
  },
  "0677": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgTypeMapName: 40 bytes, three lines separated by $0D: 'TYPE MAP NAME' / ' (F7 RANDOM)' /",
    "' (F5 SYM/RND)'.  Printed by the map chooser at $76C9 (A = $77, Y = $06) while the player types the",
    "base-32 name of the battlefield both sides have agreed on, or picks a random / symmetrical-random one.",
    "(The manual look-up check at $09EF prints its own prompt from $0A47, not this one.)"
   ]
  },
  "069F": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "msgBtnToAccept: 27 bytes, two lines separated by $0D: 'BTN TO ACCEPT' / '(F 1 TO FLIP)'.  Printed at",
    "$76D9 while the generated map is on screen; the fire button accepts it and F1 shows it rotated 180",
    "degrees (mapFlipFlag $0B9D)."
   ]
  },
  "06BA": {
   "unit": "game/scratchAndMessages0400",
   "routine": [
    "switchToRequestedScreen - the game's screen dispatcher, called once per iteration from every screen's main loop to honour a pending F1/F3/F5/F7 keypress.  It returns at once while the chat editor has the keyboard, while no function key is pending, or when the key names the screen that is already showing; otherwise it makes the request current, throws the caller's stack away, turns every sprite off, rebuilds the bitmap display and jumps into the new screen's entry point - so it never returns in that case.",
    "In:  zp_66 requestedScreenKey ($00 F1, $01 F3, $02 F5, $03 F7, $FF or a typed character = no request), $90FB currentScreen, $929C chatState, $0B9B bit 7 filmPlaybackMode",
    "Out: either RTS with nothing changed, or: $90FB updated, sprites and their shadows ($9020, $9022, $9023, $9024) cleared, $92F9 cleared unless a film is replaying, S = $FD, and a JMP to enterMapScreen ($1FC3, screen 0, with $9020 preset to $C4), runMissileScreen ($7587, screen 1), enterDroneScreen ($7FE6, screen 2) or enterMainMenu ($141D, anything else)",
    "Called from: the console loop $1463, the battlefield loop $2033 and, in the comcen-screens overlay, the missile screen $75C3 and the drone screen $8040.  Screens 1 and 2 live in that overlay, so they can only be reached while it is resident.  (XREF.txt also lists the map generator as calling $75C3 and $8040; that is an artefact of the two $6F00-$87FF overlays sharing one address range - in the map generator those two addresses hold unrelated code.)"
   ],
   "line": "$929C is non-zero while the chat line editor owns the keyboard"
  },
  "06BD": {
   "unit": "game/scratchAndMessages0400",
   "line": "refuse to switch in the middle of typing a message"
  },
  "06BF": {
   "unit": "game/scratchAndMessages0400",
   "line": "zp_66 = the last key seen by the IRQ poll; the four function keys decode to 0-3 in keyMatrixCodeTable, everything else has bit 7 set"
  },
  "06C1": {
   "unit": "game/scratchAndMessages0400",
   "line": "nothing pending: $FF (no key) and any typed character ($80-$FF) are both negative"
  },
  "06C3": {
   "unit": "game/scratchAndMessages0400",
   "line": "$90FB = the screen already on show: 0 battlefield map, 1 comcen missile, 2 drone, 3 console/main menu"
  },
  "06C6": {
   "unit": "game/scratchAndMessages0400",
   "line": "a different screen was asked for - go and switch"
  },
  "06C8": {
   "unit": "game/scratchAndMessages0400",
   "line": "same screen or nothing to do: back into the caller's main loop"
  },
  "06C9": {
   "unit": "game/scratchAndMessages0400",
   "line": "the requested screen becomes the current one"
  },
  "06CC": {
   "unit": "game/scratchAndMessages0400",
   "line": "$FD is the game's 'empty stack' value ..."
  },
  "06CE": {
   "unit": "game/scratchAndMessages0400",
   "line": "... reset it, because this routine jumps into the new screen instead of returning (mainStart does the same at $0AAC)"
  },
  "06CF": {
   "unit": "game/scratchAndMessages0400",
   "line": "zero for all the sprite registers below"
  },
  "06D1": {
   "unit": "game/scratchAndMessages0400",
   "line": "$9022 = the shadow of $D015 that the IRQ copies out every frame"
  },
  "06D4": {
   "unit": "game/scratchAndMessages0400",
   "line": "and switch all eight sprites off immediately as well"
  },
  "06D7": {
   "unit": "game/scratchAndMessages0400",
   "line": "$9020 = shadow of $D010, the sprite X bit 8 flags"
  },
  "06DA": {
   "unit": "game/scratchAndMessages0400",
   "line": "$9023 = shadow of $D01D, sprite X expansion"
  },
  "06DD": {
   "unit": "game/scratchAndMessages0400",
   "line": "$9024 = shadow of $D017, sprite Y expansion"
  },
  "06E0": {
   "unit": "game/scratchAndMessages0400",
   "line": "$0B9B bit 7 = a game film is being replayed"
  },
  "06E3": {
   "unit": "game/scratchAndMessages0400",
   "line": "during a replay leave the drone flags alone - the recorded command stream has to play back exactly as it was taken down"
  },
  "06E5": {
   "unit": "game/scratchAndMessages0400",
   "line": "clear the drone handshake flags $92F9 (bit 0 = a 'drone finished' command $92 is already queued, bits 4-5 = the launch/arrival states) so the new screen starts clean"
  },
  "06E8": {
   "unit": "game/scratchAndMessages0400",
   "line": "rebuild the whole bitmap display: VIC registers, video matrix, colour RAM and the window frames"
  },
  "06EB": {
   "unit": "game/scratchAndMessages0400",
   "line": "dispatch on the new screen number"
  },
  "06EE": {
   "unit": "game/scratchAndMessages0400",
   "line": "not the battlefield map"
  },
  "06F0": {
   "unit": "game/scratchAndMessages0400",
   "line": "$C4 = %11000100: preset the X bit-8 flag for sprites 2, 6 and 7 - the map screen's sprites that live in the right-hand half of the display"
  },
  "06F2": {
   "unit": "game/scratchAndMessages0400",
   "line": "into the $D010 shadow, before the map screen puts its sprites up"
  },
  "06F5": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 0 (F1): the battlefield map at $1FC3"
  },
  "06F8": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 2?"
  },
  "06FA": {
   "unit": "game/scratchAndMessages0400",
   "line": "no"
  },
  "06FC": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 2 (F5): the drone screen at $7FE6, in the comcen-screens overlay"
  },
  "06FF": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 1?"
  },
  "0701": {
   "unit": "game/scratchAndMessages0400",
   "line": "no - then it can only be 3"
  },
  "0703": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 1 (F3): the comcen missile screen at $7587, in the comcen-screens overlay"
  },
  "0706": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 3 (F7): the console with its STATS / REPAIR / MISC panels at $141D"
  },
  "0709": {
   "unit": "game/scratchAndMessages0400",
   "routine": [
    "runComcenStunnedBlackout - the 'COMCEN STUNNED!' cut-scene: when your command centre has been knocked out, control is taken away, the screen goes blank and the engine keeps running unwatched for exactly one full round of all 100 units before the game hands control back.  It releases the drone, clears the screen, resets the stack, then loops on updateGameFrame with the input lockout re-armed and bit 6 of comcenStunStatus set, which makes the sprite enabler at $31A6 and the cell plotter at $5C7C give up so that nothing is drawn at all, until unitOrderIndex comes back to the value it had on entry; then it queues message $0C, unlocks the input, clears bits 4-6 and re-enters whichever screen was showing.",
    "In:  $9162 unitOrderIndex (captured into the CMP operand at $073E), $922A comcenStunStatus, $90FB currentScreen, $92C9 gameEndCountdown and $0BA8 gameEndReason (abort conditions), $90FC stepDelayTimer",
    "Out: one blind engine round; $0B7D re-armed each step and cleared at the end; $922A left at bit 7 + countdown; message $0C queued through $C566; never returns - it jumps to runMapMainLoop ($1FD0, after redrawing the map and viewport), missileScreenResume ($759C), resetDroneScreen ($7FF6) or enterMainMenuKeepSelection ($1414)",
    "Called from: JMP at $4546 only, taken after a unit step when comcenStunStatus has reached $FF (written by $730D when the missile hits the comcen) and no film is being replayed.  During a replay the cut-scene is skipped and the overlay queues the message on its own at $72F6 instead."
   ],
   "line": "if a drone is still in the air, hand it back ($8230 in the comcen-screens overlay) - the player is about to lose control of everything"
  },
  "070C": {
   "unit": "game/scratchAndMessages0400",
   "line": "wipe the message line and the screen windows: this is the blackout"
  },
  "070F": {
   "unit": "game/scratchAndMessages0400",
   "line": "this routine never returns to the caller of $4546 ..."
  },
  "0711": {
   "unit": "game/scratchAndMessages0400",
   "line": "... so reset the stack to the game's empty value"
  },
  "0712": {
   "unit": "game/scratchAndMessages0400",
   "line": "$9162 = how far the round's 100-unit process order has got; $4537 has just bumped it"
  },
  "0715": {
   "unit": "game/scratchAndMessages0400",
   "line": "self-modifying: this is the operand byte of the CMP at $073D ($073E), so the loop below ends when the round order comes back round to this same unit - exactly one full pass of all 100 units"
  },
  "0718": {
   "unit": "game/scratchAndMessages0400",
   "line": "$CF = %11001111: keep bit 7 (comcen stunned), set bit 6 (run blind) and load the low nibble with a full $F stun countdown.  Just as important, it is no longer $FF, so the test at $453A cannot start this cut-scene again on the next unit step"
  },
  "071A": {
   "unit": "game/scratchAndMessages0400",
   "line": "arm the blackout"
  },
  "071D": {
   "unit": "game/scratchAndMessages0400",
   "line": "top of the blackout loop: re-read the status byte, because updateGameFrame may have changed it"
  },
  "0720": {
   "unit": "game/scratchAndMessages0400",
   "line": "bit 6 suppresses all drawing: the sprite enabler at $31A6 and the viewport plotter at $5C7C both give up when it is set, and updateGameFrame itself skips the input handling at $40BA"
  },
  "0722": {
   "unit": "game/scratchAndMessages0400",
   "line": "store it back"
  },
  "0725": {
   "unit": "game/scratchAndMessages0400",
   "line": "the same $CF value doubles as the input lockout count: the IRQ decrements $0B7D every frame and ignores the keyboard and joystick while it is non-zero.  It is re-armed on every step, so the player stays locked out for the whole round"
  },
  "0728": {
   "unit": "game/scratchAndMessages0400",
   "line": "$92C9 counts down the end-of-game sequence; non-zero means the game is already over"
  },
  "072B": {
   "unit": "game/scratchAndMessages0400",
   "line": "the game is ending - stop the blackout now"
  },
  "072D": {
   "unit": "game/scratchAndMessages0400",
   "line": "$0BA8 = 0 while a game is running (1 aborted, $08 time up, $A0/$C0 a comcen was knocked out)"
  },
  "0730": {
   "unit": "game/scratchAndMessages0400",
   "line": "the game has ended - stop"
  },
  "0732": {
   "unit": "game/scratchAndMessages0400",
   "line": "$90FC = frames still to wait before the engine may take its next unit step"
  },
  "0735": {
   "unit": "game/scratchAndMessages0400",
   "line": "not yet: spin here, re-testing both abort conditions while the IRQ ticks the timer down"
  },
  "0737": {
   "unit": "game/scratchAndMessages0400",
   "line": "one engine step: one unit is processed, with the input locked out and nothing drawn"
  },
  "073A": {
   "unit": "game/scratchAndMessages0400",
   "line": "where has the round order got to now?"
  },
  "073D": {
   "unit": "game/scratchAndMessages0400",
   "line": "the immediate operand was patched at $0715 with the index the round was at when the comcen went down"
  },
  "073F": {
   "unit": "game/scratchAndMessages0400",
   "line": "keep stepping until it comes round again - the index runs 0..99 and is reset to 0 at $474D when the round finishes"
  },
  "0741": {
   "unit": "game/scratchAndMessages0400",
   "line": "message id $0C = 'COMCEN STUNNED!' (the string lives at $8742 in the comcen-screens overlay; sound $04)"
  },
  "0743": {
   "unit": "game/scratchAndMessages0400",
   "line": "put it in the 8-entry message ring at $9249"
  },
  "0746": {
   "unit": "game/scratchAndMessages0400",
   "line": "0 = no lockout ..."
  },
  "0748": {
   "unit": "game/scratchAndMessages0400",
   "line": "... give the keyboard and the joystick back to the player"
  },
  "074B": {
   "unit": "game/scratchAndMessages0400",
   "line": "and stop running blind:"
  },
  "074E": {
   "unit": "game/scratchAndMessages0400",
   "line": "clear bits 4-6 (bit 6 was the blackout), keeping bit 7 'comcen stunned' and the low-nibble countdown that the console's REPAIR panel shows"
  },
  "0750": {
   "unit": "game/scratchAndMessages0400",
   "line": "store the tidied status byte"
  },
  "0753": {
   "unit": "game/scratchAndMessages0400",
   "line": "return to whichever screen was showing when the comcen was hit"
  },
  "0756": {
   "unit": "game/scratchAndMessages0400",
   "line": "not the battlefield map"
  },
  "0758": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 0: redraw the 40x40 overview panel that the blackout wiped ..."
  },
  "075B": {
   "unit": "game/scratchAndMessages0400",
   "line": "... and the 7x5-cell tactical viewport"
  },
  "075E": {
   "unit": "game/scratchAndMessages0400",
   "line": "back into the map's main loop, not its entry point - the screen has just been set up by hand"
  },
  "0761": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 1?"
  },
  "0762": {
   "unit": "game/scratchAndMessages0400",
   "line": "no"
  },
  "0764": {
   "unit": "game/scratchAndMessages0400",
   "line": "the missile screen has its own re-entry point for exactly this case ($759C)"
  },
  "0767": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 2?"
  },
  "0768": {
   "unit": "game/scratchAndMessages0400",
   "line": "no - then it is the console"
  },
  "076A": {
   "unit": "game/scratchAndMessages0400",
   "line": "re-initialise the drone panel; the drone itself was released at $0709"
  },
  "076D": {
   "unit": "game/scratchAndMessages0400",
   "line": "screen 3: back to the console, keeping the menu item that was selected"
  },
  "0770": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "tileVariantMaskTable: 40 bytes ($0770-$0797), indexed by map cell value - $40, and by cell - $20 for",
    "cells below $40 (randomTileVariant $7872 subtracts $40 and adds $20 back if that went negative), so the",
    "first 32 entries serve both the $20-$3F and the $40-$5F cell ranges and the last 8 serve $60-$67.",
    "The byte is a bit mask: the map generator ANDs a fresh scenario random with it and adds the result to",
    "the base tile, so a mask of 0 means 'always this exact tile', 1 means 'one of two' and 3 means 'one of",
    "four'.  Only eight entries are non-zero: cells $49, $4B and $65 (and $29, $2B) get mask 1, cells $4D,",
    "$51, $59, $5D, $61 (and $2D, $31, $39, $3D) get mask 3."
   ],
   "line": "cells $40-$4D (and $20-$2D): all fixed except $49 and $4B (mask 1) and $4D (mask 3)"
  },
  "077E": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "$077E is also the base address of terrainClassTable below: everything that wants the terrain class of a",
    "cell reads $077E + cell value, so the class bytes really start at $077E+$21 = $079F.  The 26 bytes here",
    "are still tile variant masks (cells $4E-$67), which is why the two tables have to sit exactly this far",
    "apart."
   ],
   "line": "cells $4E-$67 (and $2E-$3F): mask 3 at $51, $59, $5D and $61, mask 1 at $65, everything else fixed"
  },
  "0798": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "tileVariantPad: the single $00 that pads the variant masks out to the glyph table.  As terrain class data it would be the class of cell $1A, which never occurs."
   ],
   "line": "$00 pad; read as terrain class data through the $077E base this would be the class of cell $1A, which never occurs"
  },
  "0799": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "unitGlyphBaseTable: 5 bytes plus a pad, the base glyph number of the 16x16 unit picture for each unit",
    "type: $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN.  The facing (unit flags AND 3) is added to",
    "it, then $2C is subtracted and the result doubled to index unitGlyphPointerTable, i.e. glyph =",
    "type*4 + facing ($289A-$28A8 for the tactical view, $7512 and $7538 for the radar screen, where the",
    "enemy version has bit 7 set).",
    "The trailing $00 at $079E is both the pad and, read as terrain class data through the $077E base, the",
    "class of cell $20 - class 0, blank ground."
   ],
   "line": "$2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN, then the $00 pad at $079E"
  },
  "079F": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "terrainClassTable: map cell value -> terrain class 0-8, 75 bytes covering cells $21-$6B.  Every reader",
    "addresses it as $077E + cell (see the note there); $5BC8 gets to the same bytes as $07BE + (cell-$40)",
    "after clamping the index below $21.",
    "The class is what the rest of the engine works with: terrainMoveCostTable $405E (move points charged",
    "per step: 20, 65, 30, 40, 20, 40, 20, 20), terrainHeightTable $38B3 (0, 0, 0, 1, 2, 3, 4, 0 - line of",
    "sight and the combat height bonus), terrainSightBonusTable $35BB (0, 0, 0, 1, 1, 2, 2, 0 - extra range,",
    "doubled when spotting) and terrainColourTable $07EA are all indexed by it.  Class 2 (woods) is also",
    "tested by name at $36B5 and $3729 because it blocks line of sight.  Note that those four engine tables",
    "only have eight entries, so class 8 - the off-map border - runs off the end of them; nothing ever walks",
    "or shoots there."
   ],
   "line": "cells $21-$34 -> class 3, the lower contour band: costs 40 move points, height 1, +1 sight"
  },
  "07B3": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $35-$3F -> $FF, an invalid class: these cell values are never put on the map"
  },
  "07BE": {
   "unit": "game/scratchAndMessages0400",
   "line": "cell $40 -> class 0, plain open ground: 20 move points, no height, no sight bonus, drawn yellow"
  },
  "07BF": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $41-$54 -> class 5, the upper contour band: 40 move points, height 3, +2 sight"
  },
  "07D3": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $55-$5C -> class 1, river: 65 move points, the most expensive terrain in the game, drawn cyan"
  },
  "07DB": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $5D-$60 -> class 2, woods: 30 move points and the only terrain that blocks line of sight, drawn green"
  },
  "07DF": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $61-$64 -> class 4, hill: 20 move points, height 2, +1 sight"
  },
  "07E3": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $65-$66 -> class 6, summit: 20 move points, height 4 (the best view in the game), +2 sight"
  },
  "07E5": {
   "unit": "game/scratchAndMessages0400",
   "line": "cell $67 -> class 7, the recycler pad where damaged units are repaired ($3F27 stamps $67 into the map)"
  },
  "07E6": {
   "unit": "game/scratchAndMessages0400",
   "line": "cells $68-$6B -> class 8, the off-map border: $278A builds these four values as $68 + ((row+col) AND 3) for any cell outside the 40x40 battlefield"
  },
  "07EA": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "terrainColourTable: 9 bytes, the colour-RAM nibble for terrain class 0-8, read at $27AB (viewport) and",
    "$5BCB (overview map) straight after a class look-up: 7 yellow open ground, 3 cyan river, 5 green woods,",
    "8 orange for the contour bands, hills and summits, 0 black for the recycler pad and the off-map border."
   ],
   "line": "class 0-8: $07 yellow, $03 cyan, $05 green, $08 orange x4, $00 black x2"
  },
  "07F3": {
   "unit": "game/scratchAndMessages0400",
   "block": [
    "table0400Tail: 13 bytes of $00 filler to the end of the page.  The block move that installed the page copies four whole pages, so this is simply whatever followed the tables in the source file."
   ],
   "line": "unused filler"
  }
 }
}
