{
 "chunk": "game_main_0800_4FC4",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "4FC4",
   "name": "readCmdArgs3",
   "summary": "Fetches the three parameter bytes that follow the current command byte in the merged command buffer ($908F, read index $90B7) into cmdParam0/1/2 ($73/$74/$75) and leaves $90B7 pointing at the next command. Listed as data; every 4-byte command handler starts with JSR $4FC4.",
   "inputs": "$90B7 = index of the command byte in $908F",
   "outputs": "$73,$74,$75 = parameters; $90B7 advanced by 4; Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "4FD0",
   "name": "readCmdArgs2",
   "summary": "Entry into readCmdArgs3 used by 3-byte commands: reads the two bytes after the command byte into cmdParam1/cmdParam2 ($74/$75) and advances $90B7 by 3. Hidden as data.",
   "inputs": "$90B7",
   "outputs": "$74,$75; $90B7 += 3",
   "confidence": "high"
  },
  {
   "addr": "4FE4",
   "name": "mirrorCmdCoordsIfRemote",
   "summary": "If the command being executed came from the opponent (cmdFromOpponent $76 bit 7) converts cmdParam1/cmdParam2 from the opponent's coordinate system with mirrorMapCoordinate ($3FA3, 39-x) so that both machines run the same simulation while each player sees his own side at the bottom.",
   "inputs": "$76 bit 7, $74/$75 = column/row",
   "outputs": "$74/$75 mirrored when remote",
   "confidence": "high"
  },
  {
   "addr": "4FF7",
   "name": "readCmdArg1",
   "summary": "Reads the single byte after the command byte into cmdParam0 ($73) and advances $90B7 by 2. Hidden as data; used by all 2-byte unit/group commands.",
   "inputs": "$90B7",
   "outputs": "$73; $90B7 += 2",
   "confidence": "high"
  },
  {
   "addr": "5005",
   "name": "mirrorCmdCoordsIfRemoteDec",
   "summary": "For a remote command mirrors cmdParam1/2 and then decrements both, i.e. mirrors the top-left corner of a 2x2-cell object (the drone); no-op for own commands.",
   "inputs": "$76, $74/$75",
   "outputs": "$74/$75 = 38-x, 38-y when remote",
   "confidence": "medium"
  },
  {
   "addr": "5011",
   "name": "cmdMoveUnit",
   "summary": "Handler of command $80 (MOVE, 4 bytes: unit, col, row): reads and mirrors the arguments, then jumps to the move-order routine $34C6 which clears the unit's group-membership bit, refreshes its display flags and sets the destination. Hidden as data; reached through cmdHandlerTable ($4D2A).",
   "inputs": "$908F command bytes",
   "outputs": "unit destination tables updated via $34C6",
   "confidence": "high"
  },
  {
   "addr": "501A",
   "name": "cmdMoveGroupRelative",
   "summary": "Handler of command $81 (4 bytes: unit, col, row): reads/mirrors the arguments and jumps to $4A52, which moves every member of the unit's group by the same offset as the named unit (formation move). Hidden as data.",
   "inputs": "$73 unit, $74/$75 target",
   "outputs": "group destinations via $4A52",
   "confidence": "high"
  },
  {
   "addr": "5023",
   "name": "cmdCloakOn",
   "summary": "Handler of command $82 (2 bytes: unit): first clears the unit's blitz bits through cmdBlitzOff (the read index is saved in the self-modified operand at $502D and restored), then re-reads the unit and, unless it is type 3 (spy), sets unitWaypointCol bits 6-7 (cloak ordered + active). Falls into the shared tail at $5058 (own command: pendingRedrawUnit $923B = unit) and updateComcenCloakLock. Hidden as data.",
   "inputs": "$908F, $90B7",
   "outputs": "F898[unit] |= $C0, F8FC[unit] &= $3F, $923B, $9245",
   "confidence": "high"
  },
  {
   "addr": "504B",
   "name": "cmdCloakOff",
   "summary": "Handler of command $84 (2 bytes: unit): clears bits 6-7 of unitWaypointCol (cloak off). Tail at $5058: for an own command records the unit in pendingRedrawUnit ($923B) so the tick loop redraws it, then updateComcenCloakLock. Hidden as data; also called as a subroutine by cmdBlitzOn.",
   "inputs": "$73 after readCmdArg1",
   "outputs": "F898[unit] &= $3F; $923B; $9245",
   "confidence": "high"
  },
  {
   "addr": "5062",
   "name": "cmdBlitzOff",
   "summary": "Handler of command $95 (2 bytes: unit): clears bits 6-7 of unitWaypointRow (blitz ordered/active). Hidden as data; also called by cmdCloakOn.",
   "inputs": "$908F",
   "outputs": "F8FC[unit] &= $3F",
   "confidence": "high"
  },
  {
   "addr": "5070",
   "name": "cmdBlitzOn",
   "summary": "Handler of command $93 (2 bytes: unit): calls cmdCloakOff for the unit (cloak and blitz are exclusive), re-reads the unit and, unless it is a spy (type 3), sets unitWaypointRow bits 6-7 and jumps to applyBlitzStepCost ($3EE2). Hidden as data.",
   "inputs": "$908F",
   "outputs": "F8FC[unit] |= $C0, F898 cleared, energy/movement via $3EE2",
   "confidence": "high"
  },
  {
   "addr": "5098",
   "name": "cmdGroupCloakOn",
   "summary": "Handler of command $83 (2 bytes: group id): first runs cmdGroupBlitzOff for the group, then re-parses the argument with X=$80 through the shared group routine at $50D4 (table F898, OR $C0): every living non-spy member of the group gets its cloak bits set, groupStatusTable[group] gets bit 7 set, and for an own command pendingRedrawGroup ($923A) = group. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F898 of members |= $C0, F8FC &= $3F, $91F4[g], $923A, $9245",
   "confidence": "high"
  },
  {
   "addr": "50AA",
   "name": "cmdGroupBlitzOn",
   "summary": "Handler of command $94 (2 bytes: group id): runs cmdGroupCloakOff for the group, then the shared group routine with table F8FC, OR $C0 and the applyBlitzStepCost call enabled: all living non-spy members get blitz set and pay the blitz cost; groupStatusTable[group] = (old & $BF) | $40. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F8FC of members |= $C0, $91F4[g]",
   "confidence": "high"
  },
  {
   "addr": "50BE",
   "name": "cmdGroupBlitzOff",
   "summary": "Handler of command $96 (2 bytes: group id) and entry of the self-modifying group routine: patches the operands at $512D/$514B/$514D/$5134/$5139/$513B (table F8FC, AND $3F, OR 0, no blitz cost) and continues at $50EB. groupStatusTable[group] &= $BF. Hidden as data; also used by cmdGroupCloakOn.",
   "inputs": "$908F",
   "outputs": "F8FC of members &= $3F, $91F4[g] &= $BF",
   "confidence": "high"
  },
  {
   "addr": "50D2",
   "name": "cmdGroupCloakOff",
   "summary": "Handler of command $85 (2 bytes: group id): X=0 -> shared routine with table F898, OR 0: clears the cloak bits of every member, groupStatusTable[group] &= $7F. The common body at $50EB-$5150 reads the group byte, ORs $40 into it to match unitFlagsTable & $7C, loops over all 100 units and finally jumps to updateComcenCloakLock. Hidden as data.",
   "inputs": "$908F; patched operands",
   "outputs": "F898 of members &= $3F, $91F4[g] &= $7F, $9245",
   "confidence": "high"
  },
  {
   "addr": "5151",
   "name": "updateComcenCloakLock",
   "summary": "Shared tail of the cloak/blitz handlers: unitInspectLockFlags ($9245) = unitWaypointCol[comcenUnit] & $80, i.e. bit 7 set while the player's own comcen is cloaked. Hidden as data.",
   "inputs": "$9236, F898",
   "outputs": "$9245",
   "confidence": "medium"
  },
  {
   "addr": "515D",
   "name": "cmdClearTarget",
   "summary": "Handler of command $86 (2 bytes: boomer unit): getBoomerTarget ($3C74) gives the boomer ordinal in Y and boomerTargetTable[Y] is cleared (CLEAR TARGET menu item). Hidden as data.",
   "inputs": "$908F",
   "outputs": "$91DC[ordinal] = 0",
   "confidence": "high"
  },
  {
   "addr": "516B",
   "name": "cmdSetTarget",
   "summary": "Handler of command $87 (3 bytes: boomer unit, target unit): reads the two arguments, moves them so that $73 = boomer and $75 = target (the EOR #$80 on $76 is undone again and has no effect) and jumps to setBoomerTargetCmd ($3C50). Hidden as data.",
   "inputs": "$908F",
   "outputs": "$91DC[ordinal] = target | $80",
   "confidence": "high"
  },
  {
   "addr": "5185",
   "name": "cmdJoinGroup",
   "summary": "Handler of command $88 (3 bytes: unit, group id): the register shuffle is a no-op apart from $73 = group; jumps to $4AD4 which adds unit $74 to group $75 (MEMBERSHIP/JOIN GROUP). Hidden as data.",
   "inputs": "$908F",
   "outputs": "unit group bits via $4AD4",
   "confidence": "high"
  },
  {
   "addr": "519B",
   "name": "cmdLeaveGroup",
   "summary": "Handler of command $89 (2 bytes: unit): clears bit 6 (group membership active) of unitFlagsTable and jumps to $4C6B which copies bits 2-6 of unitFlagsTable into unitDisplayFlags. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F708[unit] &= $BF, FB54[unit]",
   "confidence": "high"
  },
  {
   "addr": "51AB",
   "name": "cmdPlaceUnit",
   "summary": "Handler of command $8A (4 bytes: unit, col, row): teleports the unit - position, destination (with the idle bit), waypoint (keeping the cloak/blitz bits which are read into the patched ORA operands at $51C9/$51E5) and unitDisplayFlags bit 7 are all set to col/row. Used when units are positioned (setup phase / deployment). Hidden as data.",
   "inputs": "$908F",
   "outputs": "F640,F6A4,F7D0,F834,F898,F8FC,FB54 of the unit",
   "confidence": "medium"
  },
  {
   "addr": "51EA",
   "name": "cmdSetCustomMapSeed",
   "summary": "Handler of command $A6 (4 bytes: 3 seed bytes): sets mapKindFlag ($0B9D) = 1 and falls into cmdSetMapSeed. Sent by overlay A ($79BD) during game setup. Hidden as data.",
   "inputs": "$908F",
   "outputs": "$0B9D = 1, then as cmdSetMapSeed",
   "confidence": "medium"
  },
  {
   "addr": "51EE",
   "name": "cmdSetMapSeed",
   "summary": "Handler of command $8B (4 bytes): mapKindFlag ($0B9D) = 0, copies the three argument bytes into the map seed $0B95-$0B97 and clears bit 7 of gamePhase ($B1). Hidden as data.",
   "inputs": "$908F",
   "outputs": "$0B9D, $0B95-$0B97, $B1 &= $7F",
   "confidence": "medium"
  },
  {
   "addr": "520C",
   "name": "cmdComcenRepairedOrSetupReady",
   "summary": "Handler of the 1-byte command $9E. During the battle ($B1 == 0) it clears the stun counter (low nibble of unitStunTable) of the sending side's comcen - the successful comcen repair from repairTabAction. In the setup phases ($B1 != 0) it is the 'ready' acknowledgement: clears the sender's bit in pauseState ($BE) via getPauseClearMask, sets uiDelayTimer $0B7D = $20, injects key code $20 into newKeyEvent ($90EA) and resets chatState ($929C). Hidden as data.",
   "inputs": "$B1, $76, $0B9F, $0C8A",
   "outputs": "F960[comcen] &= $F0 or $BE/$0B7D/$90EA/$929C",
   "confidence": "medium"
  },
  {
   "addr": "523E",
   "name": "cmdSessionControl",
   "summary": "Handler of command $8C (2 bytes: reason). Executed immediately while the packets are merged (not queued). Reason 0 = opponent aborted / left the game: sets $923F = $C0, gameEndCountdown $92C9 = $62 (no game running) or $71 (game running, with 'GAME ABORTED.' when remote) and aborts the dispatcher by dropping two return addresses and jumping to the comm module with X=2. Reasons 1-2 do nothing here (2 = READY handshake handled by readyForNewGameHandshake). Reason 3 = pause/timeout: pauseState = 3, 'TIMEOUT. RUN/STOP TO RESUME.' kept as persistent message, input lockout 60 frames. Reason 5 = disconnect: pauseState = $80. Reason 4 (and others) = resume: clears the sender's pauseState bit (getPauseClearMask); when no bits remain the message is cleared and 'RESUMED' shown.",
   "inputs": "A = reason byte, X/Y preserved through saveRegsForCmdReturn, $76, $0BA8, $0B9B",
   "outputs": "$BE, $92A1, $0B7D, $929C, $92C9, $923F, messages",
   "confidence": "high"
  },
  {
   "addr": "52AE",
   "name": "getPauseClearMask",
   "summary": "Returns the AND mask used to clear pauseState bits when one side resumes: $FC (both bits) for comm build 1 ($0BA5 bit 7), $FD for an own resume, $FE for the opponent's resume; in the latter case, if the opponent's bit (bit 1 of X = old pauseState) was set, 'OPPONENT READY.' (message 4) is queued.",
   "inputs": "X = current pauseState, $0BA5, $76",
   "outputs": "A = mask",
   "confidence": "medium"
  },
  {
   "addr": "52C8",
   "name": "cmdDroneMove",
   "summary": "Handler of command $8D (4 bytes: heading, col, row), executed immediately by the packet merger, never queued. Stores heading/col/row in $73-$75, mirrors for a remote drone (mirrorCmdCoordsIfRemoteDec), and unless droneFlags $92F9 bits 4-5 are set, sets bit 0 of $920D and - when screen 1 (the map/radar screen) is active - disables sprite 0 in spriteEnableShadow, stores the drone position in $92F6/$92F7, converts it to sprite coordinates with $340B, sets sprite 0 shape via $3340 from (heading+4)&7 and colour 1.",
   "inputs": "$E020 packet bytes at Y, $76",
   "outputs": "$92F6/$92F7, $920D, sprite 0 shadows; X/Y restored to the saved values",
   "confidence": "medium"
  },
  {
   "addr": "532F",
   "name": "saveRegsForCmdReturn",
   "summary": "Stores X and Y into the LDX/LDY operands at $532A/$532C so that cmdSessionControl and cmdDroneMove return with the registers the packet merger had (the merge loop index).",
   "inputs": "X, Y",
   "outputs": "patched $532B/$532D",
   "confidence": "high"
  },
  {
   "addr": "5336",
   "name": "cmdDroneDetonate",
   "summary": "Handler of command $8E (3 bytes: col, row): own command clears $920D bit 5; if droneFlags bit 7 is already set (drone finished) the command byte in the buffer is overwritten with $0E so the film records a no-op. Otherwise droneFlags = $40, coordinates mirrored/decremented for the opponent, sound $11, A=$19 to overlay B $72A1 (blast), explosion effect at col/row through $33C8 with A=1, and for a remote command $920D &= $C0. Hidden as data.",
   "inputs": "$908F, $76, $92F9, $0B9B",
   "outputs": "$92F9, $920D, $908F command byte, sound/graphics",
   "confidence": "medium"
  },
  {
   "addr": "537B",
   "name": "cmdDroneRelease",
   "summary": "Handler of the 1-byte command $8F: advances the read index and, for a remote command, clears bits 0-5 of $920D (drone control finished on the other side). Hidden as data.",
   "inputs": "$76",
   "outputs": "$920D",
   "confidence": "low"
  },
  {
   "addr": "538B",
   "name": "cmdDroneArrived",
   "summary": "Handler of the 1-byte command $92 (sent by overlay B $79CA when the drone is within one cell of its target): $920D &= $3F and for an own command droneFlags |= $10. The packet merger additionally sets droneFlags = $80 as soon as a $92 byte is seen in either packet. Hidden as data.",
   "inputs": "$76",
   "outputs": "$920D, $92F9",
   "confidence": "medium"
  },
  {
   "addr": "53A3",
   "name": "cmdDroneLaunch",
   "summary": "Handler of the 1-byte command $97: droneFlags = 0; for the opponent's launch plays sound 7 and shows 'DRONE ALERT!' (overlay B $820F); decrements the launcher's remaining drone count dronesLeft[$92AE + 0 own / 1 opponent] if nonzero and calls $403B (stats refresh). Hidden as data.",
   "inputs": "$76, $92AE",
   "outputs": "$92F9 = 0, $92AE",
   "confidence": "medium"
  },
  {
   "addr": "53C0",
   "name": "cmdMissileStrike",
   "summary": "Handler of command $90 (3 bytes: col, row). In the setup phase ($B1 != 0) it just stores the two values as dronesLeft/missilesLeft for the sender ($92AE/$92B0 index 0 own, 1 opponent). In battle: if the sender still has missiles and the game runs, decrements missilesLeft, calls $403B, mirrors the coordinates, and if on the map hits the unit standing there with 14 energy (7 for a comcen, via $3B22), draws the explosion effect ($33C8 with A=0) and plays sound $0C. Hidden as data.",
   "inputs": "$908F, $B1, $76, $0BA8",
   "outputs": "$92AE/$92B0, unit damage, effects",
   "confidence": "medium"
  },
  {
   "addr": "542E",
   "name": "cmdSelfDestruct",
   "summary": "Handler of command $91 (2 bytes: unit, bit 7 = whole group of that unit): destroys the unit, or every living member of the unit's group, through destroyUnit. Hidden as data.",
   "inputs": "$908F",
   "outputs": "unitTypeTable entries = $FF",
   "confidence": "high"
  },
  {
   "addr": "5459",
   "name": "destroyUnit",
   "summary": "Kills unit $73 unless it is a comcen (49 or 99): sound/explosion $34 via $3B52 and unitTypeTable[unit] = $FF (slot freed). Hidden as data.",
   "inputs": "$73 = unit",
   "outputs": "F76C[unit] = $FF",
   "confidence": "medium"
  },
  {
   "addr": "5470",
   "name": "cmdMoveKeepGroup",
   "summary": "Handler of command $98 (4 bytes: unit, col, row): reads/mirrors the arguments and jumps to $4C59, which decrements pendingGroupOrderCount ($929E) when the unit belongs to pendingGroupOrderId ($929D) and then sets the destination through $34D5 without touching the group bits (MOVE TO GOAL for group members). Hidden as data.",
   "inputs": "$908F",
   "outputs": "destination tables, $929E",
   "confidence": "medium"
  },
  {
   "addr": "5479",
   "name": "cmdAddEnergy",
   "summary": "Handler of command $9A (3 bytes: unit, amount): unless the game ended by knockout ($0BA8 bit 7) adds amount to the unit's energy (bits 0-5, capped at 50) keeping the spotted bits 6-7. Sent by the comcen energy repair. Hidden as data.",
   "inputs": "$908F, $0BA8",
   "outputs": "F9C4[unit]",
   "confidence": "high"
  },
  {
   "addr": "549E",
   "name": "cmdHaltGroup",
   "summary": "Handler of command $9B (2 bytes: group id): for every living member of the group sets destination and waypoint to the current position, the idle bits (unitDestCol bit 7, unitDisplayFlags bit 7), keeps the cloak bits but clears the blitz bits. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F7D0,F834,F898,F8FC,FB54 of members",
   "confidence": "high"
  },
  {
   "addr": "54E1",
   "name": "cmdSetRecycler",
   "summary": "Handler of command $9C (4 bytes: mode, col, row): recyclerMode[side] = mode (side = sender's side); if mode < 2 the coordinates are mirrored for a remote sender and stored in recyclerCol/recyclerRow[side]; an own command also draws the recycler on the map (drawRecyclerOnMap with Y=$FF). Sent by overlay A ($8436) during setup. Hidden as data.",
   "inputs": "$908F, $0B9F, $76",
   "outputs": "$92A6/$92A8/$92AA",
   "confidence": "high"
  },
  {
   "addr": "5511",
   "name": "cmdChooseSide",
   "summary": "Handler of command $9D (2 bytes: chosen side). Comm build 1: local side forced to 0. Otherwise only the opponent's choice matters: if it differs from ours (selectedUnit $90F8 holds our pick) playerSide = (our pick > theirs), else playerSide = random | $80 (conflict to be resolved); gamePhase $B1 = $DF ends the side-selection phase. Hidden as data.",
   "inputs": "$908F, $0BA5, $76, $90F8",
   "outputs": "$0B9F, $B1",
   "confidence": "medium"
  },
  {
   "addr": "553B",
   "name": "sendChooseSide",
   "summary": "Queues command $9D with the locally chosen side (playerSide copied to cmdParam0 and selectedUnit). Called from overlay A's side selection ($7976).",
   "inputs": "$0B9F",
   "outputs": "outgoing ring, $90F8",
   "confidence": "medium"
  },
  {
   "addr": "5548",
   "name": "cmdDigIn",
   "summary": "Handler of command $9F (2 bytes: unit): setUnitDigBits with $20 -> unitStunTable bits 5-7 = 001 (digging in progress). Hidden as data.",
   "inputs": "$908F",
   "outputs": "F960[unit]",
   "confidence": "high"
  },
  {
   "addr": "554C",
   "name": "cmdDigOut",
   "summary": "Handler of command $A0 (2 bytes: unit): setUnitDigBits with 0 (clears dig-in/dug-in bits) and clears the dug-in bit 6 of unitTypeTable. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F960[unit] &= $1F, F76C[unit] &= $BF",
   "confidence": "high"
  },
  {
   "addr": "555A",
   "name": "setUnitDigBits",
   "summary": "Patches the ORA operand at $5568 with A, reads the unit argument and sets unitStunTable[unit] = (old & $1F) | A. Returns Y = unit. Hidden as data.",
   "inputs": "A = new bits 5-7",
   "outputs": "F960[unit]; Y",
   "confidence": "high"
  },
  {
   "addr": "556D",
   "name": "cmdGroupDigIn",
   "summary": "Handler of command $A1 (2 bytes: group id): every living member of the group gets unitStunTable bits 5-7 = 001 via the loop at $5573. Hidden as data.",
   "inputs": "$908F",
   "outputs": "F960 of members",
   "confidence": "high"
  },
  {
   "addr": "5571",
   "name": "cmdGroupDigOut",
   "summary": "Handler of command $A2 (2 bytes: group id): clears the dig bits of every living member (shared loop with cmdGroupDigIn, ORA operand patched at $5568). Hidden as data.",
   "inputs": "$908F",
   "outputs": "F960 of members",
   "confidence": "high"
  },
  {
   "addr": "5596",
   "name": "cmdSetFormation",
   "summary": "Handler of command $A3 (3 bytes: group id, formation): reads the arguments and jumps to $4C84, which re-arranges the destinations of all group members around the group position according to the formation table at $4C7E. Hidden as data.",
   "inputs": "$908F",
   "outputs": "group destinations",
   "confidence": "medium"
  },
  {
   "addr": "559C",
   "name": "cmdSetGameType",
   "summary": "Handler of command $A4 (2 bytes: options byte): gameTypeOptions $0BA3 = argument. Sent by overlay A ($833E) so both machines use the same game type/options. Hidden as data.",
   "inputs": "$908F",
   "outputs": "$0BA3",
   "confidence": "high"
  },
  {
   "addr": "55A5",
   "name": "cmdSetupOption",
   "summary": "Handler of command $A5 (3 bytes: kind, value), sent by overlay A's setup screens. kind 0: adds value*10 to the sender's side's 16-bit point total ($92A2/$92A4); kind 1: $92AC[other side] = value; kind >= 2: $92B4[0 own / 1 opponent] = value (the per-player setting that $34D5 compares with 4 to forbid moving the comcen). Hidden as data.",
   "inputs": "$908F, $0B9F, $76",
   "outputs": "$92A2/$92A4, $92AC, $92B4",
   "confidence": "medium"
  },
  {
   "addr": "55EC",
   "name": "queueCmdBytePending",
   "summary": "Queues the single command byte in A (1-byte commands such as $92/$97/$9E/$8F) and increments pendingOwnCmdCount ($929F).",
   "inputs": "A",
   "outputs": "ring buffer, $929F",
   "confidence": "high"
  },
  {
   "addr": "55EF",
   "name": "queueCmdByte",
   "summary": "Appends A to the 40-byte outgoing command ring ($9065, write index $908D, wraps at 40) and writes an $FF end marker after it. Does nothing during film playback ($0B9B bit 7).",
   "inputs": "A, $908D, $0B9B",
   "outputs": "$9065[], $908D",
   "confidence": "high"
  },
  {
   "addr": "560A",
   "name": "queueCmd4",
   "summary": "Queues a 4-byte command: A, cmdParam0, cmdParam1, cmdParam2 (e.g. $80 move, $81, $98, $8D) and counts it once in pendingOwnCmdCount.",
   "inputs": "A = code, $73/$74/$75",
   "outputs": "ring buffer, $929F",
   "confidence": "high"
  },
  {
   "addr": "560F",
   "name": "queueCmd3",
   "summary": "Queues a 3-byte command: A, cmdParam1, cmdParam2 (e.g. $87, $88, $90, $8E, $A3, $A5) and increments pendingOwnCmdCount.",
   "inputs": "A = code, $74/$75",
   "outputs": "ring buffer, $929F",
   "confidence": "high"
  },
  {
   "addr": "561F",
   "name": "queueCmd2",
   "summary": "Queues a 2-byte command: A, cmdParam0 (unit/group/reason commands $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2, $A4) and increments pendingOwnCmdCount.",
   "inputs": "A = code, $73",
   "outputs": "ring buffer, $929F",
   "confidence": "high"
  },
  {
   "addr": "562B",
   "name": "sendOutgoingPacket",
   "summary": "Builds the next packet for the opponent and hands it to the comm module. If the drone is under local control ($920D bit 6, not paused, droneFlags bit 7 clear) first steers it (overlay B $731E/$81FE) and queues $8D heading,col,row. Then copies whole commands from the ring (read index $908E) into the packet buffer $E027 as long as the total stays below 5 bytes; a $92 (drone arrived) is dropped while droneFlags bit 6 is set, an $8E (detonate) found while $920D bit 7 is clear flushes the whole ring (read index = write index) and sends an empty packet. Sets $E01F = packet length and calls sub_E000 with X=1 (transmit). Called after every merged exchange ($4F02) and by disconnectFromOpponent and overlay B.",
   "inputs": "$9065/$908D/$908E, $920D, $92F9, $BE",
   "outputs": "$E027.., $E01F, $908E, $562A",
   "confidence": "high"
  },
  {
   "addr": "56B1",
   "name": "pollMenuSession",
   "summary": "Jump target of pollOpponentLink while commSessionState ($0B9C) bit 7 is set (link up but no game running): if the carrier ($E03C bit 6) is gone -> disconnectFromOpponent, else handleMenuSessionPacket.",
   "inputs": "$E03C",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "56BB",
   "name": "handleMenuSessionPacket",
   "summary": "Menu-level receive: sets linkPollDelay $9163 = 10 and returns unless a packet was received ($E01D bit 7). A 2-byte [$8C,05] means the opponent disconnected -> hangUpModem; [$8C,02] shows 'OPPONENT READY FOR NEW GAME.' (rate limited by frameCounterMid $46 >= 3). Then completeExchange.",
   "inputs": "$E01D, $E01E, $E020/$E021, $46",
   "outputs": "message $1F, falls into completeExchange",
   "confidence": "high"
  },
  {
   "addr": "56F1",
   "name": "completeExchange",
   "summary": "Makes sure the current packet exchange finishes: nothing pending -> return; exchange complete (bit 6) -> consume it (sub_E000 X=2); a send already requested or in progress (bits 0/5) -> wait; otherwise sends an empty packet ($E01F=0, A = $0B9C, X=1). Waits up to $4B ticks (zp_7F) for completion, then consumes the packet; on timeout hangs up the modem.",
   "inputs": "$E01D, $0B9C",
   "outputs": "$E01F, $7F, comm module state",
   "confidence": "high"
  },
  {
   "addr": "572B",
   "name": "readyForNewGameHandshake",
   "summary": "Before a new game: completeExchange, persistent message $0B 'WAITING FOR OPPONENT...', then repeatedly sends [$8C,02] (buildReadyPacket) and waits for the exchange to complete. When the opponent's packet is also [$8C,02]: commSessionState = 2 (game session), message cleared, packet consumed and the screen redrawn ($C39F). [$8C,05] -> hangUpModem. Anything else is consumed and the READY packet resent. Called from overlay A ($795A).",
   "inputs": "$E01D/$E01E/$E020/$E021",
   "outputs": "$0B9C = 2, $92A1",
   "confidence": "high"
  },
  {
   "addr": "577C",
   "name": "buildReadyPacket",
   "summary": "Fills the outgoing packet buffer with $8C,$02 and sets $E01F = 2.",
   "inputs": "-",
   "outputs": "$E027/$E028/$E01F",
   "confidence": "high"
  },
  {
   "addr": "578A",
   "name": "recordExchangeToFilm",
   "summary": "Appends the merged command buffer of one exchange to the game film in RAM under I/O: skipped if empty, not in the battle phase ($B1 != 0) or when filmPtr has reached filmEndPtr. Record = [ (splitIndex<<4) | length ] [ timestamp, 1 or 2 bytes ] [ length command bytes ], written with writeFilmByte; finally advanceFilmPtr.",
   "inputs": "$908F, $92EB, $92EC, $BC/$BD, $92F1/$92F2",
   "outputs": "film bytes, $BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "57BE",
   "name": "writeFilmTimestamp",
   "summary": "Writes the time stamp of a film record at ($BC),Y: delta = gameClock ($91CB) - previous clock - 1 (previous value kept in the operand at $57C3); if the clock moved by 0..-14 a single byte (delta<<4 | exchangeCounter $923C) is written, otherwise two bytes: exchangeCounter then the absolute clock. Updates the previous-clock operand.",
   "inputs": "Y = offset, $91CB, $923C",
   "outputs": "1-2 film bytes, Y advanced",
   "confidence": "high"
  },
  {
   "addr": "57E7",
   "name": "advanceFilmPtr",
   "summary": "Adds Y to the 16-bit filmPtr ($BC/$BD).",
   "inputs": "Y",
   "outputs": "$BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "57F2",
   "name": "finalizeFilm",
   "summary": "Called from endGameToMainMenu: if there is room, writes the end-of-film marker ($FF when gameEndReason bit 0 is set = aborted game, $FE = game reached its own end) plus a time stamp; then saveFilmEndAndRewind, writes the header: film length (end - $D000) at $D000/1, setupByte0B94 at $D02F, gameClock at $D031, both 16-bit scores at $D00D-$D010, the checksum at $D033, and restores filmPtr to the end.",
   "inputs": "$0BA8, $BC/$BD, $91CB, $92A2-$92A5, $0B94, $0B9F",
   "outputs": "film header $D000-$D033, $92F1/$92F2",
   "confidence": "high"
  },
  {
   "addr": "585C",
   "name": "setFilmPtrToEnd",
   "summary": "filmPtr ($BC/$BD) = filmEndPtr ($92F1/$92F2).",
   "inputs": "$92F1/$92F2",
   "outputs": "$BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "5867",
   "name": "saveFilmEndAndRewind",
   "summary": "filmEndPtr = filmPtr, then filmPtr = $D000 (rewindFilmPtr).",
   "inputs": "$BC/$BD",
   "outputs": "$92F1/$92F2, $BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "5874",
   "name": "readFilmByte",
   "summary": "Reads the byte at (filmPtr),Y from the RAM hidden under the I/O area ($D000-$DFFF): disables CIA2 interrupts, sets $01 = $34, reads, restores $01 = $35 and the CIA2 ICR mask from the comm module's $E033. X preserved (self-modified at $5891). Also used by overlay A and the comm module.",
   "inputs": "Y = offset, $BC/$BD",
   "outputs": "A = byte",
   "confidence": "high"
  },
  {
   "addr": "5893",
   "name": "writeFilmByte",
   "summary": "Stores A at (filmPtr),Y in the RAM under I/O with the same interrupt/banking protocol as readFilmByte. X preserved.",
   "inputs": "A, Y, $BC/$BD",
   "outputs": "RAM under I/O",
   "confidence": "high"
  },
  {
   "addr": "58B2",
   "name": "copyPageUnderIo",
   "summary": "Copies one 256-byte page with the I/O area banked out; source and destination are the self-modified operands at $58C3/$58C4 and $58C6/$58C7 (patched by writeFilmSetupSnapshot, overlay A and the comm module). Used to snapshot the unit tables into the film header and to restore them.",
   "inputs": "patched operands",
   "outputs": "256 bytes copied",
   "confidence": "high"
  },
  {
   "addr": "58D5",
   "name": "rewindFilmPtr",
   "summary": "filmPtr ($BC/$BD) = $D000, the start of the film/saved-game buffer.",
   "inputs": "-",
   "outputs": "$BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "58DE",
   "name": "verifyFilmHeader",
   "summary": "Checks a loaded film: recomputes the header checksum (computeFilmHeaderChecksum) and compares it with $D033, and compares $D02F with setupByte0B94. C=0 and A = $D004 when valid, C=1 otherwise (caller shows 'NOT A GAME FILM!'). Called from overlay A $81F4.",
   "inputs": "film header",
   "outputs": "C, A = $D004",
   "confidence": "high"
  },
  {
   "addr": "5900",
   "name": "computeFilmHeaderChecksum",
   "summary": "Sums the 51 header bytes $D000-$D032 with carry chaining into zp_18 (rewindFilmPtr first).",
   "inputs": "film header",
   "outputs": "$18 = checksum, $BC/$BD = $D000",
   "confidence": "high"
  },
  {
   "addr": "5915",
   "name": "waitForExchangeIdle",
   "summary": "Calls pollOpponentLink until linkTimeoutCounter ($9223) is zero, i.e. until the pending packet exchange has completed. Used by overlay A after sending setup commands.",
   "inputs": "$9223",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "591E",
   "name": "compareFilmPtrToEnd",
   "summary": "Compares filmPtr with filmEndPtr: C=1 when filmPtr >= filmEndPtr (buffer full while recording, end reached while playing).",
   "inputs": "$BC/$BD, $92F1/$92F2",
   "outputs": "C",
   "confidence": "high"
  },
  {
   "addr": "5929",
   "name": "writeFilmSetupSnapshot",
   "summary": "Writes the game-start snapshot into the film header: the 20 setup bytes $92A2-$92B5 (scores, recycler settings, drone/missile counts, ...) to $D034-$D047, then the first eight unit arrays $F640-$F95F (col,row,flags,type,dest,waypoint) to $D048-$D367 with copyPageUnderIo, and the game clock to $D030. Called from overlay A ($7B80) when a game starts.",
   "inputs": "$92A2.., $F640.., $91CB",
   "outputs": "$D030-$D367",
   "confidence": "high"
  },
  {
   "addr": "5983",
   "name": "getFilmSnapshotDestAddr",
   "summary": "Returns A=$68, X=$D2: the address $D268 used as last page destination of the unit snapshot (also called by the comm module / $EE00 overlay when restoring).",
   "inputs": "-",
   "outputs": "A/X",
   "confidence": "medium"
  },
  {
   "addr": "5988",
   "name": "getFilmSnapshotSrcAddr",
   "summary": "Returns A=$60, X=$F8: the address $F860 (last page source of the unit snapshot).",
   "inputs": "-",
   "outputs": "A/X",
   "confidence": "medium"
  },
  {
   "addr": "598D",
   "name": "setFilmLimitToBufferEnd",
   "summary": "filmEndPtr = $DFF0, the recording limit of the film buffer. Called from overlay A $78D1 when recording starts.",
   "inputs": "-",
   "outputs": "$92F1/$92F2",
   "confidence": "high"
  },
  {
   "addr": "5998",
   "name": "initFilmStream",
   "summary": "Writes the command-stream start offset $0368 to $D002/$D003 and sets filmPtr = $D368 (first byte after the snapshot). Called from overlay A $7B8C.",
   "inputs": "-",
   "outputs": "$D002/$D003, $BC/$BD",
   "confidence": "high"
  },
  {
   "addr": "59B3",
   "name": "isCmdRingNearlyFull",
   "summary": "Computes the free space of the outgoing command ring ((readIdx - writeIdx) mod 40) and returns C=1 when it is below 5 bytes (the caller checkFirePressed then refuses new orders). Equal indices mean empty -> C=0.",
   "inputs": "$908D, $908E",
   "outputs": "C",
   "confidence": "high"
  },
  {
   "addr": "59D0",
   "name": "finishFilmPlayback",
   "summary": "Reached when the $FF end marker of an aborted-game film is read: rewinds, restores gameClock from $D031 and the two 16-bit scores from $D00D-$D010, increments gameEndReason (game over), restores filmPtr, queues message 2, sets $923F = $C0, clears the status line ($92A1/$0548 = $9E) and starts gameEndCountdown at $7D.",
   "inputs": "film header, $0B9F",
   "outputs": "$91CB, $92A2-$92A5, $0BA8, $923F, $92A1, $0548, $92C9",
   "confidence": "high"
  },
  {
   "addr": "5A14",
   "name": "checkLinkTimeout",
   "summary": "Link watchdog while waiting for the opponent's packet: sets bit 7 of linkTimeoutCounter ($9223, advanced every 8 frames by the IRQ). Below $E1 returns C=0; from $E1 shows 'PHONE TROUBLE.' (unless sound $15 is playing) and returns C=1; at $FF: in battle phase voicePauseReconnect and restart, in setup phases commSessionState = $FF, disconnect, 'TRY AGAIN.', prevLinkTypeFlag = $C8 and jump to returnToMainMenu.",
   "inputs": "$9223, $B1, $67C8",
   "outputs": "C, $9223, $0B9C, $0BA6",
   "confidence": "high"
  },
  {
   "addr": "5AD7",
   "name": "drawOverviewMap",
   "summary": "Draws the strategic overview (mini-map) in the 20x20 character window at screen (1,1)-(20,20): background colour 7, then for each 2x2 block of map cells builds one 8-byte character (four 2x2-pixel quadrants via buildOverviewQuadrant), adds the quarter lines, writes it to the bitmap, colour $62 into screen RAM and the terrain colour ($32) into colour RAM (+$4C00). Map pointer $48/$49 walks $F000 by 2 cells and skips every other row.",
   "inputs": "$F000 map, unit tables",
   "outputs": "bitmap/screen/colour RAM, $9004 = 7",
   "confidence": "high"
  },
  {
   "addr": "5B6C",
   "name": "drawOverviewQuarterLine",
   "summary": "Overlays the field lines on the top scanline of an overview character: at character row 11 (map row 20, midfield) the 2nd and 4th pixel, at rows 6 and 16 (map rows 10/30) only the 4th pixel are set to colour 1 unless the pixel pair is colour 2/2 ($A). Works on the 8-byte buffer $90B8.",
   "inputs": "Y = character row, $90B8",
   "outputs": "$90B8[0]",
   "confidence": "medium"
  },
  {
   "addr": "5BAD",
   "name": "buildOverviewQuadrant",
   "summary": "Builds quadrant X (0 top-left,1 top-right,2 bottom-left,3 bottom-right) of an overview character for map cell A: looks up the terrain colour (D_07EA[D_07BE[idx]] -> $32) then falls into buildOverviewQuadrantNoColour. Terrain index = cell-$20 for $20-$3F, cell-$40 for $41-$60, $40 = blank.",
   "inputs": "A = map cell, X = quadrant, Y preserved",
   "outputs": "$32, $90B8 bytes",
   "confidence": "high"
  },
  {
   "addr": "5BD4",
   "name": "buildOverviewQuadrantNoColour",
   "summary": "ORs the 4-scanline pattern for map cell A into quadrant X of $90B8 (mask from quadrantPixelMask, rows from quadrantRowOffset). Units ($80|idx): hidden unless own side (patched BCC/BCS at $5BF7/$5BFE select the side), replay view >= $82, revealAllFlag or the unit's spotted bit (F9C4 bit 7); pattern $5AB7, or $5ABF when cloaked, masked with overviewUnitMask $922B (complemented for the other side); dead units and $9242 bit 7 draw the terrain underneath (FA70). Terrain uses the pattern pointed to by overviewPatternLo/Hi.",
   "inputs": "A = cell, X = quadrant, Y = return value",
   "outputs": "$90B8, X/Y restored",
   "confidence": "high"
  },
  {
   "addr": "5C7C",
   "name": "drawMapCell",
   "summary": "Redraws one map cell everywhere it is visible: A = cell byte, X = column, Y = row (saved in $54-$56). Skipped during the knock-out sequence ($922A bit 6). On the battlefield screen: if the cell lies inside the 7x5 viewport -> drawViewportCell; then the overview pixel quadrant is rewritten (clear quadrant mask, buildOverviewQuadrantNoColour, quarter lines). On screen 1 -> drawScreen1Cell. Called from $3F89, $437D, $48AE (unit moves/redraws).",
   "inputs": "A/X/Y, $90FB, $A5/$A6",
   "outputs": "bitmap updates; A/X/Y restored",
   "confidence": "high"
  },
  {
   "addr": "5D1A",
   "name": "drawViewportCell",
   "summary": "Draws map cell ($54,$55,$56) into the 7x5 battlefield viewport: screen character = ((col-viewX)*2+25, (row-viewY)*2+1), temporarily sets viewOrigin to the cell, renderMapTile builds the 2x2-character tile in tileBuffer ($04C8) and the 32 bytes are copied to the bitmap (second character row at +$140). Also used by scrollViewBitmap ($1F1B/$1F44).",
   "inputs": "$54/$55/$56, $A5/$A6",
   "outputs": "bitmap, $90F6/$90F7 scratch",
   "confidence": "high"
  },
  {
   "addr": "5D7F",
   "name": "drawScreen1Cell",
   "summary": "Redraws map cell ($54,$55,$56) on overlay-B screen 1 (the 21x17-cell map window at character (3,3), one character per cell): a unit equal to comcenUnit is handed to overlay B $7959; otherwise, if the cell is within 21 columns/17 rows of the view origin, computes the bitmap pointer (row/col tables +3) and the colour-RAM pointer, sets $9237/$9239 = $0B and $9238 = 1 and calls overlay B $74C3 and $7396 with viewOrigin temporarily set to the cell.",
   "inputs": "$54-$56, $A5/$A6, $9236",
   "outputs": "bitmap/colour RAM via overlay B, $9237-$9239",
   "confidence": "medium"
  },
  {
   "addr": "5E03",
   "name": "clearOverviewWindow",
   "summary": "Sets the text window to left 1, top 1, width 20, bottom 21 (the overview area) and clears it with $C06A. Called at start-up and by clearCurrentScreenWindows.",
   "inputs": "-",
   "outputs": "$39-$3C, window cleared",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "18",
   "scope": "zp",
   "name": "tempByte18",
   "meaning": "scratch: film header checksum accumulator (computeFilmHeaderChecksum), character row in drawOverviewMap",
   "confidence": "high"
  },
  {
   "addr": "19",
   "scope": "zp",
   "name": "tempByte19",
   "meaning": "scratch: page counter in writeFilmSetupSnapshot, character column in drawOverviewMap",
   "confidence": "high"
  },
  {
   "addr": "32",
   "scope": "zp",
   "name": "tileColour",
   "meaning": "colour-RAM byte for the overview character being built (terrain colour from D_07EA)",
   "confidence": "high"
  },
  {
   "addr": "46",
   "scope": "zp",
   "name": "frameCounterMid",
   "meaning": "middle byte of the frame counter; used to rate-limit the 'OPPONENT READY FOR NEW GAME.' message",
   "confidence": "medium"
  },
  {
   "addr": "48",
   "scope": "zp",
   "name": "mapPtr",
   "meaning": "pointer (lo) into the $F000 map while drawing the overview",
   "confidence": "high"
  },
  {
   "addr": "49",
   "scope": "zp",
   "name": "mapPtrHi",
   "meaning": "pointer (hi) of mapPtr",
   "confidence": "high"
  },
  {
   "addr": "54",
   "scope": "zp",
   "name": "mapCellValue",
   "meaning": "map byte of the cell being redrawn by drawMapCell/drawViewportCell/drawScreen1Cell",
   "confidence": "high"
  },
  {
   "addr": "55",
   "scope": "zp",
   "name": "mapCellX",
   "meaning": "column of that cell",
   "confidence": "high"
  },
  {
   "addr": "56",
   "scope": "zp",
   "name": "mapCellY",
   "meaning": "row of that cell",
   "confidence": "high"
  },
  {
   "addr": "69",
   "scope": "zp",
   "name": "mergeFillIndex",
   "meaning": "number of bytes already placed in the merged command buffer $908F while the two packets are merged",
   "confidence": "high"
  },
  {
   "addr": "6A",
   "scope": "zp",
   "name": "packetScanIndex",
   "meaning": "read offset into the packet being merged ($E020 received / $E027 own)",
   "confidence": "high"
  },
  {
   "addr": "72",
   "scope": "zp",
   "name": "messageTimer72",
   "meaning": "message countdown; cleared when 'RESUMED' is shown",
   "confidence": "medium"
  },
  {
   "addr": "73",
   "scope": "zp",
   "name": "cmdParam0",
   "meaning": "first parameter of the current command (unit, group id, reason, side, ...)",
   "confidence": "high"
  },
  {
   "addr": "74",
   "scope": "zp",
   "name": "cmdParam1",
   "meaning": "second parameter (column / unit / kind)",
   "confidence": "high"
  },
  {
   "addr": "75",
   "scope": "zp",
   "name": "cmdParam2",
   "meaning": "third parameter (row / target / value)",
   "confidence": "high"
  },
  {
   "addr": "76",
   "scope": "zp",
   "name": "cmdFromOpponent",
   "meaning": "bit 7 set while the command being executed came from the opponent's packet (coordinates must be mirrored, 'own' side effects skipped); also $80 while the received packet is merged",
   "confidence": "high"
  },
  {
   "addr": "7F",
   "scope": "zp",
   "name": "timeoutTicks",
   "meaning": "tick countdown used as timeout while waiting for the comm module ($4B in completeExchange)",
   "confidence": "high"
  },
  {
   "addr": "A5",
   "scope": "zp",
   "name": "viewOriginCol",
   "meaning": "map column of the left edge of the battlefield viewport (temporarily set to the cell being drawn)",
   "confidence": "high"
  },
  {
   "addr": "A6",
   "scope": "zp",
   "name": "viewOriginRow",
   "meaning": "map row of the top edge of the viewport",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "0 while the battle runs (film recording, comcen repair meaning of $9E); nonzero in side-selection/setup phases; cleared bit 7 by the map-seed command, set to $DF by cmdChooseSide",
   "confidence": "high"
  },
  {
   "addr": "B4",
   "scope": "zp",
   "name": "bitmapPtr",
   "meaning": "pointer (lo) to the character cell in the $A000 bitmap",
   "confidence": "high"
  },
  {
   "addr": "B5",
   "scope": "zp",
   "name": "bitmapPtrHi",
   "meaning": "pointer (hi) of bitmapPtr",
   "confidence": "high"
  },
  {
   "addr": "B6",
   "scope": "zp",
   "name": "screenPtr",
   "meaning": "pointer (lo) to the cell in screen RAM $8C00 (+$4C00 = colour RAM)",
   "confidence": "high"
  },
  {
   "addr": "B7",
   "scope": "zp",
   "name": "screenPtrHi",
   "meaning": "pointer (hi) of screenPtr",
   "confidence": "high"
  },
  {
   "addr": "BC",
   "scope": "zp",
   "name": "filmPtr",
   "meaning": "pointer (lo) into the game-film buffer in the RAM under the I/O area ($D000-$DFEF); read/written through readFilmByte/writeFilmByte",
   "confidence": "high"
  },
  {
   "addr": "BD",
   "scope": "zp",
   "name": "filmPtrHi",
   "meaning": "pointer (hi) of filmPtr",
   "confidence": "high"
  },
  {
   "addr": "BE",
   "scope": "zp",
   "name": "pauseState",
   "meaning": "pause/ready bits: 3 after a timeout/pause (both players must resume), bit 0/1 cleared by the own/opponent's resume or setup-ready command, $80 = disconnect pending; 0 = running",
   "confidence": "medium"
  },
  {
   "addr": "39",
   "scope": "zp",
   "name": "textWindowTop",
   "meaning": "text window top row (set to 1 by clearOverviewWindow)",
   "confidence": "high"
  },
  {
   "addr": "3A",
   "scope": "zp",
   "name": "textWindowBottom",
   "meaning": "text window bottom limit (21)",
   "confidence": "high"
  },
  {
   "addr": "3B",
   "scope": "zp",
   "name": "textWindowLeft",
   "meaning": "text window left column (1)",
   "confidence": "high"
  },
  {
   "addr": "3C",
   "scope": "zp",
   "name": "textWindowWidth",
   "meaning": "text window width (20)",
   "confidence": "high"
  },
  {
   "addr": "04C8",
   "scope": "abs",
   "name": "tileBuffer",
   "meaning": "32-byte 2x2-character tile built by renderMapTile and copied to the bitmap by drawViewportCell",
   "confidence": "high"
  },
  {
   "addr": "0548",
   "scope": "abs",
   "name": "chatLineMarker",
   "meaning": "$9E written at film end to blank the status/chat line",
   "confidence": "medium"
  },
  {
   "addr": "07BE",
   "scope": "abs",
   "name": "terrainColourIndex",
   "meaning": "33-entry table indexed by terrain index (cell-$20 or cell-$40) giving an index into D_07EA",
   "confidence": "medium"
  },
  {
   "addr": "07EA",
   "scope": "abs",
   "name": "terrainColourTable",
   "meaning": "colour-RAM values for terrain classes (also used by renderMapTile)",
   "confidence": "medium"
  },
  {
   "addr": "0B7D",
   "scope": "abs",
   "name": "uiDelayTimer",
   "meaning": "input lockout frames: $3C after a pause/timeout command, $20 after a setup-ready command",
   "confidence": "medium"
  },
  {
   "addr": "0B94",
   "scope": "abs",
   "name": "setupByte0B94",
   "meaning": "settings byte written to film header offset $2F and verified on film load (game/scenario version)",
   "confidence": "medium"
  },
  {
   "addr": "0B95",
   "scope": "abs",
   "name": "mapSeed",
   "meaning": "3-byte map seed $0B95-$0B97 set by commands $8B/$A6",
   "confidence": "high"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "filmPlaybackFlag",
   "meaning": "bit 7 = film playback / solo observer mode: commands are read from the film instead of the link and nothing is queued; bits 0-1 = replay viewpoint ($82 = see everything)",
   "confidence": "high"
  },
  {
   "addr": "0B9C",
   "scope": "abs",
   "name": "commSessionState",
   "meaning": "0 = no session, 2 = game session agreed (READY handshake), $80 = link up at menu level (pollMenuSession), $FF = link lost",
   "confidence": "high"
  },
  {
   "addr": "0B9D",
   "scope": "abs",
   "name": "mapKindFlag",
   "meaning": "0 after command $8B, 1 after command $A6 (two kinds of map seed, probably standard vs custom/edited map)",
   "confidence": "low"
  },
  {
   "addr": "0B9F",
   "scope": "abs",
   "name": "playerSide",
   "meaning": "local player's side (0/1); EOR 1 = opponent; decided by cmdChooseSide (bit 7 = conflict, random)",
   "confidence": "high"
  },
  {
   "addr": "0BA3",
   "scope": "abs",
   "name": "gameTypeOptions",
   "meaning": "game type/options byte, set on both machines by command $A4",
   "confidence": "high"
  },
  {
   "addr": "0BA5",
   "scope": "abs",
   "name": "commBuildFlag",
   "meaning": "bit 7 = comm build 1: pause bits cleared at once, side forced to 0 by cmdChooseSide",
   "confidence": "medium"
  },
  {
   "addr": "0BA6",
   "scope": "abs",
   "name": "prevLinkTypeFlag",
   "meaning": "set to $C8 when the link is given up after a timeout",
   "confidence": "low"
  },
  {
   "addr": "0BA8",
   "scope": "abs",
   "name": "gameEndReason",
   "meaning": "0 while a game runs; 1 = aborted/no game, $08 time up, $A0/$C0 knockout; bit 0 selects the film end marker ($FF vs $FE), bit 7 blocks cmdAddEnergy",
   "confidence": "high"
  },
  {
   "addr": "0C8A",
   "scope": "abs",
   "name": "lastUnitOfSide",
   "meaning": "$31,$63: comcen index of side 0/1 (used by the $9E comcen-repair handler)",
   "confidence": "high"
  },
  {
   "addr": "562A",
   "scope": "abs",
   "name": "outPacketFill",
   "meaning": "work byte: number of bytes already placed in the outgoing packet by sendOutgoingPacket",
   "confidence": "high"
  },
  {
   "addr": "67C8",
   "scope": "abs",
   "name": "voiceSoundIds",
   "meaning": "3 bytes: sound id currently playing on each SID voice ($67C8 voice 0); $15 is checked before showing PHONE TROUBLE",
   "confidence": "medium"
  },
  {
   "addr": "9004",
   "scope": "abs",
   "name": "bgColorShadow",
   "meaning": "shadow of $D021, set to 7 by drawOverviewMap",
   "confidence": "high"
  },
  {
   "addr": "9008",
   "scope": "abs",
   "name": "spriteColorShadow",
   "meaning": "sprite 0 colour = 1 for the drone marker",
   "confidence": "high"
  },
  {
   "addr": "9010",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "sprite 0 X = drone screen position",
   "confidence": "high"
  },
  {
   "addr": "9018",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "sprite 0 Y = drone screen position",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "shadow of $D015; bit 0 cleared by cmdDroneMove",
   "confidence": "high"
  },
  {
   "addr": "9065",
   "scope": "abs",
   "name": "outgoingCmdRing",
   "meaning": "40-byte ring of outgoing command bytes ($9065-$908C), $FF after the last byte",
   "confidence": "high"
  },
  {
   "addr": "908D",
   "scope": "abs",
   "name": "outgoingCmdWriteIdx",
   "meaning": "write index into outgoingCmdRing (0-39)",
   "confidence": "high"
  },
  {
   "addr": "908E",
   "scope": "abs",
   "name": "outgoingCmdReadIdx",
   "meaning": "read index into outgoingCmdRing (next byte to send)",
   "confidence": "high"
  },
  {
   "addr": "908F",
   "scope": "abs",
   "name": "mergedCmdBuffer",
   "meaning": "merged command buffer of one exchange ($908F-$90B6, 40 bytes): side 0's packet followed by side 1's; also the film record payload",
   "confidence": "high"
  },
  {
   "addr": "90B7",
   "scope": "abs",
   "name": "mergedCmdReadIdx",
   "meaning": "read index into mergedCmdBuffer while the dispatcher executes commands; handlers advance it past their arguments",
   "confidence": "high"
  },
  {
   "addr": "90B8",
   "scope": "abs",
   "name": "overviewCharBuffer",
   "meaning": "8-byte character bitmap being assembled for the overview map",
   "confidence": "high"
  },
  {
   "addr": "90EA",
   "scope": "abs",
   "name": "newKeyEvent",
   "meaning": "key code $20 injected by the setup-ready command",
   "confidence": "medium"
  },
  {
   "addr": "90F6",
   "scope": "abs",
   "name": "viewOriginColSave",
   "meaning": "saved viewOriginCol while a single cell is drawn",
   "confidence": "high"
  },
  {
   "addr": "90F7",
   "scope": "abs",
   "name": "viewOriginRowSave",
   "meaning": "saved viewOriginRow while a single cell is drawn",
   "confidence": "high"
  },
  {
   "addr": "90F8",
   "scope": "abs",
   "name": "selectedUnit",
   "meaning": "selected unit; during side selection it holds the locally chosen side",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "0 battlefield (viewport + overview updated), 1 overlay-B map screen (drawScreen1Cell, drone sprite)",
   "confidence": "high"
  },
  {
   "addr": "9163",
   "scope": "abs",
   "name": "linkPollDelay",
   "meaning": "countdown (4-frame units) set to 10 after each processed packet; pollOpponentLink is idle while nonzero (not in film mode)",
   "confidence": "high"
  },
  {
   "addr": "91CB",
   "scope": "abs",
   "name": "gameClock",
   "meaning": "game time counter (counts down to 0 = time up); stored in film time stamps and the film header",
   "confidence": "high"
  },
  {
   "addr": "91CC",
   "scope": "abs",
   "name": "cursorSpriteIndex",
   "meaning": "set to 0 by cmdDroneMove so sprite 0 is positioned",
   "confidence": "medium"
  },
  {
   "addr": "91DC",
   "scope": "abs",
   "name": "boomerTargetTable",
   "meaning": "per-boomer target lock, cleared by cmdClearTarget",
   "confidence": "high"
  },
  {
   "addr": "91F4",
   "scope": "abs",
   "name": "groupStatusTable",
   "meaning": "16 bytes per group id: bit 7 = group cloaked, bit 6 = group blitz (set/cleared by the group cloak/blitz commands)",
   "confidence": "medium"
  },
  {
   "addr": "920B",
   "scope": "abs",
   "name": "droneHeading",
   "meaning": "heading 0-7 of the remotely controlled drone, sent as first byte of command $8D",
   "confidence": "medium"
  },
  {
   "addr": "920D",
   "scope": "abs",
   "name": "droneStateFlags",
   "meaning": "drone control flags: bit 6 = drone under local control (moves are sent every exchange), bit 7 gates $8E, bit 5 cleared by own detonation, bit 0 set when a drone move is executed; bits 0-5 cleared when the opponent's drone finishes",
   "confidence": "low"
  },
  {
   "addr": "9223",
   "scope": "abs",
   "name": "linkTimeoutCounter",
   "meaning": "0 after a packet arrived; checkLinkTimeout sets bit 7 and the IRQ then increments it every 8 frames ($E1 = PHONE TROUBLE, $FF = give up/reconnect)",
   "confidence": "high"
  },
  {
   "addr": "922A",
   "scope": "abs",
   "name": "linkState",
   "meaning": "bit 6 = knock-out sequence running: drawMapCell does nothing",
   "confidence": "medium"
  },
  {
   "addr": "922B",
   "scope": "abs",
   "name": "overviewUnitMask",
   "meaning": "pixel mask (AND) applied to the unit pattern on the overview for side 0 units; complemented for side 1 (set by overlay B $7909)",
   "confidence": "medium"
  },
  {
   "addr": "9236",
   "scope": "abs",
   "name": "comcenUnit",
   "meaning": "index of the local player's comcen (49/99)",
   "confidence": "high"
  },
  {
   "addr": "9237",
   "scope": "abs",
   "name": "screen1DrawParam0",
   "meaning": "set to $0B by drawScreen1Cell before calling overlay B's cell drawer (probably a colour)",
   "confidence": "low"
  },
  {
   "addr": "9238",
   "scope": "abs",
   "name": "screen1DrawParam1",
   "meaning": "set to 1 by drawScreen1Cell",
   "confidence": "low"
  },
  {
   "addr": "9239",
   "scope": "abs",
   "name": "screen1DrawParam2",
   "meaning": "set to $0B by drawScreen1Cell",
   "confidence": "low"
  },
  {
   "addr": "923A",
   "scope": "abs",
   "name": "pendingRedrawGroup",
   "meaning": "group id whose members must be redrawn by the tick loop ($418D); $FF = none; set by own group-cloak commands",
   "confidence": "medium"
  },
  {
   "addr": "923B",
   "scope": "abs",
   "name": "pendingRedrawUnit",
   "meaning": "unit to redraw in the tick loop ($417B); $FF = none; set by own cloak on/off commands (and by recycleDeadUnits)",
   "confidence": "medium"
  },
  {
   "addr": "923C",
   "scope": "abs",
   "name": "exchangeCounter",
   "meaning": "tick/10 quotient at the last command exchange; its low nibble is stored in film time stamps and must match on playback",
   "confidence": "high"
  },
  {
   "addr": "923F",
   "scope": "abs",
   "name": "gameOverFlags",
   "meaning": "$C0 when the game ends (abort command, film end, computeGameResult)",
   "confidence": "medium"
  },
  {
   "addr": "9242",
   "scope": "abs",
   "name": "unitDrawOverride",
   "meaning": "bit 7 set: overview draws the terrain under units instead of unit blobs",
   "confidence": "medium"
  },
  {
   "addr": "9245",
   "scope": "abs",
   "name": "unitInspectLockFlags",
   "meaning": "bit 7 = own comcen cloaked (unit inspection limited)",
   "confidence": "medium"
  },
  {
   "addr": "929C",
   "scope": "abs",
   "name": "chatState",
   "meaning": "reset to 0 by pause/resume/ready commands",
   "confidence": "medium"
  },
  {
   "addr": "929D",
   "scope": "abs",
   "name": "pendingGroupOrderId",
   "meaning": "group id (FB54 & $3C) of the group currently being given MOVE TO GOAL orders",
   "confidence": "low"
  },
  {
   "addr": "929E",
   "scope": "abs",
   "name": "pendingGroupOrderCount",
   "meaning": "number of outstanding $98 orders for pendingGroupOrderId; decremented when each is executed",
   "confidence": "low"
  },
  {
   "addr": "929F",
   "scope": "abs",
   "name": "pendingOwnCmdCount",
   "meaning": "number of own commands queued but not yet executed; incremented by the queue routines, decremented when an own command is executed",
   "confidence": "high"
  },
  {
   "addr": "92A1",
   "scope": "abs",
   "name": "persistentMessageId",
   "meaning": "status message kept on screen: $0B waiting for opponent, $19 timeout, $9E none",
   "confidence": "high"
  },
  {
   "addr": "92A2",
   "scope": "abs",
   "name": "playerPointsLo",
   "meaning": "per-side 16-bit score low bytes (film header $D00D-$D010, command $A5 kind 0)",
   "confidence": "high"
  },
  {
   "addr": "92A4",
   "scope": "abs",
   "name": "playerPointsHi",
   "meaning": "per-side score high bytes",
   "confidence": "high"
  },
  {
   "addr": "92A6",
   "scope": "abs",
   "name": "recyclerMode",
   "meaning": "per-side recycler setting set by command $9C",
   "confidence": "high"
  },
  {
   "addr": "92A8",
   "scope": "abs",
   "name": "recyclerCol",
   "meaning": "per-side recycler column",
   "confidence": "high"
  },
  {
   "addr": "92AA",
   "scope": "abs",
   "name": "recyclerRow",
   "meaning": "per-side recycler row",
   "confidence": "high"
  },
  {
   "addr": "92AC",
   "scope": "abs",
   "name": "setupValue92AC",
   "meaning": "per-side setup value written by command $A5 kind 1 into the other side's entry",
   "confidence": "low"
  },
  {
   "addr": "92AE",
   "scope": "abs",
   "name": "dronesLeft",
   "meaning": "2 bytes [0 own, 1 opponent]: drones remaining (set by $90 during setup, decremented by $97 drone launch)",
   "confidence": "medium"
  },
  {
   "addr": "92B0",
   "scope": "abs",
   "name": "missilesLeft",
   "meaning": "2 bytes [0 own, 1 opponent]: missiles remaining (set by $90 during setup, decremented by a $90 strike)",
   "confidence": "medium"
  },
  {
   "addr": "92B4",
   "scope": "abs",
   "name": "comcenMobility",
   "meaning": "2 bytes [0 own, 1 opponent] set by command $A5 kind >= 2; value 4 makes $34D5 refuse to move the comcen",
   "confidence": "low"
  },
  {
   "addr": "92C2",
   "scope": "abs",
   "name": "exchangeActivityFlag",
   "meaning": "bit 0 set whenever a command is executed in an exchange; in the setup phase the game clock only advances on exchanges with activity",
   "confidence": "medium"
  },
  {
   "addr": "92C9",
   "scope": "abs",
   "name": "gameEndCountdown",
   "meaning": "positive value counts up every 8 frames to $80 and then ends the game/returns to the menu: $62/$71 after an abort command, $7D after film end",
   "confidence": "high"
  },
  {
   "addr": "92EB",
   "scope": "abs",
   "name": "mergedCmdLength",
   "meaning": "total length of the merged command buffer of the current exchange (film record length nibble)",
   "confidence": "high"
  },
  {
   "addr": "92EC",
   "scope": "abs",
   "name": "mergedCmdSplitIdx",
   "meaning": "offset in mergedCmdBuffer where side 1's commands begin (film record high nibble)",
   "confidence": "high"
  },
  {
   "addr": "92ED",
   "scope": "abs",
   "name": "mergedCmdOwnerFlag",
   "meaning": "$80 while the commands being executed belong to side 1; toggled at the split index; copied to cmdFromOpponent after XOR with the local side",
   "confidence": "high"
  },
  {
   "addr": "92EE",
   "scope": "abs",
   "name": "cmdLength",
   "meaning": "length (1-4 bytes incl. the command byte) of the current command code from cmdLengthTable ($4D78), via $4D9F",
   "confidence": "high"
  },
  {
   "addr": "92F1",
   "scope": "abs",
   "name": "filmEndPtr",
   "meaning": "end pointer of the film: $DFF0 recording limit, actual end after finalizeFilm / on playback",
   "confidence": "high"
  },
  {
   "addr": "92F2",
   "scope": "abs",
   "name": "filmEndPtrHi",
   "meaning": "high byte of filmEndPtr",
   "confidence": "high"
  },
  {
   "addr": "92F6",
   "scope": "abs",
   "name": "droneCol",
   "meaning": "current map column of the drone (command $8D)",
   "confidence": "medium"
  },
  {
   "addr": "92F7",
   "scope": "abs",
   "name": "droneRow",
   "meaning": "current map row of the drone",
   "confidence": "medium"
  },
  {
   "addr": "92F9",
   "scope": "abs",
   "name": "droneFlags",
   "meaning": "0 at launch ($97); $80 = drone arrived/finished (seen $92 in either packet), $40 = detonated ($8E), $10 = own arrival executed; bits 4-5 block further drone moves",
   "confidence": "medium"
  },
  {
   "addr": "92FC",
   "scope": "abs",
   "name": "revealAllFlag",
   "meaning": "bit 7: overview shows enemy units even when not spotted",
   "confidence": "high"
  },
  {
   "addr": "D000",
   "scope": "abs",
   "name": "filmBuffer",
   "meaning": "game film / saved game in the RAM under I/O: $D000/1 film length, $D002/3 stream start offset ($0368), $D004 id byte, $D00D-$D010 final scores, $D02F setupByte0B94, $D030/$D031 game clock at start/end, $D033 header checksum, $D034-$D047 copy of $92A2-$92B5, $D048-$D367 copy of $F640-$F95F, $D368-$DFEF command records",
   "confidence": "high"
  },
  {
   "addr": "E01D",
   "scope": "abs",
   "name": "commExchangeState",
   "meaning": "comm module exchange state: bit 0 = send requested, bit 5 = sending, bit 6 = exchange complete (own packet sent and opponent's received), bit 7 = packet received",
   "confidence": "medium"
  },
  {
   "addr": "E01E",
   "scope": "abs",
   "name": "commRxLength",
   "meaning": "length of the received packet in $E020..",
   "confidence": "high"
  },
  {
   "addr": "E01F",
   "scope": "abs",
   "name": "commTxLength",
   "meaning": "length of the outgoing packet in $E027..",
   "confidence": "high"
  },
  {
   "addr": "E020",
   "scope": "abs",
   "name": "commRxBuffer",
   "meaning": "received packet (up to 7 bytes), merged into mergedCmdBuffer; $8C and $8D are executed immediately",
   "confidence": "high"
  },
  {
   "addr": "E027",
   "scope": "abs",
   "name": "commTxBuffer",
   "meaning": "outgoing packet (up to 7 bytes) filled by sendOutgoingPacket / buildReadyPacket",
   "confidence": "high"
  },
  {
   "addr": "E033",
   "scope": "abs",
   "name": "commCia2IcrMask",
   "meaning": "CIA2 ICR enable mask used by the comm module, restored after every RAM-under-I/O access",
   "confidence": "high"
  },
  {
   "addr": "E03B",
   "scope": "abs",
   "name": "commLinkActive",
   "meaning": "nonzero while the link is open",
   "confidence": "high"
  },
  {
   "addr": "E03C",
   "scope": "abs",
   "name": "commLineStatus",
   "meaning": "bit 6 = carrier present; lost carrier at menu level disconnects",
   "confidence": "high"
  },
  {
   "addr": "F640",
   "scope": "abs",
   "name": "unitCol",
   "meaning": "per-unit map column (100 entries)",
   "confidence": "high"
  },
  {
   "addr": "F6A4",
   "scope": "abs",
   "name": "unitRow",
   "meaning": "per-unit map row",
   "confidence": "high"
  },
  {
   "addr": "F708",
   "scope": "abs",
   "name": "unitFlagsTable",
   "meaning": "per-unit flags: bits 2-4 group, bit 5 side, bit 6 group membership active (& $7C compared with group|$40)",
   "confidence": "high"
  },
  {
   "addr": "F76C",
   "scope": "abs",
   "name": "unitTypeTable",
   "meaning": "per-unit type (0-2 = type; 3 = spy cannot cloak/blitz), bit 6 dug in, bit 7 dead, $FF free slot",
   "confidence": "high"
  },
  {
   "addr": "F7D0",
   "scope": "abs",
   "name": "unitDestCol",
   "meaning": "per-unit destination column, bit 7 = idle",
   "confidence": "high"
  },
  {
   "addr": "F834",
   "scope": "abs",
   "name": "unitDestRow",
   "meaning": "per-unit destination row",
   "confidence": "high"
  },
  {
   "addr": "F898",
   "scope": "abs",
   "name": "unitWaypointCol",
   "meaning": "per-unit waypoint column; bits 6-7 = cloak ordered/active",
   "confidence": "high"
  },
  {
   "addr": "F8FC",
   "scope": "abs",
   "name": "unitWaypointRow",
   "meaning": "per-unit waypoint row; bits 6-7 = blitz ordered/active",
   "confidence": "high"
  },
  {
   "addr": "F960",
   "scope": "abs",
   "name": "unitStunTable",
   "meaning": "per-unit: bits 0-3 stun counter (comcen repaired by $9E), bits 5-7 dig-in state ($20 digging, 0 cleared)",
   "confidence": "high"
  },
  {
   "addr": "F9C4",
   "scope": "abs",
   "name": "unitEnergyTable",
   "meaning": "per-unit energy bits 0-5 (max 50), bit 7 spotted",
   "confidence": "high"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitDisplayFlags",
   "meaning": "per-unit display flags: bits 2-6 copy of unitFlagsTable, bit 7 idle",
   "confidence": "high"
  },
  {
   "addr": "FA70",
   "scope": "abs",
   "name": "unitUnderCellAlias",
   "meaning": "= unitTerrainUnder ($FAF0) indexed with the raw cell byte $80|unit",
   "confidence": "high"
  },
  {
   "addr": "F6EC",
   "scope": "abs",
   "name": "unitTypeAlias",
   "meaning": "= unitTypeTable indexed with $80|unit",
   "confidence": "high"
  },
  {
   "addr": "F818",
   "scope": "abs",
   "name": "unitWaypointColAlias",
   "meaning": "= unitWaypointCol indexed with $80|unit",
   "confidence": "high"
  },
  {
   "addr": "F944",
   "scope": "abs",
   "name": "unitEnergyAlias",
   "meaning": "= unitEnergyTable indexed with $80|unit",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "562A",
   "length": 1,
   "type": "byteTable",
   "name": "outPacketFill",
   "description": "work variable of sendOutgoingPacket (bytes placed in the outgoing packet)"
  },
  {
   "addr": "5A59",
   "length": 33,
   "type": "byteTable",
   "name": "overviewPatternLo",
   "description": "low bytes of the 4-byte quadrant pattern for terrain indices 0-32 (patterns at $5A9B-$5AD6)"
  },
  {
   "addr": "5A7A",
   "length": 33,
   "type": "byteTable",
   "name": "overviewPatternHi",
   "description": "high bytes (all $5A) of the pattern pointers; the listing merges this with the patterns that follow"
  },
  {
   "addr": "5A9B",
   "length": 60,
   "type": "byteTable",
   "name": "overviewPatterns",
   "description": "15 patterns of 4 scanline bytes (multicolour, both horizontal halves present, the quadrant mask selects one): $5A9B blank, $5AB7 unit blob, $5ABF cloaked unit, others terrain textures"
  },
  {
   "addr": "5B64",
   "length": 4,
   "type": "byteTable",
   "name": "quadrantPixelMask",
   "description": "AND mask for quadrant 0-3 of an overview character: $F0 left half, $0F right half"
  },
  {
   "addr": "5B68",
   "length": 4,
   "type": "byteTable",
   "name": "quadrantRowOffset",
   "description": "first scanline of quadrant 0-3: 0,0,4,4 (4 rows each)"
  }
 ],
 "misclassified": [
  {
   "addr": "4FC4",
   "length": 32,
   "actual": "code",
   "evidence": "LDY $90B7/INY/LDA $908F,Y/STA $73/INC $90B7/... /STY $90B7/RTS: argument fetchers readCmdArgs3 ($4FC4) and readCmdArgs2 ($4FD0), called by the handlers at $5011, $501A, $51AB, $5470, $54E1, $51F3 and by JSR $4FD0 at $516B, $5185, $5336, $53C0, $5479, $5596, $55A5"
  },
  {
   "addr": "4FF7",
   "length": 14,
   "actual": "code",
   "evidence": "LDY $90B7/INY/LDA $908F,Y/STA $73/INY/STY $90B7/RTS = readCmdArg1, called from ~20 handlers"
  },
  {
   "addr": "5011",
   "length": 557,
   "actual": "code",
   "evidence": "$5011-$523D: the command handlers pointed to by cmdHandlerTable $4D2A (entries 0-12, 30, 38); every block starts with JSR $4FC4/$4FD0/$4FF7 and ends with RTS or a JMP into known code ($34C6, $4A52, $3C50, $4AD4, $4C6B, $3EE2); also the self-modifying group routine $50BE-$5150"
  },
  {
   "addr": "5336",
   "length": 517,
   "actual": "code",
   "evidence": "$5336-$553A: handlers for table entries 14-19, 23-29 ($5336, $537B, $538B, $53A3, $53C0, $542E, $5470, $5479, $549E, $54E1, $5511), plain 6502 with JSRs to $4FD0/$4FF7/$403B/$25FA/$3B22/$33C8/$C89A/$820F/$72A1 and overlay entry $FD5D"
  },
  {
   "addr": "5548",
   "length": 164,
   "actual": "code",
   "evidence": "$5548-$55EB: handlers for table entries 31-37 ($5548, $554C, $556D, $5571, $5596, $559C, $55A5) incl. the shared setUnitDigBits at $555A; JSR $FCCD (multiply) in the $A5 handler"
  }
 ],
 "insights": [
  "Lock-step networking: every 10 game ticks ($41DD-$41EE: $9162 mod 10, or mod 50 in setup) $923C = tick/10 and sub_4DB7 runs. It waits for the comm module's $E01D bit 6 (own packet sent AND opponent's packet received), merges the two packets into mergedCmdBuffer $908F - always side 0's commands first, then side 1's ($92EC = split index, $92EB = total) - executes them through the dispatcher sub_4E91 and then records the merged buffer into the film, acknowledges the packet (sub_E000 X=2) and sends the next packet from the outgoing ring (sendOutgoingPacket). Because both machines execute identical command streams in identical order, only player commands are ever transmitted; the simulation itself is deterministic (shared RNG seed).",
  "Command format: 1 command byte with bit 7 set ($80-$A6) followed by 0-3 parameter bytes; cmdLengthTable $4D78 (indexed by code & $3F) gives the total length, cmdHandlerTable $4D2A/$4D2B (lo/hi pairs) the handler. Bytes without bit 7 are skipped by their length (handler $5336 rewrites a suppressed $8E as $0E so the film keeps a no-op). zp_76 bit 7 tells a handler the command came from the opponent; coordinates are then mirrored with 39-x/39-y (mirrorCmdCoordsIfRemote) because each player sees his own side at the bottom.",
  "Command codes (handler, length): $80 move unit,x,y ($5011,4); $81 move group relative ($501A,4); $82 cloak on unit ($5023,2); $83 group cloak on ($5098,2); $84 cloak off ($504B,2); $85 group cloak off ($50D2,2); $86 clear boomer target ($515D,2); $87 set target boomer,target ($516B,3); $88 join group unit,group ($5185,3); $89 leave group ($519B,2); $8A place unit,x,y ($51AB,4); $8B map seed x3 ($51EE,4); $8C session control reason ($523E,2: 0 abort/left game, 2 ready for new game, 3 pause/timeout, 4 resume, 5 disconnect); $8D drone move heading,x,y ($52C8,4, executed at merge time); $8E drone detonate x,y ($5336,3); $8F drone released ($537B,1); $90 missile strike x,y / setup counts ($53C0,3); $91 self destruct unit|$80=group ($542E,2); $92 drone arrived ($538B,1); $93 blitz on ($5070,2); $94 group blitz on ($50AA,2); $95 blitz off ($5062,2); $96 group blitz off ($50BE,2); $97 drone launch ($53A3,1); $98 move keeping group ($5470,4); $99 unused; $9A add energy unit,amount ($5479,3); $9B halt group ($549E,2); $9C set recycler mode,x,y ($54E1,4); $9D choose side ($5511,2); $9E comcen repaired / setup ready ($520C,1); $9F dig in ($5548,2); $A0 dig out ($554C,2); $A1 group dig in ($556D,2); $A2 group dig out ($5571,2); $A3 formation group,kind ($5596,3); $A4 game type ($559C,2); $A5 setup option kind,value ($55A5,3); $A6 custom map seed x3 ($51EA,4).",
  "Outgoing side: queueCmdByte appends to the 40-byte ring $9065 (write $908D, read $908E); queueCmd2/3/4 and queueCmdBytePending add the parameters from $73-$75 and count the command in pendingOwnCmdCount $929F. sendOutgoingPacket moves at most 4 bytes (whole commands only) per exchange into $E027 and sets $E01F, so a packet never exceeds 4 bytes; isCmdRingNearlyFull makes checkFirePressed refuse new orders when less than 5 bytes are free. Nothing is queued during film playback ($0B9B bit 7).",
  "Game film: recorded in the RAM hidden under the I/O area ($D000-$DFEF, accessed with $01=$34 and CIA2 interrupts masked: readFilmByte/writeFilmByte/copyPageUnderIo). Header $D000-$D367 (length, stream offset $0368, final scores, setupByte0B94, clock, checksum of $D000-$D032 at $D033, copy of $92A2-$92B5, copy of unit arrays $F640-$F95F), then records: [split<<4 | len] [time stamp: (clockDelta<<4 | exchangeCounter) or exchangeCounter, absoluteClock] [len command bytes]; end marker $FE (game ended by itself) or $FF (aborted game; playback then restores the final scores via finishFilmPlayback). Playback in pollOpponentLink waits until $923C and $91CB match the time stamp, copies the payload into $908F and runs the same dispatcher.",
  "pollOpponentLink (sub_4DA9, $4DA9-$4E90, just before this chunk) is the idle hook called from menus and wait loops (11 call sites): housekeeping $C4EF, then film playback, or menu-level session polling (pollMenuSession when $0B9C bit 7), or in-game waiting for the opponent's packet with checkLinkTimeout (PHONE TROUBLE after ~13 s, reconnect/give up after ~17 s). waitForExchangeIdle loops on it until linkTimeoutCounter is 0.",
  "Session handshake: readyForNewGameHandshake exchanges [$8C,02] packets until both sides sent it ($0B9C = 2); at menu level a received [$8C,02] prints 'OPPONENT READY FOR NEW GAME.' and [$8C,05] hangs up. Pause/resume uses $8C reason 3/4 with pauseState zp_BE bits (both players must resume unless comm build 1); during the setup phase the 1-byte $9E plays the same 'ready' role (F1 when done), while in battle $9E un-stuns the sender's comcen.",
  "Strategic overview (mini-map): 20x20 characters at screen (1,1), 2x2 map cells per character, each cell a 2x2 multicolour-pixel quadrant ($90B8 work buffer, masks $5B64/$5B68, patterns $5A9B via $5A59/$5A7A). Units are 'blobs' ($5AB7, $5ABF cloaked) coloured through the mask $922B (complemented for the other side); enemy units only when spotted (F9C4 bit 7), revealAllFlag or replay view $82. Dotted quarter lines at character rows 6/11/16 (map rows 10/20/30). drawMapCell (A=cell,X=col,Y=row) is the single entry used by the engine to refresh a cell in the viewport (drawViewportCell, 2x2 characters at column 25 row 1), the overview and overlay-B screen 1 (drawScreen1Cell, 21x17 cells at character (3,3)).",
  "Side-dependent branch patching also hits this chunk: $5BF7 (patched by overlay A $7BE1) and $5BFE (patched by sub_2A08 at $2A16) are BCC/BCS opcodes selecting whether unit index < 50 means 'own unit' in the overview drawer.",
  "The given line range 8824-9954 does not correspond to $4FC4-$5E13; the chunk by address is lines 9188-10643 of main_0800.s and that is what this survey covers. $5E14-$5E16 (3 data bytes) belong to the next chunk."
 ]
}