{
 "chunk": "game_main_0800_4987",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "4987",
   "name": "averageGroupMoveVector",
   "summary": "Untraced code (shown as .byte). Sums, over every live unit whose F708 group bits (and #$3C) equal zp_75 and whose bit 6 (in group) is set, the signed vector destination - position (unitDestX & $7F - unitX, unitDestY & $7F - unitY) as two 16-bit totals and divides each by the member count with divideSigned16by8 ($FCEC). Helper of followGroupMovement.",
   "inputs": "zp_75 = group id bits (value of F708 & $3C); unit arrays F640/F6A4/F708/F76C/F7D0/F834",
   "outputs": "A = member count (Z=1 when the group is empty), X = average dx, Y = average dy; zp_18-zp_1C clobbered; self-modifies the sign-extension operands at $49C6/$49E3",
   "confidence": "high"
  },
  {
   "addr": "4A02",
   "name": "followGroupMovement",
   "summary": "Untraced code. Gives unit Y the group's average movement: if averageGroupMoveVector returns a non-zero vector the unit's destination becomes its position plus that vector, otherwise its destination becomes its own position (stand still); the destination is applied through the set-destination routine at $34D5 (itself mis-typed as data). Called by joinUnitToGroup so a unit that joins a moving group starts moving with it.",
   "inputs": "Y = unit index, zp_75 = group id bits",
   "outputs": "zp_74/zp_75 = new destination, unitDestX/Y of the unit updated via $34D5; zp_18/zp_19/zp_1D clobbered",
   "confidence": "high"
  },
  {
   "addr": "4A35",
   "name": "sendGroupMoveToCursor",
   "summary": "UI action (called from $2218 when a group is selected, D_9209 >= 2): queues command $81 (move group) with the selected unit and the cursor cell (viewLeftCol+3, viewTopRow+2), then calls $34A4 to set the local at-destination flag of the unit. Always returns C=0.",
   "inputs": "D_90F8 selected unit, zp_A5/zp_A6 view origin",
   "outputs": "zp_73/zp_74/zp_75 = unit, x, y; 4 bytes queued in the outgoing command ring ($560A); C=0",
   "confidence": "high"
  },
  {
   "addr": "4A52",
   "name": "moveGroupKeepingFormation",
   "summary": "Untraced code: tail of the $81 command handler ($501A -> here). Computes the offset between the leader's current destination and the ordered cell (x,y) and moves every live member of the leader's group (same F708 group bits, bit 6 set) by that offset: member destination = member destination - offset, applied through $34D5. The formation of the group is therefore preserved.",
   "inputs": "zp_73 = leader unit, zp_74/zp_75 = ordered x/y (already mirrored for remote commands)",
   "outputs": "unitDestX/Y of all members updated; zp_73 overwritten with the group key, zp_4A used as loop index; self-modifies the operands at $4A8E/$4A98",
   "confidence": "high"
  },
  {
   "addr": "4AA3",
   "name": "addSelectedUnitToGroup",
   "summary": "UI action (GROUP menu item ADDED, called from $61C6 and $6483): checks with isPointWithinGroupRange that the selected unit is within 8 cells of all current members of group D_9206; if not, shows 'TOO FAR!' and returns C=1. Otherwise sets the local group bits in unitLocalFlags (FB54 = (old & $83) | group | $40) and queues command $88 unit group.",
   "inputs": "D_9206 selected group bits, D_90F8 selected unit",
   "outputs": "C=0 added (command $88 queued, FB54 updated), C=1 too far (message shown)",
   "confidence": "high"
  },
  {
   "addr": "4AD4",
   "name": "joinUnitToGroup",
   "summary": "Untraced code: tail of the $88 command handler ($5185 -> here). Makes unit zp_74 follow the movement of group zp_75 (followGroupMovement), then writes the group id and the in-group bit into unitGroupByte (F708 = (old & 3) | group | $40) and mirrors them into unitLocalFlags with syncLocalGroupFlags.",
   "inputs": "zp_74 = unit, zp_75 = group id bits",
   "outputs": "F708/FB54 of the unit updated, destination updated; self-modifies the immediates at $4AE2/$4AE9",
   "confidence": "high"
  },
  {
   "addr": "4AF2",
   "name": "removeSelectedUnitFromGroup",
   "summary": "UI action (GROUP menu item REMOVED, called from $64B1): clears the in-group bit (bit 6) of the selected unit's unitLocalFlags immediately and queues command $89 unit so both machines remove it from unitGroupByte when the command executes.",
   "inputs": "D_90F8 selected unit",
   "outputs": "FB54 bit 6 cleared, zp_73 = unit, 2 bytes queued via $561F",
   "confidence": "high"
  },
  {
   "addr": "4B04",
   "name": "isPointWithinGroupRange",
   "summary": "Tests whether a point may join / be the target of group A: computes the group's bounding box with computeGroupBounds, then requires the point to be less than 9 cells from each of the four box edges (so the group never spans more than 8 cells). Y < $80 selects unit Y's position and the members' positions; Y = $80 selects the order target (D_91D8/D_91D9) and the members' destinations. An empty group always fits.",
   "inputs": "A = group id bits, Y = unit index or $80",
   "outputs": "C=0 fits, C=1 too far; zp_18-zp_1D clobbered; self-modifies the ldy operand at $4B0F",
   "confidence": "high"
  },
  {
   "addr": "4B5E",
   "name": "computeGroupBounds",
   "summary": "Scans all 100 units and, for live units whose unitLocalFlags group bits (FB54 & $7C) equal A | $40, accumulates the member count and the bounding box of either their positions (zp_1D bit 7 clear) or their destinations (bit 7 set, F7D0/F834 & $7F). Also called by $30C5 (group box display) with Y = $80.",
   "inputs": "A = group id bits, Y = mode flag (bit 7 = use destinations)",
   "outputs": "A = zp_1A = member count, X = zp_18 = min x, Y = zp_19 = min y, zp_1B = max x, zp_1C = max y; self-modifies the cmp operand at $4B80",
   "confidence": "high"
  },
  {
   "addr": "4BBF",
   "name": "showTooFarMessage",
   "summary": "Selects and clears the 14-column message window at column 25 ($261C), plays sound 3 and prints 'TOO FAR!' ($CB31 in the $C000 overlay) on window row $10.",
   "inputs": "-",
   "outputs": "text window changed, message printed",
   "confidence": "high"
  },
  {
   "addr": "4BD3",
   "name": "orderGroupNewPosition",
   "summary": "UI action (GROUP menu item NEW POSITION, called from $64AB): highlights the selected unit (D_9204 = unit, D_9205 = $FF), redraws the view ($26C1, $2185) and, if input is allowed ($C451), checks that the target cell D_91D8/D_91D9 lies within 8 cells of the group's destinations. If so it queues command $98 unit x y, sets the local at-destination flag ($34A4), counts consecutive orders to the same group in D_929D/D_929E and prints 'RE-POSITIONED' ($CB99) centred in colour 4; otherwise shows 'TOO FAR!'. Ends by setting zp_BB = $80 and refreshing the sprites ($2FCF, $3306).",
   "inputs": "D_90F8 selected unit, D_9206 group bits, D_91D8/D_91D9 target cell",
   "outputs": "command $98 queued or message shown; D_9204/D_9205, D_929D/D_929E, zp_BB, zp_3D/zp_42 updated; self-modifies the string pointer operands at $4C46/$4C48",
   "confidence": "medium"
  },
  {
   "addr": "4C59",
   "name": "setGroupMemberDestination",
   "summary": "Untraced code: tail of the $98 command handler ($5470 -> here). If the unit's group bits equal the group remembered in D_929D it decrements the pending-order counter D_929E, then sets the unit's destination to (zp_74, zp_75) through $34D5. Unlike command $80 the unit stays in its group.",
   "inputs": "zp_73 = unit, zp_74/zp_75 = x/y (mirrored for remote commands)",
   "outputs": "unitDestX/Y updated, D_929E possibly decremented",
   "confidence": "high"
  },
  {
   "addr": "4C6B",
   "name": "syncLocalGroupFlags",
   "summary": "Copies the authoritative group bits of unit X from unitGroupByte (F708 & $7C: group id and in-group bit) into unitLocalFlags (FB54, keeping bits 0,1 and 7). Called after every command that changes group membership ($34C6, $4AD4, $51A8) and when a unit is created ($3DFA).",
   "inputs": "X = unit index",
   "outputs": "FB54[X] updated; self-modifies the ora operand at $4C79",
   "confidence": "high"
  },
  {
   "addr": "4C84",
   "name": "rotateGroupFormation",
   "summary": "Untraced code: tail of the $A3 command handler ($5596 -> here). Rotates the formation of the leader's group about the leader's destination: for each member the offset (dx,dy) from the leader's destination is transformed according to mode zp_75 using formationRotateRules (0: (dy,-dx), 1: (-dy,dx), 2: (-dx,-dy)) and the new destination leader + offset is applied with $34D5. During the setup phase (zp_B1 != 0) the row is clamped to the issuing player's half of the map (rows 0-19 for own commands, 20-39 for remote ones).",
   "inputs": "zp_74 = leader unit, zp_75 = rotation mode 0-2, zp_76 bit 7 = remote command, zp_B1 = game phase",
   "outputs": "unitDestX/Y of all members updated; heavy self-modification of immediates ($4CC4, $4CD0, $4CDA, $4CDC, $4CE5, $4CEF, $4CF1, $4CFA, $4CFE, $4D05)",
   "confidence": "medium"
  },
  {
   "addr": "4D9F",
   "name": "getCommandLength",
   "summary": "Looks up the total length (command byte plus parameters) of command A in commandLengthTable and stores it in currentCommandLength. Used by the packet merger, the dispatcher, the outgoing packet builder ($565C) and the comm module ($EA4C/$EA92).",
   "inputs": "A = command byte ($80-$A6)",
   "outputs": "A = D_92EE = length, X = command index (A & $3F)",
   "confidence": "high"
  },
  {
   "addr": "4DA9",
   "name": "serviceCommandExchange",
   "summary": "Per-frame entry of the lock-step command exchange (called from the main loops of every screen and from the overlays). Services the message system ($C4EF) and then, unless the throttle timer D_9163 is still running (ignored during film playback), falls into processCommandExchange.",
   "inputs": "D_0B9B bit 7 film playback, D_9163 throttle",
   "outputs": "see processCommandExchange",
   "confidence": "high"
  },
  {
   "addr": "4DB7",
   "name": "processCommandExchange",
   "summary": "Film playback (D_0B9B bit 7): reads film records whose time stamp (sub-tick D_923C and clock D_91CB) has come, copies their command bytes into turnCommandBuffer and executes them; record $FE stops, $FF runs the end-of-film handler ($59D0); afterwards keeps the link alive via L_56B1 when commSessionState is non-zero. Live play: when commSessionState bit 7 is set only the link keep-alive runs; otherwise, if the game is not ending ($C86D) and the link is active, waits for D_E01D bit 6 (packet exchange complete, polling $5A14 for timeouts), then stops sound $15, reloads the throttle (10) and timeout (0), merges own and received packets so that player 0's commands always come first (mergeOutgoingPacket / mergeIncomingPacket, D_92EC = start of player 1's part) and executes them with executeTurnCommands.",
   "inputs": "D_0B9B, D_0B9C, D_0BA8, D_E03B, D_E01D, D_0B9F, film pointer zp_BC/zp_BD, D_923C, D_91CB",
   "outputs": "turnCommandBuffer filled and executed, D_92EB/D_92EC/D_9163/D_9223/zp_69 set; self-modifies $4DE9 (time-stamp byte), $4DF4 (last clock) and $4E20 (first command index)",
   "confidence": "high"
  },
  {
   "addr": "4E91",
   "name": "executeTurnCommands",
   "summary": "Executes the merged command buffer: walks turnCommandBuffer from 0 to turnCommandCount with turnCommandCursor, sets currentCommandOrigin/zp_76 to $80 for commands issued by the remote player (toggles at player1CommandStart, initial value from localPlayerIndex), sets bit 0 of D_92C2, decrements pendingOutgoingCmdCount for own commands and dispatches each command byte (bit 7 set) through commandHandlerTable with the self-modified JSR at $4EEE; bytes with bit 7 clear are skipped by their length (inferred: cancelled commands, e.g. the $8E handler overwrites its own command byte with $0E at $534F). In live play it then records the turn to the film ($578A), acknowledges the packet (sub_E000 X=2) and builds/sends the next outgoing packet ($562B).",
   "inputs": "D_908F buffer, D_92EB count, D_92EC player-1 start, D_0B9F local player, D_0B9B film flag",
   "outputs": "game state changed by the handlers; D_90B7, D_92ED, zp_76, D_92C2, D_929F updated; next packet sent",
   "confidence": "high"
  },
  {
   "addr": "4F05",
   "name": "mergeIncomingPacket",
   "summary": "Appends the received packet (D_E020, D_E01E bytes) to turnCommandBuffer at index zp_69 with zp_76 = $80 (remote). Commands are copied whole using advancePacketCursor; $97 additionally clears D_92F9, $92 sets D_92F9 = $80, while $8D (opponent cursor/drone report) is handled at once by $52C8 and $8C (abort/disconnect + reason) by $523E without being copied.",
   "inputs": "D_E01E, D_E020-D_E026, zp_69",
   "outputs": "D_908F[zp_69..] filled, zp_69 advanced, zp_6A scratch, zp_76 = $80, D_92F9 possibly changed",
   "confidence": "high"
  },
  {
   "addr": "4F5E",
   "name": "advancePacketCursor",
   "summary": "Helper of the packet mergers: preserves the command byte in A, looks up its length (getCommandLength) and sets zp_6A = Y + length, i.e. the index of the next command in the packet.",
   "inputs": "A = command byte, Y = its index in the packet",
   "outputs": "A preserved, zp_6A = next index, D_92EE = length",
   "confidence": "high"
  },
  {
   "addr": "4F6B",
   "name": "mergeOutgoingPacket",
   "summary": "Appends the local machine's own packet (D_E027, D_E01F bytes, the copy of what was just sent) to turnCommandBuffer at zp_69 with zp_76 = 0 (own). $8D is not copied: it re-centres the view origin zp_A5/zp_A6 on D_92F6/D_92F7 and decrements pendingOutgoingCmdCount; $8C is executed immediately through $523E; $92 sets D_92F9 = $80 and is copied like all other commands.",
   "inputs": "D_E01F, D_E027-D_E02D, zp_69",
   "outputs": "D_908F filled, zp_69 advanced, zp_6A scratch, zp_76 = 0, zp_A5/zp_A6 or D_929F possibly changed",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "18",
   "scope": "zp",
   "name": "groupSumDxLo",
   "meaning": "in this chunk: low byte of the summed dx in averageGroupMoveVector, min x in computeGroupBounds, average dx in followGroupMovement (general scratch elsewhere)",
   "confidence": "high"
  },
  {
   "addr": "19",
   "scope": "zp",
   "name": "groupSumDxHi",
   "meaning": "in this chunk: high byte of the summed dx, min y of the group bounds, average dy in followGroupMovement",
   "confidence": "high"
  },
  {
   "addr": "1A",
   "scope": "zp",
   "name": "groupSumDyLo",
   "meaning": "in this chunk: low byte of the summed dy (averageGroupMoveVector) or the member count (computeGroupBounds)",
   "confidence": "high"
  },
  {
   "addr": "1B",
   "scope": "zp",
   "name": "groupSumDyHi",
   "meaning": "in this chunk: high byte of the summed dy or max x of the group bounds",
   "confidence": "high"
  },
  {
   "addr": "1C",
   "scope": "zp",
   "name": "groupMemberCount",
   "meaning": "in this chunk: member count (averageGroupMoveVector) or max y of the group bounds",
   "confidence": "high"
  },
  {
   "addr": "1D",
   "scope": "zp",
   "name": "groupBoundsMode",
   "meaning": "bit 7 set = computeGroupBounds uses destinations instead of positions; followGroupMovement keeps the unit index here",
   "confidence": "high"
  },
  {
   "addr": "4A",
   "scope": "zp",
   "name": "groupLoopIndex",
   "meaning": "unit loop counter (99..0) in moveGroupKeepingFormation",
   "confidence": "high"
  },
  {
   "addr": "69",
   "scope": "zp",
   "name": "mergedCommandLength",
   "meaning": "fill index of turnCommandBuffer while the own and received packets are merged; becomes turnCommandCount / player1CommandStart",
   "confidence": "high"
  },
  {
   "addr": "6A",
   "scope": "zp",
   "name": "packetReadIndex",
   "meaning": "index of the next command inside the packet being merged (set by advancePacketCursor)",
   "confidence": "high"
  },
  {
   "addr": "73",
   "scope": "zp",
   "name": "cmdParam0",
   "meaning": "first parameter of the command being sent or executed (normally the unit index)",
   "confidence": "high"
  },
  {
   "addr": "74",
   "scope": "zp",
   "name": "cmdParam1",
   "meaning": "second command parameter (normally x / map column, or the unit for $88/$A3)",
   "confidence": "high"
  },
  {
   "addr": "75",
   "scope": "zp",
   "name": "cmdParam2",
   "meaning": "third command parameter (normally y / map row, or the group bits / rotation mode)",
   "confidence": "high"
  },
  {
   "addr": "76",
   "scope": "zp",
   "name": "isRemoteCommand",
   "meaning": "bit 7 set while a command issued by the remote player is merged or executed (its coordinates are mirrored with 39 - v by $4FE4); 0 for own commands",
   "confidence": "high"
  },
  {
   "addr": "A5",
   "scope": "zp",
   "name": "viewLeftCol",
   "meaning": "map column of the left edge of the battlefield view; cursor column = zp_A5 + 3",
   "confidence": "high"
  },
  {
   "addr": "A6",
   "scope": "zp",
   "name": "viewTopRow",
   "meaning": "map row of the top edge of the battlefield view; cursor row = zp_A6 + 2",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "0 during the battle; non-zero during setup, where rotateGroupFormation clamps rows to the player's half of the map",
   "confidence": "high"
  },
  {
   "addr": "BB",
   "scope": "zp",
   "name": "cursorOrderMode",
   "meaning": "cursor/order state flag read by the battlefield input code ($218F, $6474); orderGroupNewPosition sets it to $80 (group order pending / group mode)",
   "confidence": "low"
  },
  {
   "addr": "3D",
   "scope": "zp",
   "name": "textColour",
   "meaning": "text colour byte for the $C000 print routines; set to 4 together with zp_42 for the 'RE-POSITIONED' message",
   "confidence": "high"
  },
  {
   "addr": "42",
   "scope": "zp",
   "name": "messageTextColour",
   "meaning": "text colour used while a message is displayed (set to 4 in orderGroupNewPosition)",
   "confidence": "medium"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "filmPlaybackFlag",
   "meaning": "bit 7 set: commands come from the recorded film instead of the comm link (WATCH FILM); also suppresses recording and sending",
   "confidence": "high"
  },
  {
   "addr": "0B9C",
   "scope": "abs",
   "name": "commSessionState",
   "meaning": "0 = in-game exchange; bit 7 set = linked but outside the battle, only the keep-alive at L_56B1 runs",
   "confidence": "medium"
  },
  {
   "addr": "0B9F",
   "scope": "abs",
   "name": "localPlayerIndex",
   "meaning": "0 or 1: decides the merge order (player 0's commands first) and the initial value of currentCommandOrigin",
   "confidence": "high"
  },
  {
   "addr": "0BA8",
   "scope": "abs",
   "name": "noGameInProgress",
   "meaning": "non-zero when no game is running; film playback then only keeps the link alive",
   "confidence": "high"
  },
  {
   "addr": "0C8A",
   "scope": "abs",
   "name": "comcenUnitBySide",
   "meaning": "2 bytes {$31,$63} = unit index of each side's COMCEN (49 and 99)",
   "confidence": "high"
  },
  {
   "addr": "4C46",
   "scope": "abs",
   "name": "groupMessagePtrLo",
   "meaning": "self-modified lda operand: low byte of the string printed by orderGroupNewPosition ($99 -> $CB99 'RE-POSITIONED')",
   "confidence": "high"
  },
  {
   "addr": "4C48",
   "scope": "abs",
   "name": "groupMessagePtrHi",
   "meaning": "self-modified ldy operand: high byte of that string pointer ($CB)",
   "confidence": "high"
  },
  {
   "addr": "4B0F",
   "scope": "abs",
   "name": "groupCheckSubject",
   "meaning": "self-modified ldy operand: unit index or $80 (order target) tested by isPointWithinGroupRange",
   "confidence": "high"
  },
  {
   "addr": "4B80",
   "scope": "abs",
   "name": "groupKeyOperand",
   "meaning": "self-modified cmp operand: group id bits | $40 matched against FB54 & $7C in computeGroupBounds",
   "confidence": "high"
  },
  {
   "addr": "4C79",
   "scope": "abs",
   "name": "groupBitsOperand",
   "meaning": "self-modified ora operand: F708 & $7C copied into FB54 by syncLocalGroupFlags",
   "confidence": "high"
  },
  {
   "addr": "4DE9",
   "scope": "abs",
   "name": "filmTimeStampByte",
   "meaning": "self-modified lda operand: second byte of the film record being examined (low nibble sub-tick, high nibble clock delta)",
   "confidence": "high"
  },
  {
   "addr": "4DF4",
   "scope": "abs",
   "name": "filmLastClock",
   "meaning": "self-modified adc operand: clock value (D_91CB) of the last film record played; base for the 1-byte delta time stamps (also written by the overlay at $7914 when playback starts)",
   "confidence": "high"
  },
  {
   "addr": "4E20",
   "scope": "abs",
   "name": "filmCommandStart",
   "meaning": "self-modified ldy operand: offset of the first command byte inside the current film record",
   "confidence": "high"
  },
  {
   "addr": "4EEF",
   "scope": "abs",
   "name": "commandHandlerVector",
   "meaning": "2-byte operand of the JSR at $4EEE, loaded from commandHandlerTable for every executed command",
   "confidence": "high"
  },
  {
   "addr": "67C8",
   "scope": "abs",
   "name": "currentSoundId",
   "meaning": "id of the sound currently playing; sound $15 is stopped when a packet exchange completes",
   "confidence": "low"
  },
  {
   "addr": "908F",
   "scope": "abs",
   "name": "turnCommandBuffer",
   "meaning": "40-byte buffer ($908F-$90B6) holding the merged commands of one exchange: player 0's commands first, then player 1's; also filled from film records during playback",
   "confidence": "high"
  },
  {
   "addr": "90B7",
   "scope": "abs",
   "name": "turnCommandCursor",
   "meaning": "index of the command currently executed in turnCommandBuffer; the parameter fetchers at $4FC4/$4FD0/$4FF7 advance it",
   "confidence": "high"
  },
  {
   "addr": "90F8",
   "scope": "abs",
   "name": "selectedUnit",
   "meaning": "index of the unit the player has selected (subject of the group commands)",
   "confidence": "high"
  },
  {
   "addr": "9163",
   "scope": "abs",
   "name": "exchangeThrottleTimer",
   "meaning": "reloaded with 10 after each packet exchange and decremented every 4 frames by the IRQ; serviceCommandExchange does nothing while it is non-zero",
   "confidence": "high"
  },
  {
   "addr": "91CB",
   "scope": "abs",
   "name": "gameClock",
   "meaning": "game clock value that counts down during the battle (game ends on time when it reaches 0, $4606); film records are stamped with it",
   "confidence": "high"
  },
  {
   "addr": "91D8",
   "scope": "abs",
   "name": "orderTargetX",
   "meaning": "map column of the cell chosen for a unit/group order (written at $21FD)",
   "confidence": "medium"
  },
  {
   "addr": "91D9",
   "scope": "abs",
   "name": "orderTargetY",
   "meaning": "map row of the cell chosen for a unit/group order (written at $2205)",
   "confidence": "medium"
  },
  {
   "addr": "9204",
   "scope": "abs",
   "name": "highlightedUnit",
   "meaning": "unit drawn inverted on the map (set to the selected unit by orderGroupNewPosition)",
   "confidence": "high"
  },
  {
   "addr": "9205",
   "scope": "abs",
   "name": "highlightedGroupId",
   "meaning": "group whose members are drawn inverted; $FF = none",
   "confidence": "high"
  },
  {
   "addr": "9206",
   "scope": "abs",
   "name": "selectedGroupBits",
   "meaning": "group id of the currently selected group in F708/FB54 format (bits 2-5, i.e. id * 4)",
   "confidence": "high"
  },
  {
   "addr": "9223",
   "scope": "abs",
   "name": "exchangeTimeoutCounter",
   "meaning": "cleared to 0 when a packet arrives; counts up every 8 frames while negative (IRQ) and is evaluated by $5A14 (link lost at $FF)",
   "confidence": "medium"
  },
  {
   "addr": "923C",
   "scope": "abs",
   "name": "gameClockSubTick",
   "meaning": "sub-division (0-15) of the game clock unit, computed at $41EB; film records are due when both it and gameClock match",
   "confidence": "high"
  },
  {
   "addr": "929D",
   "scope": "abs",
   "name": "lastOrderedGroupBits",
   "meaning": "group bits of the group that last received a NEW POSITION ($98) order",
   "confidence": "high"
  },
  {
   "addr": "929E",
   "scope": "abs",
   "name": "pendingGroupOrderCount",
   "meaning": "number of $98 orders to lastOrderedGroupBits that have been sent but not yet executed (incremented on send, decremented by setGroupMemberDestination)",
   "confidence": "high"
  },
  {
   "addr": "929F",
   "scope": "abs",
   "name": "pendingOutgoingCmdCount",
   "meaning": "number of own commands queued but not yet executed; decremented when an own command (or own $8D) is processed",
   "confidence": "high"
  },
  {
   "addr": "92C2",
   "scope": "abs",
   "name": "turnActivityFlags",
   "meaning": "bit 0 set whenever a command is executed (also set to 1 at $44D5); tested by the clock code at $45F4",
   "confidence": "medium"
  },
  {
   "addr": "92EB",
   "scope": "abs",
   "name": "turnCommandCount",
   "meaning": "number of valid bytes in turnCommandBuffer for the current exchange (temporarily the raw film header byte during playback)",
   "confidence": "high"
  },
  {
   "addr": "92EC",
   "scope": "abs",
   "name": "player1CommandStart",
   "meaning": "index in turnCommandBuffer where player 1's commands begin; stored in the high nibble of the film record header",
   "confidence": "high"
  },
  {
   "addr": "92ED",
   "scope": "abs",
   "name": "currentCommandOrigin",
   "meaning": "$80 while commands of the remote player are executed, 0 for own commands; copied to zp_76",
   "confidence": "high"
  },
  {
   "addr": "92EE",
   "scope": "abs",
   "name": "currentCommandLength",
   "meaning": "length of the last command looked up by getCommandLength; used as the byte copy counter",
   "confidence": "high"
  },
  {
   "addr": "92F6",
   "scope": "abs",
   "name": "reportedCursorX",
   "meaning": "x of the position carried by the $8D report command (set from the opponent's $8D by $52C8; own $8D re-centres the view on it)",
   "confidence": "low"
  },
  {
   "addr": "92F7",
   "scope": "abs",
   "name": "reportedCursorY",
   "meaning": "y of the position carried by the $8D report command",
   "confidence": "low"
  },
  {
   "addr": "92F9",
   "scope": "abs",
   "name": "strikeStateFlags",
   "meaning": "state of the special weapon/strike sequence: $80 set by command $92, cleared by $97, $40 set by $8E, bit 4 set by an own $92",
   "confidence": "low"
  },
  {
   "addr": "E01D",
   "scope": "abs",
   "name": "commExchangeState",
   "meaning": "comm module exchange state: bit 6 = a complete packet has been received and the exchange may be processed; sub_E000 X=1 marks the outgoing packet pending, X=2 clears it after processing",
   "confidence": "medium"
  },
  {
   "addr": "E01E",
   "scope": "abs",
   "name": "incomingPacketLength",
   "meaning": "number of command bytes received in incomingPacketBuffer",
   "confidence": "high"
  },
  {
   "addr": "E01F",
   "scope": "abs",
   "name": "outgoingPacketLength",
   "meaning": "number of command bytes in outgoingPacketBuffer (set by $562B, max 4)",
   "confidence": "high"
  },
  {
   "addr": "E020",
   "scope": "abs",
   "name": "incomingPacketBuffer",
   "meaning": "7-byte buffer of the command bytes received from the opponent in the last exchange",
   "confidence": "high"
  },
  {
   "addr": "E027",
   "scope": "abs",
   "name": "outgoingPacketBuffer",
   "meaning": "7-byte buffer of the command bytes sent in the last exchange (merged back as the own half of the turn)",
   "confidence": "high"
  },
  {
   "addr": "E03B",
   "scope": "abs",
   "name": "commLinkActive",
   "meaning": "non-zero while the comm link is open; the exchange is skipped otherwise",
   "confidence": "high"
  },
  {
   "addr": "F640",
   "scope": "abs",
   "name": "unitX",
   "meaning": "unit array (100 entries): map column",
   "confidence": "high"
  },
  {
   "addr": "F6A4",
   "scope": "abs",
   "name": "unitY",
   "meaning": "unit array: map row",
   "confidence": "high"
  },
  {
   "addr": "F708",
   "scope": "abs",
   "name": "unitGroupByte",
   "meaning": "unit array: bits 0-1 animation frame, bits 2-5 group id, bit 6 = member of a group. Authoritative (changed only by executed commands, so identical on both machines)",
   "confidence": "high"
  },
  {
   "addr": "F76C",
   "scope": "abs",
   "name": "unitType",
   "meaning": "unit array: bits 0-2 unit type, bit 7 set = unit removed/dead (skipped by all group loops)",
   "confidence": "high"
  },
  {
   "addr": "F7D0",
   "scope": "abs",
   "name": "unitDestX",
   "meaning": "unit array: destination column in bits 0-6, bit 7 set = stationary (destination equals position)",
   "confidence": "high"
  },
  {
   "addr": "F834",
   "scope": "abs",
   "name": "unitDestY",
   "meaning": "unit array: destination row in bits 0-6 (bit 7 used as a flag elsewhere)",
   "confidence": "high"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitLocalFlags",
   "meaning": "unit array: local mirror of the group bits (bits 2-5 id, bit 6 member) updated immediately by the UI and resynced from F708 by syncLocalGroupFlags; bit 7 = at destination ($34A4), bits 0-1 other flags",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "4C7E",
   "length": 6,
   "type": "byteTable",
   "name": "formationRotateRules",
   "description": "Two 3-byte rule sets indexed by the rotation mode of command $A3: $4C7E for dx, $4C81 for dy. Bit 7 of a rule = negate the component, bit 0 = store it as the new dy (zp_75) instead of dx (zp_74). Values {$81,$01,$80} and {$00,$80,$81} give the rotations (dy,-dx), (-dy,dx), (-dx,-dy)."
  },
  {
   "addr": "4D2A",
   "length": 78,
   "type": "wordTable",
   "name": "commandHandlerTable",
   "description": "39 little-endian handler addresses for command bytes $80-$A6, read as D_4D2A,x / D_4D2B,x with x = (cmd & $3F) * 2 and JSRed through $4EEE. Entry $8D is $FFFF because that command is consumed by the packet mergers. Handlers: $80 5011, $81 501A, $82 5023, $83 5098, $84 504B, $85 50D2, $86 515D, $87 516B, $88 5185, $89 519B, $8A 51AB, $8B 51EE, $8C 523E, $8E 5336, $8F 537B, $90 53C0, $91 542E, $92 538B, $93 5070, $94 50AA, $95 5062, $96 50BE, $97 53A3, $98 5470, $99 56B1, $9A 5479, $9B 549E, $9C 54E1, $9D 5511, $9E 520C, $9F 5548, $A0 554C, $A1 556D, $A2 5571, $A3 5596, $A4 559C, $A5 55A5, $A6 51EA."
  },
  {
   "addr": "4D78",
   "length": 39,
   "type": "byteTable",
   "name": "commandLengthTable",
   "description": "Total length in bytes (command byte included, 1-4) of each command $80-$A6, indexed by cmd & $3F; read by getCommandLength."
  }
 ],
 "misclassified": [
  {
   "addr": "4987",
   "length": 174,
   "actual": "code",
   "evidence": "Decodes cleanly as two routines (averageGroupMoveVector $4987-$4A01, followGroupMovement $4A02-$4A34) ending in RTS; $4A02 is the JSR target of $4ADE and $4A04 calls $4987; uses divideSigned16by8 ($FCEC) and the unit arrays."
  },
  {
   "addr": "4A52",
   "length": 81,
   "actual": "code",
   "evidence": "Jump target of the $81 handler ($5020: jmp $4A52); clean code that loops over the 100 units and calls $34D5, ends in RTS at $4AA2."
  },
  {
   "addr": "4AD4",
   "length": 30,
   "actual": "code",
   "evidence": "Jump target of the $88 handler ($5198: jmp $4AD4); self-modifying code that calls $4A02 and jumps to sub_4C6B."
  },
  {
   "addr": "4C59",
   "length": 18,
   "actual": "code",
   "evidence": "Jump target of the $98 handler ($5476: jmp $4C59); compares FB54 group bits with D_929D and jumps to $34D5."
  },
  {
   "addr": "4C84",
   "length": 166,
   "actual": "code",
   "evidence": "Jump target of the $A3 handler ($5599: jmp $4C84); self-modifying formation rotation loop ending in RTS at $4D29. Only $4C7E-$4C83 (6 bytes, formationRotateRules) of the .byte block is data."
  }
 ],
 "insights": [
  "Command protocol: an order is a byte string consisting of a command byte $80-$A6 (bit 7 set) followed by 0-3 parameters; the total length comes from commandLengthTable ($4D78) and the handler from commandHandlerTable ($4D2A). Parameters are conventionally (unit, x, y) and are fetched by the untraced helpers $4FC4 (3 params -> zp_73/74/75), $4FD0 (2 params -> zp_74/75) and $4FF7 (1 param -> zp_73), all of which advance turnCommandCursor ($90B7). Remote commands (zp_76 bit 7) have their x/y mirrored by $4FE4 (39 - v via $3FA3) because each player sees the map from his own side; the setup phase puts own units in rows 0-19 and the opponent's in rows 20-39 of local coordinates.",
  "Lock-step exchange: every 'turn' both machines swap one packet of at most 4 command bytes (whole commands only, built by $562B from the 40-byte outgoing ring at $9065 into $E027/$E01F and sent with sub_E000 X=1; the opponent's packet arrives in $E020/$E01E and D_E01D bit 6 signals completion). processCommandExchange then builds turnCommandBuffer ($908F) with player 0's commands first and player 1's second (player1CommandStart $92EC), executes them identically on both machines, records the turn into the film ($578A), acknowledges with sub_E000 X=2 and immediately sends the next packet. A throttle of 10 x 4 frames ($9163) and a timeout counter ($9223, evaluated by $5A14) frame the exchange. Practice mode must rely on the comm module / $EC00-$EFFF sub-overlays producing the 'received' packet locally, since the exchange code is the only route by which commands take effect.",
  "Film (WATCH FILM) format, as consumed by processCommandExchange and written by $578A/$57BE: per turn [header = player1CommandStart << 4 | length][time stamp][command bytes]; the time stamp is one byte (clockDelta << 4 | subTick) when the game clock ($91CB, counting down) moved by 0-14 since the previous record, else two bytes (subTick, clock). Record byte $FE or $FF marks the end of the film (bit 0 = D_0BA8 & 1; $FF triggers the end-of-film score display $59D0). The film lives in RAM under the I/O area from $D034 (pointer zp_BC/zp_BD, end D_92F1/D_92F2) and is accessed with $01 = $34 by $5874 (read) / $5893 (write) with interrupts and CIA2 IRQs masked. $8D reports never reach the film because the mergers consume them.",
  "Group model: a unit's group is bits 2-5 of unitGroupByte ($F708) with bit 6 = member; unitLocalFlags ($FB54) holds a second copy of the same bits that the UI changes immediately (ADDED/REMOVED) and that syncLocalGroupFlags ($4C6B) re-copies from F708 whenever a command changes membership - two sources of truth by design (local preview vs. lock-step state). selectedGroupBits ($9206) stores the id in the same bits-2-5 format. A group may never span more than 8 cells: isPointWithinGroupRange ($4B04) rejects a joining unit or a NEW POSITION target that is 9 or more cells from any edge of the group's bounding box ('TOO FAR!', string $CB31). GROUP menu strings in the $C000 overlay: 'ADDED' $CB3B, 'REMOVED' $CB40, 'NOT IN GROUP' $CB8D, 'RE-POSITIONED' $CB99, 'NEW POSITION' $CBA6.",
  "Group commands implemented in this chunk: $81 unit x y = move the whole group so that the formation relative to the leader's destination is kept (moveGroupKeepingFormation); $88 unit group = join (joinUnitToGroup, the unit adopts the group's average movement vector); $89 unit = leave; $98 unit x y = move a member without leaving the group (setGroupMemberDestination, counted in D_929D/D_929E); $A3 unit mode = rotate the formation about the leader's destination by (dy,-dx), (-dy,dx) or (-dx,-dy) (rotateGroupFormation). Other handlers seen while tracing (medium confidence): $80 move unit and leave its group ($5011 -> $34C6), $8A place unit at x,y during setup ($51AB), $8C reason = abort/disconnect ($523E), $8D = cursor/drone position report (consumed by $52C8, shows a white sprite on screen 1), $90 x y = strike on a cell with D_92B0 strikes per side ($53C0), $91 = remove unit(s) ($542E), $9A unit amount = energy repair ($5479), $9B = halt a whole group ($549E), $9C = per-player option level ($54E1), $9D = choose side ($5511), $A4 = game type options -> $0BA3 ($559C), $A5 = points/per-player values ($55A5), $A6/$8B = scenario seed -> $0B95-$0B97 ($51EA/$51EE).",
  "Mis-typed code outside this chunk (reached only through commandHandlerTable or from these handlers, so the tracer missed it): $4FC4-$4FE3 and $4FF7-$5004 (parameter fetchers), $5011-$523D and $5336-$55EB (all command handlers except sub_523E/sub_52C8/sub_532F), and $34C6-$3567 (unit leaves group + set destination: $34D5 with Y = unit, zp_74/75 = x/y clamps to 0-39, sets F7D0/F834, flags bit 7 of F7D0 and FB54 when the destination equals the position, refreshes F898/F8FC low bits with the position and clears F960 bits 5-7 and F76C bit 6). All of them decode cleanly with tools/m6502.py.",
  "Self-modifying style: nearly every routine here patches immediate operands instead of using zero page (e.g. $49C6/$49E3 sign extension, $4B0F/$4B80 group keys, $4CD0-$4D05 in rotateGroupFormation, $4DE9/$4DF4/$4E20 film parsing, $4EEF handler vector). rotateGroupFormation even relies on the N flag left by 'ldx #rule' (bit 7 of the rule byte) to decide whether to negate, so the BPL at $4CDD/$4CF2 tests the rule, not the value.",
  "D_0C8A = {$31,$63} is the unit index of each side's COMCEN (49 and 99; units 0-49 belong to player 0, 50-99 to player 1); the code at $44EB-$44F1 and $5402-$5410 special-cases these two indices. The game clock $91CB counts down in units subdivided by $923C (0-15, computed at $41EB); the game ends on time when it reaches 0 ($4606 sets D_0BA8 = 8) and message $1D is shown when it equals D_92EF."
 ]
}