{
 "chunk": "game_main_new_4FC4",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "4FC4",
   "name": "readCmdArgs3",
   "summary": "Argument reader for the 4-byte commands ($80,$81,$8A,$8B,$98,$9C,$A6): reads mergedCmdBuffer[idx+1] into cmdParam0 ($73), bumps mergedCmdReadIdx by 1 and falls through into readCmdArgs2, which picks up the remaining two bytes and leaves the index on the next command. Net advance is 4, matching commandLengthTable.",
   "inputs": "D_90B7 = offset of the command byte in D_908F; D_908F merged command buffer",
   "outputs": "zp_73/zp_74/zp_75 = the three parameters; D_90B7 += 4; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "4FD0",
   "name": "readCmdArgs2",
   "summary": "Argument reader for the 3-byte commands ($87,$88,$8E,$90,$9A,$A3,$A5) and the fall-through tail of readCmdArgs3: reads the two bytes after the command byte into cmdParam1/cmdParam2 and advances mergedCmdReadIdx by 3 (by 2 more when entered from $4FC4).",
   "inputs": "D_90B7, D_908F",
   "outputs": "zp_74, zp_75; D_90B7 += 3; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "4FE4",
   "name": "mirrorCmdCoordsIfRemote",
   "summary": "Point mirror for map coordinates carried by a command: when the command currently being executed came from the opponent (cmdFromOpponent zp_76 bit 7 set) both cmdParam1 and cmdParam2 are put through mirrorMapCoordinate ($3FA3, 39-x). Both machines run one shared simulation while each player sees his own side at the bottom of the map, so every coordinate received over the link has to be rotated 180 degrees.",
   "inputs": "zp_76 bit 7, zp_74 = column, zp_75 = row",
   "outputs": "zp_74/zp_75 replaced by 39-x/39-y for a remote command, untouched for an own command; A clobbered",
   "confidence": "high"
  },
  {
   "addr": "4FF7",
   "name": "readCmdArg1",
   "summary": "Argument reader for the 2-byte commands (all the per-unit and per-group orders $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2, $A4): reads the single byte after the command byte into cmdParam0 and advances mergedCmdReadIdx by 2.",
   "inputs": "D_90B7, D_908F",
   "outputs": "zp_73; D_90B7 += 2; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "5005",
   "name": "mirrorCmdCoordsIfRemoteDec",
   "summary": "Coordinate mirror for the drone, which occupies a 2x2 cell block: for a remote command it calls mirrorCmdCoordsIfRemote and then decrements both coordinates, giving 38-x / 38-y, i.e. the top-left corner of the mirrored 2x2 block. No-op for an own command. Used by cmdDroneMove and cmdDroneDetonate.",
   "inputs": "zp_76, zp_74, zp_75",
   "outputs": "zp_74/zp_75 = 38-x, 38-y when remote; A clobbered",
   "confidence": "medium"
  },
  {
   "addr": "5011",
   "name": "cmdMoveUnit",
   "summary": "Handler of command $80 MOVE (4 bytes: unit, col, row) reached through commandHandlerTable. Reads and mirrors the arguments, then tail-jumps into applyMoveOrderOld ($34C6), which drops the unit out of its group, refreshes its display flags and sets the destination.",
   "inputs": "D_908F/D_90B7",
   "outputs": "unit destination/waypoint tables via $34C6",
   "confidence": "high"
  },
  {
   "addr": "501A",
   "name": "cmdMoveGroupRelative",
   "summary": "Handler of command $81 (4 bytes: unit, col, row): reads and mirrors the arguments and tail-jumps into moveGroupKeepingFormation ($4A52), which shifts every member of the named unit's group by the same offset so the formation is preserved.",
   "inputs": "D_908F/D_90B7",
   "outputs": "destinations of all group members via $4A52",
   "confidence": "high"
  },
  {
   "addr": "5023",
   "name": "cmdCloakOn",
   "summary": "Handler of command $82 CLOAK ON (2 bytes: unit). Cloak and blitz are mutually exclusive, so it first runs cmdBlitzOff on the same unit; because that handler consumes the argument it saves mergedCmdReadIdx in the self-modified LDA operand at $502C and restores it before re-reading. Type 3 (SPY) units are rejected; otherwise unitWaypointCol bits 6-7 are set (cloak ordered + active) and control falls into the shared tail at $5058.",
   "inputs": "D_908F, D_90B7, F76C unit type",
   "outputs": "D_F898[unit] |= $C0, D_F8FC[unit] &= $3F, D_923B and D_9245 via the tail; self-modifies $502D",
   "confidence": "high"
  },
  {
   "addr": "504B",
   "name": "cmdCloakOff",
   "summary": "Handler of command $84 CLOAK OFF (2 bytes: unit); also called as a subroutine by cmdBlitzOn. Clears bits 6-7 of unitWaypointCol. The shared tail at $5058 records the unit in pendingRedrawUnit ($923B) for an own command only (the opponent's cloaking must not trigger a local redraw) and then falls into updateComcenCloakLock. Note there is no SPY exemption here, unlike the group handler.",
   "inputs": "zp_73 after readCmdArg1; zp_76",
   "outputs": "D_F898[unit] &= $3F; D_923B = unit for an own command; D_9245",
   "confidence": "high"
  },
  {
   "addr": "5062",
   "name": "cmdBlitzOff",
   "summary": "Handler of command $95 BLITZ OFF (2 bytes: unit); also called as a subroutine by cmdCloakOn. Clears bits 6-7 of unitWaypointRow (blitz ordered/active).",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F8FC[unit] &= $3F; zp_73 = unit; Y = unit",
   "confidence": "high"
  },
  {
   "addr": "5070",
   "name": "cmdBlitzOn",
   "summary": "Handler of command $93 BLITZ ON (2 bytes: unit). Mirror image of cmdCloakOn: saves/restores mergedCmdReadIdx around a call to cmdCloakOff (blitz cancels cloak), rejects type 3 (SPY), sets unitWaypointRow bits 6-7 and tail-jumps into applyBlitzStepCost ($3EE2), which pays the energy for the extra movement points.",
   "inputs": "D_908F, D_90B7, F76C unit type",
   "outputs": "D_F8FC[unit] |= $C0, D_F898[unit] &= $3F, energy/movement via $3EE2; self-modifies $507A",
   "confidence": "high"
  },
  {
   "addr": "5098",
   "name": "cmdGroupCloakOn",
   "summary": "Handler of command $83 (2 bytes: group id byte). Runs cmdGroupBlitzOff for the group first (exclusive states, index saved/restored via $50A2), then enters applyGroupCloakOrBlitz at $50D4 with X=$80 so the shared body works on table $F898 with OR $C0 and sets bit 7 of groupStatusTable.",
   "inputs": "D_908F/D_90B7; group id byte carries bits 2-5 (bits 2-4 group index, bit 5 side)",
   "outputs": "D_F898 of every living non-SPY member |= $C0, D_F8FC &= $3F, D_91F4[g] |= $80, D_923A, D_9245",
   "confidence": "high"
  },
  {
   "addr": "50AA",
   "name": "cmdGroupBlitzOn",
   "summary": "Handler of command $94 (2 bytes: group id byte). Runs cmdGroupCloakOff first, then enters the shared body at $50C1 with X=$C0 / A=$40 so it works on table $F8FC with OR $C0, calls applyBlitzStepCost for each member and sets groupStatusTable[g] = (old & $BF) | $40.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F8FC of every living non-SPY member |= $C0, per-member blitz cost, D_91F4[g]",
   "confidence": "high"
  },
  {
   "addr": "50BE",
   "name": "cmdGroupBlitzOff",
   "summary": "Handler of command $96 (2 bytes: group id byte) and the second patch entry of the self-modifying group routine. With X=A=0 it patches the operands at $512D (blitz-cost gate = 0 -> skipped), $514D (groupStatusTable OR = 0), $514B (groupStatusTable AND = $BF) and selects table $F8FC, then continues at $50EB.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F8FC of members &= $3F, D_91F4[g] &= $BF",
   "confidence": "high"
  },
  {
   "addr": "50D2",
   "name": "cmdGroupCloakOff",
   "summary": "Handler of command $85 (2 bytes: group id byte): enters the patch block at $50D4 with X=0, selecting table $F898, OR 0 and groupStatusTable AND $7F, so the cloak bits of every member are cleared.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F898 of members &= $3F, D_91F4[g] &= $7F, D_923A for an own command, D_9245",
   "confidence": "high"
  },
  {
   "addr": "50EB",
   "name": "applyGroupCloakOrBlitz",
   "summary": "Shared, heavily self-modified body of the four group cloak/blitz handlers (entered by branch, never by JSR). It stores the chosen table address into the LDA/STA operands at $5134/$513B, reads the group id argument, records pendingRedrawGroup ($923A) for an own command when the cloak variant is running (the AND operand at $514B equals $7F), ORs $40 into the group byte so it matches unitGroupByte & $7C, then walks units 99..0 skipping dead slots ($F76C bit 7), non-members and SPYs (type 3), optionally calling applyBlitzStepCost, and rewrites table[unit] = (old & $3F) | patchedBits. Finally it updates groupStatusTable[(groupByte & $3C) >> 2] and falls into updateComcenCloakLock.",
   "inputs": "patched operands at $512D/$5134/$5139/$513B/$514B/$514D; zp_73 group byte; D_F708, D_F76C",
   "outputs": "cloak or blitz bits of all matching units, D_91F4[group], D_923A, D_9245",
   "confidence": "high"
  },
  {
   "addr": "5151",
   "name": "updateComcenCloakLock",
   "summary": "Common tail of all cloak/blitz handlers and the only writer of $9245: copies bit 7 of unitWaypointCol[ownComcen] into unitInspectLockFlags, so $9245 bit 7 is set exactly while the local player's own COMCEN is cloaked. Read by the console/inspection code at $2064/$2096 and by the overlays ($75A5, $8048). Besides the fall-through from the cloak handlers it is called directly by both $6F00 overlay variants at $72E7.",
   "inputs": "D_9236 own comcen index, D_F898; no arguments",
   "outputs": "D_9245 = D_F898[comcen] & $80; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "515D",
   "name": "cmdClearTarget",
   "summary": "Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit): getBoomerTarget ($3C74) converts the unit index into the BOOMER ordinal in Y and boomerTargetTable[Y] is zeroed.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_91DC[ordinal] = 0",
   "confidence": "high"
  },
  {
   "addr": "516B",
   "name": "cmdSetTarget",
   "summary": "Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit). After readCmdArgs2 it rearranges the parameters so zp_73 = BOOMER and zp_75 = target and jumps to setBoomerTargetCmd ($3C50). The EOR #$80 applied to cmdFromOpponent at $5171 is undone by the PLA/STA at $517B, so it has no effect - dead code left from an earlier version.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_91DC[ordinal] = target | $80 via $3C50; zp_73/zp_75 rewritten",
   "confidence": "high"
  },
  {
   "addr": "5185",
   "name": "cmdJoinGroup",
   "summary": "Handler of command $88 JOIN GROUP (3 bytes: unit, group id byte): jumps to joinUnitToGroup ($4AD4) with zp_74 = unit and zp_75 = group. The eight instructions at $5188-$5197 are a redundant shuffle - only 'zp_73 = group' survives and nothing downstream reads zp_73, so the whole block is effectively dead code.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F708[unit] = (old & 3) | group | $40 and D_FB54 via $4AD4",
   "confidence": "high"
  },
  {
   "addr": "519B",
   "name": "cmdLeaveGroup",
   "summary": "Handler of command $89 (2 bytes: unit): clears bit 6 (in-group) of unitGroupByte and jumps to syncLocalGroupFlags ($4C6B), which copies bits 2-6 of unitGroupByte into unitDisplayFlags so the map stops drawing the group marker.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F708[unit] &= $BF, D_FB54[unit]",
   "confidence": "high"
  },
  {
   "addr": "51AB",
   "name": "cmdPlaceUnit",
   "summary": "Handler of command $8A (4 bytes: unit, col, row): teleports a unit during setup/deployment. It first saves the unit's current cloak bits (F898 & $C0) and blitz bits (F8FC & $C0) into the ORA operands at $51C9 and $51E5, then writes the mirrored column to unitCol, unitWaypointCol (keeping the cloak bits) and unitDestCol (with the idle bit 7), sets bit 7 of unitDisplayFlags and writes the row to unitRow, unitDestRow and unitWaypointRow (keeping the blitz bits).",
   "inputs": "D_908F/D_90B7, zp_76",
   "outputs": "D_F640, D_F6A4, D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of the unit; self-modifies $51C9/$51E5",
   "confidence": "high"
  },
  {
   "addr": "51EA",
   "name": "cmdSetCustomMapSeed",
   "summary": "Handler of command $A6 (4 bytes: three seed bytes): sets mapKindFlag ($0B9D) = 1 and falls into cmdSetMapSeed. Sent by overlay A ($79BD) when the map is the flipped/custom variant; the receiving side's map generator uses $0B9D to decide how to build the terrain.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_0B9D = 1, then everything cmdSetMapSeed does",
   "confidence": "medium"
  },
  {
   "addr": "51EE",
   "name": "cmdSetMapSeed",
   "summary": "Handler of command $8B (4 bytes: three seed bytes): mapKindFlag = 0, copies the three arguments into mapSeed $0B95-$0B97 and clears bit 7 of gamePhase zp_B1. Player 0 sends this at the start of a session; player 1's waitForMapSeedCommand ($7C1C) spins on gamePhase bit 7 until this handler runs, so both machines generate the identical battlefield.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_0B9D = 0, D_0B95-D_0B97 = seed, zp_B1 &= $7F",
   "confidence": "high"
  },
  {
   "addr": "520C",
   "name": "cmdComcenRepairedOrSetupReady",
   "summary": "Handler of the 1-byte command $9E, which has two meanings. When gamePhase zp_B1 is zero (battle running) it clears the low nibble (stun counter) of unitStunTable for the sending side's COMCEN - the result of a successful COMCEN repair from the REPAIR console tab. When gamePhase is nonzero it is the phase-sync/READY acknowledgement of sendPhaseSyncAndWait ($7BF9): it clears the sender's bit in gamePhase with getPauseClearMask, sets inputLockoutTimer = $20, injects key code $20 (SPACE) into newKeyEvent so any 'press space' wait loop falls through, and resets chatState.",
   "inputs": "zp_B1, zp_76, D_0B9F playerSide, perSideValueTable $0C8A",
   "outputs": "battle: D_F960[comcen] &= $F0. Setup: zp_B1 &= mask, D_0B7D = $20, D_90EA = $20, D_929C = 0. D_90B7 += 1 in both cases",
   "confidence": "high"
  },
  {
   "addr": "523E",
   "name": "cmdSessionControl",
   "summary": "Handler of command $8C (2 bytes: reason). It is never dispatched from the merged buffer - both packet mergers ($4F36 and $4F9C) call it directly with A = the reason byte while the packets are being merged, so the effect is immediate and the byte is not copied into the film. Reason 0 = the opponent aborted: gameOverFlags $923F = $C0, the message queue is cleared, 'GAME ABORTED.' (message 3) is shown for a remote abort, gameEndCountdown $92C9 = $62 (no game running) or $71 (game running), and the dispatcher is abandoned by pulling two return addresses off the stack and jumping to the comm module with X=2. Reasons 1 and 2 return at once ($8C,02 is the menu-level READY-for-new-game handshake handled by handleMenuSessionPacket). Reason 3 = pause/timeout (RUN/STOP, sent by $C74B): pauseState zp_BE = 3, persistent message $19 'TIMEOUT. RUN/STOP TO RESUME.', 60-frame input lockout. Reason 5 = hang up (menu item at $65C7): pauseState = $80. Any other reason (4 = resume, sent by $C786) clears the sender's pauseState bit via getPauseClearMask and, when no bits are left, clears the persistent message and shows 'RESUMED' (message $1B). Everything except reason 0 is skipped entirely during film playback ($0B9B bit 7).",
   "inputs": "A = reason byte, X/Y = the merge loop registers, zp_76, D_0B9B, D_0BA8",
   "outputs": "zp_BE, D_92A1, D_0B7D, D_929C, D_92C9, D_923F, zp_72, message queue; returns through $532A with X/Y restored, or never returns for reason 0",
   "confidence": "high"
  },
  {
   "addr": "52AE",
   "name": "getPauseClearMask",
   "summary": "Returns the AND mask that clears the resuming side's bit of the two-bit pause/ready state (used on pauseState zp_BE by cmdSessionControl and on gamePhase zp_B1 by cmdComcenRepairedOrSetupReady). Comm build 1 ($0BA5 bit 7 set) has no independent opponent, so it returns $FC and clears both bits at once. Otherwise an own command returns $FD (clear bit 0) and a remote command returns $FE (clear bit 1); in the remote case, if bit 1 of X was still set it also queues message 4 'OPPONENT READY.'.",
   "inputs": "X = current pause/phase byte, D_0BA5, zp_76",
   "outputs": "A = AND mask $FC/$FD/$FE; may queue message 4; Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "52C8",
   "name": "cmdDroneMove",
   "summary": "Handler of command $8D (4 bytes: heading, col, row), the opponent's live drone telemetry. Only mergeIncomingPacket calls it (at $4F28, with Y pointing one past the command byte in the received packet $E020); mergeOutgoingPacket handles its own $8D inline by re-centring the view instead, and the commandHandlerTable slot for $8D is deliberately $FFFF because the byte is never copied into the merged buffer. It stores heading/col/row, mirrors the 2x2 drone origin for the remote frame, and unless droneFlags bits 4-5 say the drone is already finished it sets bit 0 of droneStateFlags and, only while screen 1 is showing, disables sprite 0, remembers the drone cell in $92F6/$92F7, converts the cell to sprite coordinates with getUnitSpriteCentreOnGrid (leaving sprite 0 hidden when the drone is off the visible grid), selects activeSpriteIndex 0, turns the heading into shape (heading+4) mod 8 - adding 4 to an 8-way heading reverses it, which is exactly the direction change the 180-degree map mirror demands - loads that shape with loadDirectionShapeSprite and sets sprite 0 colour 1.",
   "inputs": "Y = index in D_E020, zp_76, D_92F9, D_90FB current screen",
   "outputs": "zp_73/74/75, D_920D |= 1, D_9022, D_92F6/D_92F7, D_9010/D_9018, D_91CC, D_9008, sprite 0 shape; X/Y restored from the operands patched by saveRegsForCmdReturn",
   "confidence": "high"
  },
  {
   "addr": "532F",
   "name": "saveRegsForCmdReturn",
   "summary": "Two-instruction helper used by cmdSessionControl and cmdDroneMove: copies X and Y into the LDX/LDY immediate operands at $532B/$532D so the common exit at $532A hands the packet merger back exactly the loop registers it had. Necessary because both handlers are called from the middle of the merge loop rather than from the table dispatcher.",
   "inputs": "X, Y",
   "outputs": "self-modifies $532B and $532D",
   "confidence": "high"
  },
  {
   "addr": "5336",
   "name": "cmdDroneDetonate",
   "summary": "Handler of command $8E (3 bytes: col, row). For an own command it clears bit 5 of droneStateFlags. If droneFlags bit 7 is already set (the drone had reached its target/expired before the order arrived) the detonation is cancelled: the command byte in the merged buffer is neutered in place by writing $0E over it (D_908C,y with y = readIdx+3 addresses mergedCmdBuffer[readIdx]) - clearing bit 7 makes executeTurnCommands skip the command, and because getCommandLength masks with $3F the length still comes out as 3, so the film records a correctly sized no-op. Otherwise droneFlags = $40 (detonated), the coordinates are mirrored/decremented, sound $11 is played (suppressed for a silent own command outside film playback), applyDroneBlast ($72A1 in overlay B) is called with A=$19 to damage everything in the blast radius, a blast ring sprite is drawn at the cell ($33C8 with A=1), and a remote detonation finally clears bits 0-5 of droneStateFlags through the shared tail at $537E.",
   "inputs": "D_908F/D_90B7, zp_76, D_92F9, D_0B9B",
   "outputs": "D_92F9, D_920D, mergedCmdBuffer command byte, sound, blast damage and sprite",
   "confidence": "high"
  },
  {
   "addr": "537B",
   "name": "cmdDroneRelease",
   "summary": "Handler of the 1-byte command $8F: just advances mergedCmdReadIdx and falls into the shared tail at $537E which, for a remote command only, clears bits 0-5 of droneStateFlags - the opponent has let go of the drone, so the local copy of his drone-control state is reset while bits 6-7 (own control / drone screen) are preserved.",
   "inputs": "zp_76, D_90B7",
   "outputs": "D_920D &= $C0 when remote",
   "confidence": "medium"
  },
  {
   "addr": "538B",
   "name": "cmdDroneArrived",
   "summary": "Handler of the 1-byte command $92, sent by overlay B ($79CA) when the drone comes within one cell of its target: clears bits 6-7 of droneStateFlags and, for an own command, sets bit 4 of droneFlags. Both packet mergers additionally set droneFlags = $80 the moment a $92 byte is seen in either packet, and sendOutgoingPacket drops a queued $92 while droneFlags bit 6 is set.",
   "inputs": "zp_76, D_920D, D_92F9",
   "outputs": "D_920D &= $3F; D_92F9 |= $10 for an own command",
   "confidence": "high"
  },
  {
   "addr": "53A3",
   "name": "cmdDroneLaunch",
   "summary": "Handler of the 1-byte command $97: clears droneFlags (fresh drone in flight). For the opponent's launch it shows 'DRONE ALERT!' with sound (showDroneAlert, overlay B $820F) and selects index 1. Then, if that side still has drones, it decrements dronesLeft[side] and calls markComcenRevealed ($403B), which exposes the launching COMCEN for six rounds - the price of using a drone.",
   "inputs": "zp_76, D_92AE",
   "outputs": "D_92F9 = 0, D_92AE[side] decremented, comcen reveal timer and visibility bits via $403B",
   "confidence": "high"
  },
  {
   "addr": "53C0",
   "name": "cmdMissileStrike",
   "summary": "Handler of command $90 (3 bytes), which carries two different payloads. During a setup phase (gamePhase zp_B1 nonzero) the two bytes are the sender's drone and missile allowances and are stored in dronesLeft/missilesLeft for that side (index 0 own, 1 opponent) - this is what overlay A's editDronesAndMissiles sends. During the battle they are the target column/row: if the firing side still has missiles and no game-end reason is set, missilesLeft is decremented, markComcenRevealed exposes the firing COMCEN, the coordinates are mirrored for a remote shot, and if the cell is on the 40x40 map and holds a unit ($F000 cell bit 7) that unit takes 14 energy of damage - halved to 7 for either COMCEN (index $31 or $63) - through applyHitToUnit. A blast ring sprite is drawn at the cell ($33C8 with A=0) and sound $0C plays unless it is a silent own shot outside film playback.",
   "inputs": "D_908F/D_90B7, zp_B1, zp_76, D_0BA8, map $F000",
   "outputs": "D_92AE/D_92B0[side], unit energy/score via $3B22, sprite 4 blast ring, sound",
   "confidence": "high"
  },
  {
   "addr": "542E",
   "name": "cmdSelfDestruct",
   "summary": "Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole group of that unit'). For a single unit it falls straight into destroyUnit. For a group it builds the match value (unitGroupByte & $3C) | $40 into the CMP operand at $544E - note the $3C mask keeps the side bit 5, so a group never spans both sides - and walks units 99..0 calling destroyUnit on every member.",
   "inputs": "D_908F/D_90B7, D_F708",
   "outputs": "D_F76C entries set to $FF; self-modifies $544E; zp_73 used as the loop counter",
   "confidence": "high"
  },
  {
   "addr": "5459",
   "name": "destroyUnit",
   "summary": "Removes unit zp_73 from play, except a COMCEN (index $31 or $63), which is never allowed to self-destruct. It applies 52 ($34) damage through sub_3B52 - more than the 50-point maximum energy, so the kill is certain and goes through the normal explosion/score/redraw path - and then frees the slot outright by writing $FF into unitTypeTable.",
   "inputs": "zp_73 = unit index",
   "outputs": "D_F76C[unit] = $FF, explosion/sound/score side effects of $3B52; Y = unit",
   "confidence": "high"
  },
  {
   "addr": "5470",
   "name": "cmdMoveKeepGroup",
   "summary": "Handler of command $98 (4 bytes: unit, col, row), the MOVE TO GOAL order issued to each member of a group in turn: reads and mirrors the arguments and jumps to setGroupMemberDestination ($4C59), which decrements pendingGroupOrderCount when the unit belongs to pendingGroupOrderId and then sets the destination through $34D5 without disturbing the group membership bits.",
   "inputs": "D_908F/D_90B7",
   "outputs": "destination tables, D_929E",
   "confidence": "high"
  },
  {
   "addr": "5479",
   "name": "cmdAddEnergy",
   "summary": "Handler of command $9A (3 bytes: unit, amount) sent by the COMCEN energy repair: unless the game has already ended by knockout ($0BA8 bit 7) it adds the amount to bits 0-5 of unitEnergyTable, saturating at 50 ($32), and restores the two visibility/spotted bits 6-7 that were saved into the ORA operand at $5499.",
   "inputs": "D_908F/D_90B7, D_0BA8",
   "outputs": "D_F9C4[unit]; self-modifies $5499",
   "confidence": "high"
  },
  {
   "addr": "549E",
   "name": "cmdHaltGroup",
   "summary": "Handler of command $9B (2 bytes: group id byte) - the group HALT order. For every living member (walking units 99..0 and matching unitGroupByte & $7C against groupByte | $40) it copies the unit's current position into its destination and waypoint, sets the idle bits (unitDestCol bit 7 and unitDisplayFlags bit 7), preserves the cloak bits in unitWaypointCol via the ORA operand patched at $54D0, and clears the blitz bits by overwriting unitWaypointRow with the plain row.",
   "inputs": "D_908F/D_90B7, D_F708, D_F76C",
   "outputs": "D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of every member; self-modifies $54D0",
   "confidence": "high"
  },
  {
   "addr": "54E1",
   "name": "cmdSetRecycler",
   "summary": "Handler of command $9C (4 bytes: mode, col, row), sent by overlay A's chooseRecycler ($8436) during setup for game types 4-6 (SLUGGERS/FULL WAR/DEFENDER). recyclerMode[side] = mode with side = the sender's absolute side (playerSide, EOR 1 when remote); for modes 0 and 1 the coordinates are mirrored for a remote sender and stored in recyclerCol/recyclerRow[side]. An own command additionally draws the recycler on the overview map with drawRecyclerOnMap, Y = the side index restored from the operand patched at $550C.",
   "inputs": "D_908F/D_90B7, D_0B9F, zp_76",
   "outputs": "D_92A6/D_92A8/D_92AA indexed by side; map redraw; self-modifies $550C",
   "confidence": "high"
  },
  {
   "addr": "5511",
   "name": "cmdChooseSide",
   "summary": "Handler of command $9D (2 bytes: the sender's chosen side), the side-selection handshake. In comm build 1 ($0BA5 bit 7) there is no real opponent and playerSide is forced to 0. Otherwise only the opponent's copy of the command matters: our own pick was stashed in $90F8 by sendChooseSide, and if the two picks differ playerSide becomes 0 or 1 according to whether ours was the higher (ROL of the carry from the CPY). If both players picked the same side the tie is broken by nextGameRandom with bit 7 set as a 'conflict, retry' marker. Either way gamePhase zp_B1 is set to $DF, which is what startGameSession waits for.",
   "inputs": "D_908F/D_90B7, D_0BA5, zp_76, D_90F8",
   "outputs": "D_0B9F playerSide, zp_B1 = $DF",
   "confidence": "high"
  },
  {
   "addr": "553B",
   "name": "sendChooseSide",
   "summary": "Not a command handler but the sender side of the $9D exchange, called from overlay A's side-selection screen ($7976): copies playerSide into cmdParam0 and into selectedUnit $90F8 (where cmdChooseSide later finds our own pick) and queues the 2-byte command $9D through queueCmd2.",
   "inputs": "D_0B9F",
   "outputs": "zp_73, D_90F8, two bytes appended to the outgoing command ring",
   "confidence": "high"
  },
  {
   "addr": "5548",
   "name": "cmdDigIn",
   "summary": "Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20, i.e. unitStunTable bits 5-7 = 001, which processUnitStep then advances by $20 each round the unit stays still until the unit counts as fortified.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F960[unit] = (old & $1F) | $20",
   "confidence": "high"
  },
  {
   "addr": "554C",
   "name": "cmdDigOut",
   "summary": "Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears the dig progress bits 5-7 of unitStunTable, and bit 6 of unitTypeTable (the 'dug in / fortified' flag consulted by the combat code) is cleared as well.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F960[unit] &= $1F, D_F76C[unit] &= $BF",
   "confidence": "high"
  },
  {
   "addr": "555A",
   "name": "setUnitDigBits",
   "summary": "Shared prologue of cmdDigIn/cmdDigOut: patches the ORA operand at $5568 with A, reads the unit argument and falls into applyDigBitsToUnit.",
   "inputs": "A = new bits 5-7 ($20 or $00)",
   "outputs": "self-modifies $5568; zp_73 and Y = unit; D_F960[unit]",
   "confidence": "high"
  },
  {
   "addr": "5562",
   "name": "applyDigBitsToUnit",
   "summary": "Inner two-line worker shared by the single-unit and group dig handlers: unitStunTable[Y] = (old & $1F) | patchedBits, i.e. the low nibble stun counter and bit 4 survive while the dig-state bits 5-7 are replaced.",
   "inputs": "Y = unit index, patched ORA operand at $5568",
   "outputs": "D_F960[Y]",
   "confidence": "high"
  },
  {
   "addr": "556D",
   "name": "cmdGroupDigIn",
   "summary": "Handler of command $A1 (2 bytes: group id byte): patches the dig bits to $20 and runs the group loop at $5573.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_F960 of every living member",
   "confidence": "high"
  },
  {
   "addr": "5571",
   "name": "cmdGroupDigOut",
   "summary": "Handler of command $A2 (2 bytes: group id byte) and entry of the shared group dig loop: patches the dig bits to 0, reads the group byte, ORs $40 into it and walks units 99..0 calling applyDigBitsToUnit for every living unit whose unitGroupByte & $7C matches. Note that, unlike cmdDigOut, the group variant does not clear bit 6 of unitTypeTable, so a group dug in as a group can only be un-fortified unit by unit.",
   "inputs": "D_908F/D_90B7, D_F708, D_F76C",
   "outputs": "D_F960 of every member; self-modifies $5568",
   "confidence": "high"
  },
  {
   "addr": "5596",
   "name": "cmdSetFormation",
   "summary": "Handler of command $A3 (3 bytes: group id byte, formation index): reads the two arguments and jumps to rotateGroupFormation ($4C84), which re-arranges the destinations of all group members around the group centre using the offset table at $4C7E.",
   "inputs": "D_908F/D_90B7",
   "outputs": "destinations of the group members",
   "confidence": "high"
  },
  {
   "addr": "559C",
   "name": "cmdSetGameType",
   "summary": "Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions $0BA3 (bits 0-2 game type, bit 6 custom rules, bit 7 custom map). Sent by overlay A's runMapTypeMenu ($833E) so both machines agree on the scenario before the map is generated.",
   "inputs": "D_908F/D_90B7",
   "outputs": "D_0BA3",
   "confidence": "high"
  },
  {
   "addr": "55A5",
   "name": "cmdSetupOption",
   "summary": "Handler of command $A5 (3 bytes: kind, value), the generic setup-option carrier of overlay A's custom-rules editors. Kind 0 (HANDICAP, applyHandicapPoints $84AA) adds value*10 to the 16-bit counter $92A2/$92A4 indexed by the sender's absolute side; since scores are indexed by the side being scored against, this is a head start credited to the sender's opponent. The multiply clobbers X, so the index is restored from the operand patched at $55C2. Kind 1 (TERRAIN PTS, editTerrainPoints $84EC) stores the value in $92AC[senderSide EOR 1], the per-zone bonus later read at $47A9 when a unit penetrates the enemy half. Kind 2 and above (only kind 3 is ever sent - COMCEN SPEED, editComcenSpeed $85BC) stores the value in $92B4 indexed 0 = own / 1 = opponent; value 4 there is what makes $34E1 refuse to move a COMCEN at all.",
   "inputs": "D_908F/D_90B7, D_0B9F, zp_76",
   "outputs": "D_92A2/D_92A4, D_92AC, D_92B4; self-modifies $55C2",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "72",
   "scope": "zp",
   "name": "messageHoldTimer",
   "meaning": "message hold-down counter; cleared together with persistentMessageId when cmdSessionControl shows RESUMED",
   "confidence": "medium"
  },
  {
   "addr": "73",
   "scope": "zp",
   "name": "cmdParam0",
   "meaning": "first argument byte of the command being executed (unit index, group id byte, side, reason, option kind); also reused as the unit loop counter by cmdSelfDestruct",
   "confidence": "high"
  },
  {
   "addr": "74",
   "scope": "zp",
   "name": "cmdParam1",
   "meaning": "second argument byte (map column, unit index, option kind)",
   "confidence": "high"
  },
  {
   "addr": "75",
   "scope": "zp",
   "name": "cmdParam2",
   "meaning": "third argument byte (map row, target unit, option value)",
   "confidence": "high"
  },
  {
   "addr": "76",
   "scope": "zp",
   "name": "cmdFromOpponent",
   "meaning": "bit 7 set while the command currently being executed came from the opponent's packet; drives coordinate mirroring, the side index and the 'own action' side effects (redraw requests, sounds)",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "0 during the battle, nonzero during side selection/setup. The low two bits are the two players' outstanding phase-sync flags cleared by command $9E through getPauseClearMask; bit 7 is cleared by the map-seed command $8B/$A6; cmdChooseSide sets the whole byte to $DF",
   "confidence": "high"
  },
  {
   "addr": "BE",
   "scope": "zp",
   "name": "pauseState",
   "meaning": "pause/disconnect state: bit 0 own pause, bit 1 opponent pause (cleared by getPauseClearMask), 3 after a timeout, $80 after a hang-up",
   "confidence": "high"
  },
  {
   "addr": "0B7D",
   "scope": "abs",
   "name": "inputLockoutTimer",
   "meaning": "frames during which input is ignored: 60 ($3C) after a pause/timeout command, $20 after a setup-ready command",
   "confidence": "high"
  },
  {
   "addr": "0B95",
   "scope": "abs",
   "name": "mapSeed",
   "meaning": "three-byte map generator seed $0B95-$0B97, written by commands $8B and $A6",
   "confidence": "high"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "filmPlaybackFlag",
   "meaning": "bit 7 = game film playback; cmdSessionControl skips everything except the abort path and the sound gates flip",
   "confidence": "high"
  },
  {
   "addr": "0B9D",
   "scope": "abs",
   "name": "mapKindFlag",
   "meaning": "0 after command $8B, 1 after command $A6 (flipped/custom map variant)",
   "confidence": "medium"
  },
  {
   "addr": "0B9F",
   "scope": "abs",
   "name": "playerSide",
   "meaning": "local player's absolute side 0/1, decided by cmdChooseSide (bit 7 = tie, resolved randomly); EOR 1 gives the opponent",
   "confidence": "high"
  },
  {
   "addr": "0BA3",
   "scope": "abs",
   "name": "gameTypeOptions",
   "meaning": "game type and option bits, set on both machines by command $A4",
   "confidence": "high"
  },
  {
   "addr": "0BA5",
   "scope": "abs",
   "name": "commBuildFlag",
   "meaning": "bit 7 = comm module build 1 (solo trainer / no real opponent): getPauseClearMask clears both pause bits at once and cmdChooseSide forces side 0",
   "confidence": "high"
  },
  {
   "addr": "0BA8",
   "scope": "abs",
   "name": "gameEndReason",
   "meaning": "0 while a game is in progress; nonzero blocks missile strikes and energy repairs and makes an abort use the short countdown",
   "confidence": "high"
  },
  {
   "addr": "0C8A",
   "scope": "abs",
   "name": "comcenIndexBySide",
   "meaning": "two bytes $31,$63 - the COMCEN unit index of side 0 and side 1",
   "confidence": "high"
  },
  {
   "addr": "9008",
   "scope": "abs",
   "name": "spriteColourShadow",
   "meaning": "shadow of sprite 0's colour register; set to 1 for the drone marker",
   "confidence": "high"
  },
  {
   "addr": "9010",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "shadow of sprite 0's X coordinate = drone screen position",
   "confidence": "high"
  },
  {
   "addr": "9018",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "shadow of sprite 0's Y coordinate",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "shadow of $D015; cmdDroneMove clears bit 0 before deciding whether the drone is visible",
   "confidence": "high"
  },
  {
   "addr": "908C",
   "scope": "abs",
   "name": "outgoingCmdRingLast",
   "meaning": "last byte of the outgoing command ring; used only at $534F as an index base so that D_908C,readIdx+3 addresses mergedCmdBuffer[readIdx]",
   "confidence": "high"
  },
  {
   "addr": "908F",
   "scope": "abs",
   "name": "mergedCmdBuffer",
   "meaning": "merged command buffer for one exchange (40 bytes): side 0's commands followed by side 1's; the source for both the dispatcher and the game film",
   "confidence": "high"
  },
  {
   "addr": "90B7",
   "scope": "abs",
   "name": "mergedCmdReadIdx",
   "meaning": "read index into mergedCmdBuffer; every handler in this chunk advances it past its own arguments (via readCmdArgs3/readCmdArgs2/readCmdArg1 or an explicit INC for the 1-byte commands)",
   "confidence": "high"
  },
  {
   "addr": "90EA",
   "scope": "abs",
   "name": "newKeyEvent",
   "meaning": "pending key code from the keyboard scanner; command $9E injects $20 (SPACE) to release a 'press space' wait loop",
   "confidence": "high"
  },
  {
   "addr": "90F8",
   "scope": "abs",
   "name": "selectedUnit",
   "meaning": "currently selected unit; during side selection it holds our own side pick so cmdChooseSide can compare it with the opponent's",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "active screen id; cmdDroneMove only touches sprite 0 while it is 1 (the drone/radar screen)",
   "confidence": "high"
  },
  {
   "addr": "91CC",
   "scope": "abs",
   "name": "activeSpriteIndex",
   "meaning": "which sprite loadDirectionShapeSprite fills and enables; cmdDroneMove sets it to 0 for the drone",
   "confidence": "high"
  },
  {
   "addr": "91DC",
   "scope": "abs",
   "name": "boomerTargetTable",
   "meaning": "per-BOOMER locked target (index = BOOMER ordinal from getBoomerTarget); 0 = none, else target | $80",
   "confidence": "high"
  },
  {
   "addr": "91F4",
   "scope": "abs",
   "name": "groupStatusTable",
   "meaning": "16 entries indexed by (groupByte & $3C) >> 2 = group index 0-6 plus 8 for side 1; bit 7 = group cloaked, bit 6 = group blitzing",
   "confidence": "high"
  },
  {
   "addr": "920D",
   "scope": "abs",
   "name": "droneStateFlags",
   "meaning": "drone control state. Bit 6 = the drone is under local control (sendOutgoingPacket then steers it and queues $8D); bit 7 gates the $8E detonate and the missile marker path in $33C8; bit 0 is set when the opponent's drone position arrives; cmdDroneArrived clears bits 6-7 and a remote release/detonate clears bits 0-5",
   "confidence": "medium"
  },
  {
   "addr": "9236",
   "scope": "abs",
   "name": "comcenUnit",
   "meaning": "index of the local player's COMCEN ($31 or $63)",
   "confidence": "high"
  },
  {
   "addr": "923A",
   "scope": "abs",
   "name": "pendingRedrawGroup",
   "meaning": "group whose members the tick loop must redraw ($418D); set by own group cloak on/off commands",
   "confidence": "high"
  },
  {
   "addr": "923B",
   "scope": "abs",
   "name": "pendingRedrawUnit",
   "meaning": "unit the tick loop must redraw ($417B); set by own single-unit cloak on/off commands",
   "confidence": "high"
  },
  {
   "addr": "923F",
   "scope": "abs",
   "name": "gameOverFlags",
   "meaning": "set to $C0 when the opponent aborts the session (also written by the film end and computeGameResult)",
   "confidence": "high"
  },
  {
   "addr": "9245",
   "scope": "abs",
   "name": "unitInspectLockFlags",
   "meaning": "written only by updateComcenCloakLock: bit 7 set while the local COMCEN is cloaked, which restricts unit inspection in the console",
   "confidence": "high"
  },
  {
   "addr": "929C",
   "scope": "abs",
   "name": "chatState",
   "meaning": "chat input state; reset by the pause/resume/ready commands",
   "confidence": "medium"
  },
  {
   "addr": "929F",
   "scope": "abs",
   "name": "pendingOwnCmdCount",
   "meaning": "number of own commands queued but not yet executed; decremented by the dispatcher and by the mergers",
   "confidence": "high"
  },
  {
   "addr": "92A1",
   "scope": "abs",
   "name": "persistentMessageId",
   "meaning": "status message that stays on screen: $19 TIMEOUT, 0 = none",
   "confidence": "high"
  },
  {
   "addr": "92A2",
   "scope": "abs",
   "name": "sideScoreLo",
   "meaning": "two-byte-per-side 16-bit score low bytes, indexed by the side being scored against; command $A5 kind 0 seeds it with handicap points",
   "confidence": "high"
  },
  {
   "addr": "92A4",
   "scope": "abs",
   "name": "sideScoreHi",
   "meaning": "high bytes of sideScoreLo",
   "confidence": "high"
  },
  {
   "addr": "92A6",
   "scope": "abs",
   "name": "recyclerMode",
   "meaning": "per-side recycler mode from command $9C (values >= 2 carry no coordinates)",
   "confidence": "high"
  },
  {
   "addr": "92A8",
   "scope": "abs",
   "name": "recyclerCol",
   "meaning": "per-side recycler map column",
   "confidence": "high"
  },
  {
   "addr": "92AA",
   "scope": "abs",
   "name": "recyclerRow",
   "meaning": "per-side recycler map row",
   "confidence": "high"
  },
  {
   "addr": "92AC",
   "scope": "abs",
   "name": "terrainPointsBySide",
   "meaning": "per-side TERRAIN PTS setting (0-2) written by command $A5 kind 1 into the entry of the side opposite the sender; read at $47A9 when a unit penetrates the enemy half, doubled in the deepest zone",
   "confidence": "medium"
  },
  {
   "addr": "92AE",
   "scope": "abs",
   "name": "dronesLeft",
   "meaning": "two bytes [0 = own, 1 = opponent]: drones still available; set by command $90 during setup, decremented by command $97",
   "confidence": "high"
  },
  {
   "addr": "92B0",
   "scope": "abs",
   "name": "missilesLeft",
   "meaning": "two bytes [0 = own, 1 = opponent]: missiles still available; set by command $90 during setup, decremented by a battle-phase $90",
   "confidence": "high"
  },
  {
   "addr": "92B4",
   "scope": "abs",
   "name": "comcenSpeedClass",
   "meaning": "two bytes [0 = own, 1 = opponent]: COMCEN SPEED setting 0-4 from command $A5 kind 3; the value 4 makes $34E1 pin the COMCEN in place",
   "confidence": "high"
  },
  {
   "addr": "92C9",
   "scope": "abs",
   "name": "gameEndCountdown",
   "meaning": "end-of-game countdown started by an abort: $62 when no game is running, $71 when one is",
   "confidence": "high"
  },
  {
   "addr": "92F6",
   "scope": "abs",
   "name": "droneCol",
   "meaning": "last reported drone map column (also used by mergeOutgoingPacket to re-centre the view)",
   "confidence": "high"
  },
  {
   "addr": "92F7",
   "scope": "abs",
   "name": "droneRow",
   "meaning": "last reported drone map row",
   "confidence": "high"
  },
  {
   "addr": "92F9",
   "scope": "abs",
   "name": "droneFlags",
   "meaning": "drone life cycle: 0 at launch ($97), bit 7 = arrived/finished (set by either merger on seeing $92), $40 = detonated ($8E), bit 4 = own arrival, bits 4-5 block further $8D processing",
   "confidence": "high"
  },
  {
   "addr": "E01E",
   "scope": "abs",
   "name": "commRxLength",
   "meaning": "length of the received packet",
   "confidence": "high"
  },
  {
   "addr": "E020",
   "scope": "abs",
   "name": "commRxBuffer",
   "meaning": "received packet; cmdDroneMove reads its heading/col/row directly out of it",
   "confidence": "high"
  },
  {
   "addr": "E027",
   "scope": "abs",
   "name": "commTxBuffer",
   "meaning": "copy of the packet just sent; mergeOutgoingPacket passes its $8C reason byte to cmdSessionControl",
   "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": "unitGroupByte",
   "meaning": "per-unit flags: bits 0-1 facing, bits 2-4 group index, bit 5 side, bit 6 in-group; group handlers compare (value & $7C) with groupByte | $40",
   "confidence": "high"
  },
  {
   "addr": "F76C",
   "scope": "abs",
   "name": "unitTypeTable",
   "meaning": "per-unit type in bits 0-2 (3 = SPY, 4 = COMCEN), bit 6 dug in/fortified, 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 path column; bits 6-7 = cloak ordered/active",
   "confidence": "high"
  },
  {
   "addr": "F8FC",
   "scope": "abs",
   "name": "unitWaypointRow",
   "meaning": "per-unit path row; bits 6-7 = blitz ordered/active",
   "confidence": "high"
  },
  {
   "addr": "F960",
   "scope": "abs",
   "name": "unitStunTable",
   "meaning": "per-unit: bits 0-3 stun counter (cleared for the COMCEN by command $9E), bits 5-7 dig-in progress",
   "confidence": "high"
  },
  {
   "addr": "F9C4",
   "scope": "abs",
   "name": "unitEnergyTable",
   "meaning": "per-unit energy in bits 0-5 (0-50), bits 6-7 visibility/spotted",
   "confidence": "high"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitDisplayFlags",
   "meaning": "per-unit display flags: bits 2-6 mirror unitGroupByte, bit 7 = idle",
   "confidence": "high"
  }
 ],
 "dataBlocks": [],
 "misclassified": [],
 "insights": [
  "The whole range $4FC4-$55E5 is the command-handler block reached through commandHandlerTable ($4D2A). Every handler starts by consuming its own arguments with one of the three readers at the head of the block: readCmdArgs3 ($4FC4, 4-byte commands), readCmdArgs2 ($4FD0, 3-byte), readCmdArg1 ($4FF7, 2-byte); 1-byte handlers just INC $90B7 themselves. The advance always matches commandLengthTable ($4D78), which I checked entry by entry for all 39 commands.",
  "Coordinate mirroring is the core of the two-machine design: both C64s run one identical simulation, so every map coordinate that arrives from the link is turned into 39-x/39-y by mirrorCmdCoordsIfRemote ($4FE4) before it is used, and each player simply looks at the shared world from the other end. The drone, which occupies a 2x2 cell block, needs mirrorCmdCoordsIfRemoteDec ($5005) so its top-left corner mirrors to 38-x/38-y.",
  "zp_76 (cmdFromOpponent) is more than a mirror flag: it also selects the side index for the per-side arrays ($92A6/$92AE/$92B0/$92B4), and gates all the purely local side effects - pendingRedrawUnit/Group, sounds, the DRONE ALERT message - so that the opponent's actions never trigger the local player's feedback.",
  "Cancelling a command in flight: cmdDroneDetonate ($534F) writes $0E over its own $8E command byte in mergedCmdBuffer (addressed as D_908C,readIdx+3). Clearing bit 7 makes executeTurnCommands skip the command, and because getCommandLength masks the byte with $3F the skip length is still the correct 3. This is the general mechanism for 'cancelled command' bytes with bit 7 clear in the film stream.",
  "Group id bytes on the wire are not plain 0-6 indices: they carry bits 2-5 of unitGroupByte (bits 2-4 = group, bit 5 = side). All group handlers OR $40 into the byte and compare it with unitGroupByte & $7C, so a group can never span both sides. groupStatusTable $91F4 is indexed by (groupByte & $3C) >> 2, i.e. group + 8*side, 16 entries.",
  "The four group cloak/blitz handlers ($5098/$50AA/$50BE/$50D2) are one self-modifying routine: the entry patches the table address ($F898 cloak / $F8FC blitz), the AND/OR masks for the unit and for groupStatusTable, and a gate byte at $512D that decides whether applyBlitzStepCost is called per member. Cloak and blitz are mutually exclusive - each 'on' handler calls the opposite 'off' handler first, saving and restoring $90B7 around it in a self-modified operand.",
  "SPY units (type 3) are exempt from cloak and blitz in every handler that sets bits, and the group body also skips them when clearing, so a SPY's F898/F8FC bits are never touched by a group order. The single-unit cmdCloakOff/cmdBlitzOff have no such check.",
  "Commands $8C (session control) and $8D (drone telemetry) never enter mergedCmdBuffer: both packet mergers execute them in place ($4F28/$4F36 in mergeIncomingPacket, $4F84/$4F9C in mergeOutgoingPacket) and skip the copy. cmdSessionControl therefore receives A = the reason byte rather than the command byte, and saveRegsForCmdReturn ($532F) exists purely to give the merge loop its X/Y back. The commandHandlerTable slot for $8D is $FFFF; the slot for $8C at $4D42 still points at $523E but is unreachable, and if it ever were reached A would be $8C, not a reason code.",
  "Command $9E is overloaded by gamePhase: during the battle it is 'my COMCEN finished repairing' (clears the stun nibble of the sender's COMCEN), and during setup it is the phase-sync handshake of sendPhaseSyncAndWait ($7BF9) - it clears the sender's bit in gamePhase zp_B1, sets a 32-frame input lockout and injects a SPACE key so any 'press space' loop drops out. getPauseClearMask ($52AE) is shared between this use on zp_B1 and the pause/resume use on zp_BE.",
  "Command $90 is likewise overloaded by gamePhase: in setup it delivers the sender's drone and missile allowances into $92AE/$92B0, in the battle it is a missile strike doing 14 energy of damage (7 to a COMCEN).",
  "Command $A5 is the generic setup-option carrier: kind 0 = HANDICAP (value*10 added to the 16-bit score pot of the sender's side, which is the pot the opponent scores into), kind 1 = TERRAIN PTS into $92AC of the opposite side, kind 3 = COMCEN SPEED into $92B4 indexed 0 own / 1 opponent. Kind 2 is accepted by the handler (it falls into the >= 2 branch) but nothing sends it.",
  "destroyUnit ($5459) kills by applying 52 points of damage through sub_3B52 - two more than the 50-point energy maximum - so the normal explosion, scoring and redraw path runs; only then is the slot freed with $FF. COMCENs ($31/$63) are refused outright, so SELF DESTRUCT can never end the game by knockout."
 ]
}