{
 "chunk": "game_ovl_EE00_T29_EE00",
 "unit": "game/ovl_EE00_T29",
 "routines": [
  {
   "addr": "EE00",
   "name": "flyTrainerDroneStep",
   "summary": "One flight step of the solo-trainer AI's drone. The communications module build 1 (T35) JSRs here from $E0A5 once per command exchange, but only while this track-29 s4-5 page is the resident $EE00 image (droneOverlayState $8977 bit 7 set). It returns at once when droneFlags $92F9 bit 7 says the drone has already arrived or been shot down; otherwise it decrements the drone's fuel counter $8973 and, when that goes negative, fabricates the 1-byte command $8F (drone released) in the received-packet buffer and returns. Any other case falls through into pickDroneAimPointAndDecide.",
   "inputs": "$92F9 droneFlags, $8973 aiDroneFuel, $8974/$8975 drone cell, $8976 target unit index, $8978 detonate threshold, map/unit arrays $F640/$F6A4/$F960",
   "outputs": "fabricated opponent packet in rxPacketBuffer $E020.. with rxPacketLength $E01E = 1 ($8F), 3 ($8E col row) or 4 ($8D heading col row); $8973 decremented; on the move path $896F, $8974, $8975 updated; zp_18,zp_19,zp_1C,zp_1D,zp_22,zp_24,zp_27,zp_28,zp_92-zp_95 clobbered",
   "confidence": "high"
  },
  {
   "addr": "EE15",
   "name": "pickDroneAimPointAndDecide",
   "summary": "Fall-through block (branch target of $EE08 only): decides between blowing the drone up here and flying one more cell. It measures the distance from the drone to its target unit ($8976); inside 6 cells and with both drone coordinates on the map it scores the 2x2 block under the drone with scoreTargetBlock ($E9A6) and detonates when the score reaches aiDroneDetonateThreshold $8978, or only 3/4 of it when the target unit is stunned ($F960 low nibble non-zero). Otherwise it scores the four 2x2 blocks that touch the target's cell, leaves the best corner in zp_95/zp_94, sets the line-stepper start from the drone position (clamping a negative coordinate to 0 and shifting the end point by the same amount) and calls getStepDirection ($E80E); a stepper that finds no direction (drone already on the aim cell) also detonates.",
   "inputs": "$8976, $F640/$F6A4 unit col/row, $8974/$8975 drone cell, $8978, $F960, zp_1C = 0 on entry",
   "outputs": "zp_1C best block score, zp_1D best player unit, zp_94/zp_95 aim cell, zp_92/zp_93 line start, Y = heading 0-7 from getStepDirection; falls into emitDroneDetonateCommand or maybeJinkDroneHeading",
   "confidence": "high"
  },
  {
   "addr": "EE2D",
   "name": "midCheckDroneOnMapEE2D",
   "summary": "NOT an entry point in this image: $EE2D is the middle of pickDroneAimPointAndDecide (the 'ldx $8974 / bmi' on-map test before scoreTargetBlock). The sub_ prefix is imported from the other code images that share these addresses (comm build 2 has a real routine at $EE2D).",
   "inputs": "-",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "EE55",
   "name": "midStoreDetonateThresholdEE55",
   "summary": "NOT an entry point in this image: $EE55 is the 'sta zp_18' that stores the 3/4 detonate threshold for a stunned target inside pickDroneAimPointAndDecide. The label comes from the other images of this page.",
   "inputs": "-",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "EE85",
   "name": "midScanDiagonalBlockEE85",
   "summary": "NOT an entry point in this image: $EE85 is the 'dey' inside the four-block scan around the target cell (scoring the block at col-1,row-1). The label is an artifact of the T35/T34 images of this page.",
   "inputs": "-",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "EEED",
   "name": "midCheckFuelForJinkEEED",
   "summary": "NOT an entry point in this image: $EEED is the 'lda $8973 / cmp #$0C' fuel test inside maybeJinkDroneHeading. The label is an artifact of the other images of this page.",
   "inputs": "-",
   "outputs": "-",
   "confidence": "high"
  },
  {
   "addr": "EEB6",
   "name": "emitDroneDetonateCommand",
   "summary": "Detonation path (branch target of $EE3F and $EE5B): writes command $8E plus the drone's current cell, mirrored with mirrorBlockCornerForOpponent ($EA38), into the fake received packet and sets its length to 3. The engine's cmdDroneDetonate ($5336) mirrors the coordinates back and plays the blast.",
   "inputs": "$8974/$8975 drone cell",
   "outputs": "$E020=$8E, $E021/$E022 = mirrored col/row, $E01E=3; X/Y clobbered; returns",
   "confidence": "high"
  },
  {
   "addr": "EED0",
   "name": "maybeJinkDroneHeading",
   "summary": "Flight path (branch target of $EEB4): stores the heading returned by getStepDirection in zp_19 and then, only when the human player still has missiles ($92B0), the drone's cell falls inside the radar window drawn around the human's comcen (mapCellToRadarGrid $7A13 with $9236 unchanged), the radar/drone screen is the one on show ($90FB = 1), the drone still has at least 12 fuel and is at least 3 cells away from its aim point, replaces the heading 2 times in 3 (nextGameRandom >= $56) with a random 0-7 value. Inferred purpose: an evasive weave while the player can watch the drone and shoot it down with a missile strike (confidence medium).",
   "inputs": "Y = heading from $E80E, $92B0 missilesLeft, $8974/$8975, $9236 comcenUnit, $90FB currentScreen, $8973, zp_94/zp_95 aim cell",
   "outputs": "zp_19 = desired heading 0-7; zp_22/zp_24/zp_27/zp_28 clobbered; falls into steerDroneAndEmitMove",
   "confidence": "medium"
  },
  {
   "addr": "EF0F",
   "name": "steerDroneAndEmitMove",
   "summary": "Turns the drone one step toward the desired heading and reports the move. The code is a copy of overlay B's steerDrone ($731E) with the AI's variables and without the turn sound: $896F is rotated by +1 or -1 (the short way round, then AND #$07) toward zp_19, converted through headingToDirectionTable ($7316), directionDeltaTable ($20F0) and diagonalDirectionTable ($1FB2) into dx/dy and added to $8974/$8975. It then emits command $8D with (heading+4)&7 - the heading as the mirrored map sees it - and the new cell mirrored by $EA38, length 4. Note that neither coordinate is clamped, so the drone can walk off the map.",
   "inputs": "zp_19 desired heading, $896F current heading, $8974/$8975 drone cell",
   "outputs": "$896F, $8974, $8975 updated; $E020=$8D, $E021=(heading+4)&7, $E022/$E023 mirrored col/row, $E01E=4; returns",
   "confidence": "high"
  },
  {
   "addr": "EF9E",
   "name": "droneDistanceToCell",
   "summary": "Helper called at $EE26 and $EEFC: copies the drone's cell $8974/$8975 into the distance routine's second point zp_27/zp_28 and tail-jumps to cellDistance ($E82A), which returns the integer distance between (zp_22,zp_24) and the drone in A and X. Y is preserved, which is what lets the caller keep the target unit index in Y across the call.",
   "inputs": "zp_22/zp_24 = cell to measure against, $8974/$8975 drone cell",
   "outputs": "A = X = distance 0-16, zp_27/zp_28 = drone cell; Y preserved",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "896F",
   "scope": "abs",
   "name": "aiDroneHeading",
   "meaning": "clockwise heading 0-7 (0 N, 1 NE, 2 E, ... 7 NW) of the trainer AI's drone; the AI's counterpart of the player's droneHeading $920B. Turned one step per exchange by steerDroneAndEmitMove.",
   "confidence": "high"
  },
  {
   "addr": "8973",
   "scope": "abs",
   "name": "aiDroneFuel",
   "meaning": "flight steps left for the AI's drone; set to $64 (100, the same as the player's drone fuel display) when the drone is launched by the module's own $EE00 image, decremented once per exchange here; going negative sends command $8F (drone released).",
   "confidence": "high"
  },
  {
   "addr": "8974",
   "scope": "abs",
   "name": "aiDroneCol",
   "meaning": "map column of the AI's drone (may go negative or past 39 - nothing clamps it).",
   "confidence": "high"
  },
  {
   "addr": "8975",
   "scope": "abs",
   "name": "aiDroneRow",
   "meaning": "map row of the AI's drone.",
   "confidence": "high"
  },
  {
   "addr": "8976",
   "scope": "abs",
   "name": "trainerDroneTarget",
   "meaning": "index of the human player's unit the drone is flying at ($FF = no mission); picked by the target scan in the module's own $EE00 image and used here to fetch the aim cell.",
   "confidence": "high"
  },
  {
   "addr": "8977",
   "scope": "abs",
   "name": "trainerDroneTimer",
   "meaning": "state of the swappable $EE00 page: bit 7 set = this track-29 s4-5 drone image is resident (the module's other AI think routines are skipped); not touched by this chunk but it is what gates the JSR $EE00 at $E0A5.",
   "confidence": "high"
  },
  {
   "addr": "8978",
   "scope": "abs",
   "name": "aiDroneDetonateThreshold",
   "meaning": "minimum scoreTargetBlock value at which the AI detonates its drone; loaded at $ECCC from the 5-byte skill table at $EC0A ($FF,$FF,$08,$0C,$0C indexed by the play/skill level $8966), so at the two easiest levels the drone only detonates when it actually reaches its target cell.",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "active screen id; 1 = the overlay-B radar/drone screen. Only then does the drone weave.",
   "confidence": "high"
  },
  {
   "addr": "920D",
   "scope": "abs",
   "name": "droneStateFlags",
   "meaning": "drone control flags; bit 0 = a drone move has been executed, i.e. a drone is in the air. The comm module tests it at $E09E/$E0C1 to decide whether to keep this page resident; commands $8E/$8F emitted here make the engine clear it.",
   "confidence": "high"
  },
  {
   "addr": "92B0",
   "scope": "abs",
   "name": "missilesLeft",
   "meaning": "2 bytes, [0] own (the human) and [1] opponent: missiles remaining. Only index 0 is read here, as the gate for the evasive weave (inference: no missiles left means the player cannot shoot the drone down).",
   "confidence": "medium"
  },
  {
   "addr": "92F9",
   "scope": "abs",
   "name": "droneFlags",
   "meaning": "drone lifecycle flags; bit 7 = the drone has arrived or been destroyed (set by the packet merger when a $92 is seen). While it is set this routine emits nothing at all.",
   "confidence": "high"
  },
  {
   "addr": "9236",
   "scope": "abs",
   "name": "comcenUnitIndex",
   "meaning": "unit index that mapCellToRadarGrid centres the radar window on; normally $31 (= 49, the human's comcen). This chunk deliberately leaves it alone, so $EEE1 tests visibility on the human's radar (the module's own $EE00 image instead sets it to $63 around its call).",
   "confidence": "medium"
  },
  {
   "addr": "E01E",
   "scope": "abs",
   "name": "rxPacketLength",
   "meaning": "number of valid bytes in the fake 'received' packet; the trainer AI writes it itself (1, 3 or 4 here). Also called receivedPacketLength in the comm-module survey.",
   "confidence": "high"
  },
  {
   "addr": "E020",
   "scope": "abs",
   "name": "rxPacketBuffer",
   "meaning": "buffer of the opponent's command bytes for this exchange ($E020-$E026). This chunk fills it with $8F; $8E,col,row; or $8D,heading,col,row. Also called receivedPacketBuffer.",
   "confidence": "high"
  },
  {
   "addr": "F640",
   "scope": "abs",
   "name": "unitColTable",
   "meaning": "100-byte per-unit map column array (units 0-49 player, 50-99 AI).",
   "confidence": "high"
  },
  {
   "addr": "F6A4",
   "scope": "abs",
   "name": "unitRowTable",
   "meaning": "100-byte per-unit map row array.",
   "confidence": "high"
  },
  {
   "addr": "F960",
   "scope": "abs",
   "name": "unitStunDig",
   "meaning": "per-unit byte; the low nibble is the stun counter. A stunned target lets the drone detonate at 3/4 of the normal score threshold.",
   "confidence": "high"
  },
  {
   "addr": "18",
   "scope": "zp",
   "name": "zpScratch18",
   "meaning": "scratch: the target unit's column while the four candidate blocks are scanned, and the 3/4 detonate threshold in the stunned-target test.",
   "confidence": "high"
  },
  {
   "addr": "19",
   "scope": "zp",
   "name": "zpScratch19",
   "meaning": "scratch: the target unit's row during the block scan, then the desired heading 0-7 that steerDroneAndEmitMove turns toward.",
   "confidence": "high"
  },
  {
   "addr": "1C",
   "scope": "zp",
   "name": "aiBestScore",
   "meaning": "running best block score for scoreTargetBlock; $FF is the 'nothing found yet' sentinel (bit 7 set makes any non-negative score win), 0 means 'accept any non-negative score'.",
   "confidence": "high"
  },
  {
   "addr": "1D",
   "scope": "zp",
   "name": "aiBestTargetUnit",
   "meaning": "highest player unit index found in the winning block by scoreTargetBlock; written but not read by this chunk.",
   "confidence": "medium"
  },
  {
   "addr": "22",
   "scope": "zp",
   "name": "cellColA",
   "meaning": "column of the first point handed to cellDistance/distanceSquared.",
   "confidence": "high"
  },
  {
   "addr": "24",
   "scope": "zp",
   "name": "cellRowA",
   "meaning": "row of the first point handed to cellDistance/distanceSquared.",
   "confidence": "high"
  },
  {
   "addr": "27",
   "scope": "zp",
   "name": "originCol",
   "meaning": "column of the second (reference) point for cellDistance; always the drone's column here, and also the X argument of mapCellToRadarGrid.",
   "confidence": "high"
  },
  {
   "addr": "28",
   "scope": "zp",
   "name": "originRow",
   "meaning": "row of the second point for cellDistance / mapCellToRadarGrid.",
   "confidence": "high"
  },
  {
   "addr": "92",
   "scope": "zp",
   "name": "lineCurRow",
   "meaning": "line-stepper start row = the drone's row (clamped to 0 when the drone is off the top of the map).",
   "confidence": "high"
  },
  {
   "addr": "93",
   "scope": "zp",
   "name": "lineCurCol",
   "meaning": "line-stepper start column = the drone's column (clamped to 0 when negative).",
   "confidence": "high"
  },
  {
   "addr": "94",
   "scope": "zp",
   "name": "lineEndRow",
   "meaning": "line-stepper end row = the row of the chosen 2x2 aim block, written by scoreTargetBlock and shifted here when the start had to be clamped.",
   "confidence": "high"
  },
  {
   "addr": "95",
   "scope": "zp",
   "name": "lineEndCol",
   "meaning": "line-stepper end column = the column of the chosen aim block.",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "EFAB",
   "length": 37,
   "type": "unknown",
   "name": "staleCommTailBytes",
   "description": "$EFAB-$EFCF: 37 bytes that are byte-for-byte identical in all four images of this address range (comm build 1 T35, comm build 2 T34, the track-29 s0-3 $EC00 sub-overlay and this one). In build 1 they are the tail of the AI's drone target picker (inc zp_1F / cmp #$0F / ... / sta $8979 / rts) and in build 2 they are the middle of the message 'INSERT DATA DISK AND PRESS SPACE.' ($EFAF, with the 4-character disk name patched into $EFB6). Nothing in this image reaches them - they are carried along only so that the shared strings below land at their fixed addresses."
  },
  {
   "addr": "EFD0",
   "length": 4,
   "type": "text",
   "name": "msgDataDiskName",
   "description": "'DATA' - the 4-character disk name that overlay A's promptInsertDataDisk ($81B2) patches into the INSERT-DISK message. The disassembler merges $EFD0-$EFE1 into one label msgDatagamecustomOff4; it is really three strings."
  },
  {
   "addr": "EFD4",
   "length": 4,
   "type": "text",
   "name": "msgGameDiskName",
   "description": "'GAME' - the disk name used by overlay A's promptInsertGameDisk ($81A5)."
  },
  {
   "addr": "EFD8",
   "length": 10,
   "type": "text",
   "name": "msgCustomOff",
   "description": "'CUSTOM OF','F'|$80 - installed as message slot $1E by overlay A ($EFD8), which also points its ON/OFF patch at $EFDF ('OFF')."
  },
  {
   "addr": "EFE2",
   "length": 11,
   "type": "text",
   "name": "msgDestroyOff",
   "description": "'DESTROY OF','F'|$80 - message slot $1E for the DESTROY toggle; overlay A patches ON/OFF at $EFEA."
  },
  {
   "addr": "EFED",
   "length": 8,
   "type": "text",
   "name": "msgAccepted",
   "description": "'ACCEPTE','D'|$80 - printed centred on row 15 by overlay A when the fire button confirms a value."
  },
  {
   "addr": "EFF5",
   "length": 11,
   "type": "unknown",
   "name": "trailingPadEFF5",
   "description": "$C0,$DF,$00,$FF,$00,$FF,$00,$FF,$00,$FF,$FF - unreferenced trailing bytes, the same in every image of this page except the last byte ($FF here and in build 1, $00 in build 2). Probably padding left by the mastering system."
  }
 ],
 "misclassified": [],
 "insights": [
  "This page is the solo-trainer AI's drone autopilot. It is one routine at $EE00 that runs once per command exchange and its only product is a fabricated 'received' packet in rxPacketBuffer $E020 / rxPacketLength $E01E: $8D heading,col,row (fly one cell), $8E col,row (detonate) or $8F (drone released, out of fuel). The three coordinates are pre-mirrored with mirrorBlockCornerForOpponent ($EA38) and the heading is sent as (heading+4)&7 because the engine's cmdDroneMove/cmdDroneDetonate turn the opponent's coordinates round again.",
  "Page-swap protocol (comm module build 1, $E082-$E0DB): $E082 clears rxPacketLength, then if trainerDroneTimer $8977 is positive the module's own $EE00 code is resident - when droneStateFlags $920D bit 0 says a drone is in the air it sets $8977 = $FF, loads track 29 sectors 4-5 over $EE00-$EFFF (destination set by $E128) and JSRs $EE00; if $8977 is negative this page is resident - it JSRs $EE00 while $920D bit 0 stays set and, when the bit clears, reloads track 35 sectors 14-15 over $EE00, sets $8976 = $FF and $8977 = 3. So a drone flight costs two disk loads, and while the drone flies the trainer AI issues nothing except drone commands (all its other think routines live in the swapped-out page).",
  "Comm build 2 (T34, the modem build) never loads or calls $EE00 - it has no AI. Everything in this file is trainer-only ($0BA5 bit 7 set).",
  "Difficulty tuning: aiDroneDetonateThreshold $8978 comes from the 5-byte table at $EC0A ($FF,$FF,$08,$0C,$0C) indexed by the skill/play level $8966 (loaded at $ECC9-$ECCE in the track-29 s0-3 sub-overlay, together with $8979 = the strike score threshold). At skill 0-1 the threshold is $FF, so the drone never detonates early and only blows up when getStepDirection reports 'no direction left', i.e. when it is standing on its aim cell.",
  "Evasion heuristic at $EED0-$EF0E (inference, medium confidence): the drone only weaves when the human could actually shoot it - $92B0 (own missiles) non-zero, the drone's cell inside the radar window built around the human's comcen ($9236 is left at $31 here, unlike the module's own image which temporarily sets it to $63), the radar screen actually on show ($90FB = 1), at least 12 fuel left and at least 3 cells from the aim point. It then takes a random heading with probability 170/256; since the heading only turns one step per exchange the result is a wobble, not a jump.",
  "$EF0F-$EF9D is a verbatim copy of overlay B's steerDrone ($731E-$7391) with $896F/$8974/$8975 substituted for $920B/$92F6/$92F7 and the turn sound removed - useful confirmation that the heading encoding (0-7 clockwise from N) and the tables headingToDirectionTable $7316 / directionDeltaTable $20F0 / diagonalDirectionTable $1FB2 are shared between the player's and the AI's drone.",
  "getStepDirection ($E80E) returns Y = a heading 0-7 in exactly that encoding (its search tables $E68A/$E693 are dcol = 00,01,01,01,00,FF,FF,FF and drow = FF,FF,00,01,01,01,00,FF = N,NE,E,SE,S,SW,W,NW) and leaves N=0 on a match, N=1 when nothing matched. The 'bpl $EED0' at $EEB4 relies on that: no direction means the drone is already on its aim cell, so it detonates.",
  "$EE00-$EFAA of this image differs from the comm module's own $EE00 page in 409 of its 427 bytes, but $EFAB-$EFFF is byte-identical in all four images of the range (verified against the decrypted sectors t29s04/05 vs t35s14/15). The reason is that overlay A hard-codes addresses inside that tail: $EFD0 'DATA', $EFD4 'GAME', $EFD8/$EFDF 'CUSTOM OFF', $EFE2/$EFEA 'DESTROY OFF', $EFED 'ACCEPTED', and $EFAF/$EFB6 for the INSERT-DISK message.",
  "Latent bug (medium confidence): overlay A's promptInsertDiskAndWaitSpace ($81B6) unconditionally installs $EFAF as message $1E and copies four characters into $EFB6-$EFB9. That message only exists in comm build 2; with build 1 or either track-29 sub-overlay resident, $EFAF reads as the single character 'I' ($C9) and the four-byte patch lands inside the AI code at $EFB6.",
  "Label hygiene: this file is one of five different code images that share $EE00-$EFFF, and XREF.txt merges all of them under game/ovl_E000_T35. The only true entry points here are $EE00 (JSR from $E0A5) and $EF9E (JSR from $EE26 and $EEFC); sub_EE2D, sub_EE55, sub_EE85, sub_EEED and most L_ labels are imported from the other images. The branch targets that are real in this image are L_EE14, L_EE15, L_EE5D, L_EE8B, L_EE95, L_EEA1, L_EEAF, L_EEB6, L_EED0, L_EF0F, L_EF2B, L_EF35, L_EF4F, L_EF57 and L_EF6D.",
  "Minor bug at $EE32-$EE35: 'ldy $8975 / bmi L_EE5D' overwrites the target unit index in Y with the drone's row, so when the drone drifts off the top of the map the aim point is fetched from unitColTable/unitRowTable indexed by that row instead of by the target unit. Every other path into $EE5D still has Y = $8976 (cellDistance and its callee distanceSquared preserve Y)."
 ]
}