{
 "chunk": "game_main_0800_2BDD",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "2C7F",
   "name": "drawWindowFrame",
   "summary": "Draws one of the 27 predefined screen windows/panels selected by X: copies left column, top row, width and height from windowLeftTable/windowTopTable/windowWidthTable/windowHeightTable into fillColumn/fillRow/windowWidth/windowHeight, fetches the window's 9-tile frame set pointer from windowFramePtrTable and falls into drawFrameBox. Used by every screen builder (battlefield frames 0-2, main menu $18/$19, overlay B setup/stat screens 5-$16).",
   "inputs": "X = window index 0-26",
   "outputs": "fillColumn, fillRow, windowWidth, windowHeight set; frame drawn; windowWidth/windowHeight left decremented by drawFrameBox; A/X/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "2CA1",
   "name": "drawFrameBox",
   "summary": "Draws a rectangular frame out of a 9-tile set (layout: 0 top-left, 1 top edge, 2 top-right, 3 left edge, 4 unused centre, 5 right edge, 6 bottom-left, 7 bottom edge, 8 bottom-right) at fillRow/fillColumn with windowWidth x windowHeight cells, corners first then the four edges via drawFrameRow/drawFrameColumn. Also called directly by overlay B ($724A) with its own tile set.",
   "inputs": "A/Y = lo/hi pointer to the 9-byte tile set; fillRow, fillColumn, windowWidth, windowHeight; current colour state (colourOverride/fillColourValue/charColourTable)",
   "outputs": "frame drawn into the bitmap and screen RAM; windowWidth and windowHeight decremented by 1 (become right/bottom extents); zp_18/zp_19 = last cell, zp_1A = right column, zp_1B = bottom row, zp_B8/zp_B9 = tile set pointer",
   "confidence": "high"
  },
  {
   "addr": "2D1D",
   "name": "drawFrameColumn",
   "summary": "Draws tile (zp_B8),Y repeatedly down column zp_19 starting at row zp_18 until the row reaches zp_1B (exclusive). Used for the left and right frame edges.",
   "inputs": "Y = tile offset in the set, zp_B8/zp_B9 = tile set, zp_18 = first row, zp_19 = column, zp_1B = stop row",
   "outputs": "zp_18 = zp_1B; zp_1C = tile code; cells drawn",
   "confidence": "high"
  },
  {
   "addr": "2D2F",
   "name": "drawFrameRow",
   "summary": "Draws tile (zp_B8),Y repeatedly along row zp_18 starting at column zp_19 until the column reaches zp_1A (exclusive). Used for the top and bottom frame edges.",
   "inputs": "Y = tile offset, zp_B8/zp_B9 = tile set, zp_18 = row, zp_19 = first column, zp_1A = stop column",
   "outputs": "zp_19 = zp_1A; zp_1C = tile code; cells drawn",
   "confidence": "high"
  },
  {
   "addr": "2D41",
   "name": "initBitmapScreen",
   "summary": "Sets up the game's bitmap display from scratch: full-screen text window (sub_C0D7), zp_41 = 1, border and background shadows + registers = grey ($0C), colour RAM filled with $0C, screen RAM $8C00-$8FFF filled with $CC, bitmap $A000-$BF3F cleared, then $D016 = $C8 (hires, 40 columns), $D018 = $38 (screen $8C00, bitmap $A000 in VIC bank 2), $D011 = $3B (bitmap mode) with the shadows at $9000-$9002 updated, and finally initSpritePointers. Called from the startup code ($0318), the screen switcher ($06E8), mainStart ($0AAF) and drawBattlefieldScreen ($2636).",
   "inputs": "none",
   "outputs": "VIC registers and shadows $9000-$9004, colour RAM, screen RAM, bitmap cleared; zp_41 = 1, zp_7E = 0, zp_48/zp_49 clobbered, spriteEnableShadow = 0, sprite pointers $8FF8-$8FFF = $28-$2F",
   "confidence": "high"
  },
  {
   "addr": "2DBD",
   "name": "setTerrainTilePtr",
   "summary": "Computes glyphPtr (zp_B2/zp_B3) = $9C6E + A*8 and clears colourOverride. A is a map byte minus $40 (the caller at $27B2 subtracts $40), so $9C6E holds the 8-byte terrain tile definitions that $27C2 expands into the 16x16-pixel battlefield cells.",
   "inputs": "A = terrain tile index (map byte - $40)",
   "outputs": "zp_B2/zp_B3 = tile pointer, colourOverride ($2A29) = 0, Y = 0",
   "confidence": "medium"
  },
  {
   "addr": "2DDB",
   "name": "clearMessageLineAndScreen",
   "summary": "Clears the message line via $C39F and falls into clearCurrentScreenWindows. Called from the screen-switch code at $070C.",
   "inputs": "currentScreen ($90FB)",
   "outputs": "see clearCurrentScreenWindows",
   "confidence": "medium"
  },
  {
   "addr": "2DDE",
   "name": "clearCurrentScreenWindows",
   "summary": "Re-clears the content areas of whichever screen is active (currentScreen $90FB): 0 = battlefield: sprites off, text colour 0, message window cleared (sub_261C, skipped while commPauseFlags is nonzero), overview window cleared (sub_5E03), then view window cleared (sub_26A1); 1 = overlay screen (jumps to $7A6B in overlay B); 2 = enables only sprite 1 and jumps to $821F with A = 0; 3 = jumps to the menu redraw at L_158F. Also called from the $C000 overlay ($C75E) after saving the text window, e.g. when resuming after a pause.",
   "inputs": "currentScreen ($90FB), commPauseFlags (zp_BE)",
   "outputs": "spriteEnableShadow = 0 (or 2 for screen 2), zp_3D = 0 for the battlefield; tail-calls the per-screen routine",
   "confidence": "medium"
  },
  {
   "addr": "2E0F",
   "name": "magnifyWindow2x",
   "summary": "In-place 2x zoom of the cell rectangle at fillRow/fillColumn with windowWidth x windowHeight cells, centred on the rectangle: every source cell (8x8 bitmap block plus its colour byte) is redrawn as a 2x2 cell block at (fillRow - h/2 + 2*row, fillColumn - w/2 + 2*col) using the $C000 overlay's double-size glyph renderer sub_C24B. Processes the four quadrants from the outer edges inward so no source cell is overwritten before it is read; odd widths/heights set magnifyWidthOdd/magnifyHeightOdd and pass edge flags in zp_BA so the middle row/column is drawn half size. Waits for raster line $80 first. Used by overlay B ($7552) for the repeated zoom-in animation of a 7x6 cell area at row 9 column 16.",
   "inputs": "fillRow, fillColumn, windowWidth, windowHeight",
   "outputs": "bitmap/screen rewritten; windowWidth/windowHeight decremented when even; zp_20 = ceil(w/2), zp_21 = ceil(h/2), zp_3F = dest left column, zp_40 = dest top row, zp_30/zp_31 = row/column counters, zp_3D = colour byte of the current cell, zp_BA = edge flags, zp_B2-zp_B9 clobbered",
   "confidence": "medium"
  },
  {
   "addr": "2F34",
   "name": "setMagnifyCellPointers",
   "summary": "Helper of magnifyWindow2x: computes the pointers of source cell (fillRow + zp_30, fillColumn + zp_31) and copies them to zp_B2/zp_B3 (bitmap, used as the glyph source) and zp_B8/zp_B9 (screen RAM, to read the colour byte), then computes the destination cell (zp_40 + 2*zp_30, zp_3F + 2*zp_31) into bitmapPtr/screenPtr via sub_2A9F.",
   "inputs": "zp_30 = source row offset, zp_31 = source column offset, fillRow, fillColumn, zp_3F/zp_40 = destination origin",
   "outputs": "zp_B2/zp_B3 = source bitmap cell, zp_B8/zp_B9 = source screen cell, zp_B4-zp_B7 = destination cell pointers, zp_18/zp_19 = destination row/column",
   "confidence": "high"
  },
  {
   "addr": "2F6A",
   "name": "initSpritePointers",
   "summary": "Clears spriteEnableShadow and writes sprite pointers $28..$2F to $8FF8-$8FFF (the last 8 bytes of the $8C00 screen), so sprites 0-7 take their shapes from $8A00, $8A40, ... $8BC0. Called only from initBitmapScreen.",
   "inputs": "none",
   "outputs": "spriteEnableShadow = 0, $8FF8-$8FFF set, zp_48/zp_49 = $8FF8, A/X/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "2FA5",
   "name": "clearAllSpriteData",
   "summary": "Zeroes $8A00-$8BFF, the shape data of all eight sprites. Entered by JMP from the startup code at $0386 (labelled L_2FA5 in the listing but it is a routine ending in RTS).",
   "inputs": "none",
   "outputs": "$8A00-$8BFF = 0, X = 0, A = 0",
   "confidence": "high"
  },
  {
   "addr": "2FB2",
   "name": "clearSpriteShape",
   "summary": "Zeroes the 64-byte shape of sprite A (address $8A00 + A*64 via spriteOffsetLoTable/spriteOffsetHiTable, patched into the STA at L_2FC8). Called before any sprite shape is built (cursor shapes, formation sprite, circles, overlay screens).",
   "inputs": "A = sprite number 0-7",
   "outputs": "sprite shape cleared; A = 0, Y = $FF",
   "confidence": "high"
  },
  {
   "addr": "2FCF",
   "name": "setCursorShapeSquare",
   "summary": "Loads the 18-byte (6 rows x 24 px) hollow-square shape at $9E6E into sprite 0 after clearing it, sets sprite 0 colour shadow ($9008) to 0 and enables sprite 0 via enableSprites. The cursor sprite is Y-expanded so the 6 rows cover a 12-pixel cell. Called when a unit is selected/being ordered ($2015, $4C53, $5E40, $5E87, $61F5, $6393).",
   "inputs": "none",
   "outputs": "sprite 0 shape and colour, spriteEnableShadow bit 0 set",
   "confidence": "medium"
  },
  {
   "addr": "2FF3",
   "name": "setCursorShapeCross",
   "summary": "Same as setCursorShapeSquare but with the thick cross/plus shape at $9E80 (used when zp_BB is positive, see $2195).",
   "inputs": "none",
   "outputs": "sprite 0 shape = cross, colour 0, sprite 0 enabled",
   "confidence": "medium"
  },
  {
   "addr": "2FFA",
   "name": "setCursorShapeBlock",
   "summary": "Loads the small solid-block shape at $9E92 into sprite cursorSpriteIndex ($91CC) after clearing it, and sets that sprite's colour shadow to $0C unless it is sprite 0 (sprite 0 keeps its blinking colour). Used for the plain battlefield cursor ($21A8) and for sprite 1 when no group box is active ($2550).",
   "inputs": "cursorSpriteIndex ($91CC) = sprite number",
   "outputs": "sprite shape rewritten, spriteColorShadow[n] = $0C for n != 0",
   "confidence": "medium"
  },
  {
   "addr": "3023",
   "name": "mapCellToSpriteXY",
   "summary": "Converts a map cell (X = column, Y = row) to sprite coordinates on the overview map. When groupBoxActive ($920A) bit 7 is set it first adds the group-box left/top offsets ($922F/$9231), converts, then adds 1 to both results; otherwise it falls straight into mapCellToSpriteXYRaw.",
   "inputs": "X = map column, Y = map row, groupBoxActive, $922F, $9231",
   "outputs": "X = sprite X, Y = sprite Y",
   "confidence": "medium"
  },
  {
   "addr": "303A",
   "name": "mapCellToSpriteXYRaw",
   "summary": "Sprite X = column*4 + $1F, sprite Y = row*4 + $39: the 40x40 battlefield is shown on the overview panel at 4x4 pixels per cell starting at screen pixel (7,7) (sprite origin offsets $18/$32).",
   "inputs": "X = map column 0-39, Y = map row 0-39",
   "outputs": "X = sprite X coordinate, Y = sprite Y coordinate",
   "confidence": "high"
  },
  {
   "addr": "3049",
   "name": "positionCursorSprite",
   "summary": "Places sprite cursorSpriteIndex ($91CC) over the view centre cell (viewOriginCol+3, viewOriginRow+2) on the overview map: converts with mapCellToSpriteXY (group-box aware), stores into spriteXShadow/spriteYShadow and enables the sprite with the mask from spriteBitMasks. Does nothing when cursorSpriteIndex is negative.",
   "inputs": "zp_A5, zp_A6, cursorSpriteIndex, groupBoxActive and offsets",
   "outputs": "spriteXShadow[n], spriteYShadow[n], spriteEnableShadow bit n",
   "confidence": "high"
  },
  {
   "addr": "306C",
   "name": "drawLineInPathSprite",
   "summary": "Draws a line into sprite 3 (the path/route sprite at $8AC0) using the high-memory line stepper: initLineStepper ($FBB8) from (lineCurRow,lineCurCol) to (lineEndRow,lineEndCol), then plots the current cell and steps until lineStepsLeft is 0. If A has bit 7 set, two extra steps are taken per plotted point, giving a dotted line. Called from $3B11 and overlay B $8266.",
   "inputs": "A = 0 solid / $80 dotted; zp_92-zp_95 line endpoints",
   "outputs": "pixels set in sprite 3; line stepper state consumed",
   "confidence": "medium"
  },
  {
   "addr": "308A",
   "name": "plotPathCross",
   "summary": "Plots the four orthogonal neighbours of (lineCurRow, lineCurCol) into sprite 3: (row, col-1), (row-1, col), (row+1, col), then falls into plotPathSpritePixel for (row, col+1). Leaves lineCurCol incremented by 1. Marks the end point of a path ($3B14).",
   "inputs": "zp_92 = row, zp_93 = column",
   "outputs": "4 pixels set in sprite 3; zp_93 = column + 1",
   "confidence": "medium"
  },
  {
   "addr": "30A1",
   "name": "plotPathSpritePixel",
   "summary": "Sets the pixel (column zp_93, row zp_92) in sprite 3's shape at $8AC0: byte offset = row*3 + column/8, bit mask from the $C000 table at $C008. Also used directly by overlay B ($7852, $785E).",
   "inputs": "zp_92 = row 0-20, zp_93 = column 0-23",
   "outputs": "one bit set in $8AC0-$8AFF; A/X/Y clobbered, operand at $30B4 patched",
   "confidence": "high"
  },
  {
   "addr": "30C0",
   "name": "buildGroupFormationSprite",
   "summary": "Builds the group 'formation ghost' for a group move: gets the bounding box of the destination positions (unitDestCol/unitDestRow, selected by Y=$80) of all live units in group selectedGroupId via sub_4B5E, stores its min/max columns/rows in $922F/$922E and $9231/$9230, plots every member as a 2x2 pixel dot at ((destCol-minCol)*2, (destRow-minRow)*2) into spriteBuildBuffer ($9025) with spritePlotMode = set, copies the buffer to sprite 0, sets X/Y expansion for sprites 0 and 1, converts the box extents into offsets relative to the selected unit's destination (so the box follows the cursor), sets groupBoxActive = $FF, calls keepGroupBoxInsideMap and positionCursorSprite. Called from $21A2 when the group has 2 or more units.",
   "inputs": "selectedGroupId ($9206), selectedUnit ($90F8), unit arrays $F76C/$FB54/$F7D0/$F834",
   "outputs": "sprite 0 shape, spriteBuildBuffer, $922E-$9231, spriteXExpandShadow/spriteYExpandShadow bits 0-1, groupBoxActive = $FF, spritePlotMode = $40, view origin possibly nudged",
   "confidence": "medium"
  },
  {
   "addr": "31A3",
   "name": "enableSprites",
   "summary": "ORs mask A into spriteEnableShadow ($9022) unless linkState ($922A) bit 6 is set (sprites suppressed, e.g. while the link is being set up). Widely used by cursor/marker code and the $C000 overlay.",
   "inputs": "A = sprite bit mask",
   "outputs": "spriteEnableShadow updated (or unchanged); A = combined mask",
   "confidence": "high"
  },
  {
   "addr": "31AF",
   "name": "plotSpritePixel",
   "summary": "Plots pixel (X = column 0-23, Y = row 0-20) in the shape of sprite A according to spritePlotMode ($91F3): bit 7 set = toggle (EOR), bit 6 set = set (ORA, shares code with setSpritePixel), otherwise clear (AND with the inverted mask). A negative sprite number selects the 64-byte spriteBuildBuffer at $9025 instead of $8A00 + n*64. Self-modifies the operation opcode at $3232 and its mask operand.",
   "inputs": "A = sprite number (bit 7 = use $9025 buffer), X = column, Y = row, spritePlotMode",
   "outputs": "one pixel modified; zp_98/zp_99 = sprite row pointer, spritePlotTemp clobbered",
   "confidence": "high"
  },
  {
   "addr": "31CA",
   "name": "setSpritePixel",
   "summary": "Entry of plotSpritePixel that forces the ORA (set) operation; the mask is still inverted when spritePlotMode has neither bit 6 nor 7 set, so callers set spritePlotMode = $40 first (buildGroupFormationSprite does).",
   "inputs": "A = sprite number, X = column, Y = row",
   "outputs": "pixel set",
   "confidence": "high"
  },
  {
   "addr": "3239",
   "name": "drawCircleInSprite",
   "summary": "Draws a circle outline of radius X (1-10) centred at (12,10) into sprite A using 8-way octant symmetry: takes the dx and dy point lists for that radius from the tables at $9F37/$9F41 (dx pointers), $9F4B/$9F55 (dy pointers) and the step count from $9F5F, then for each step computes and plots the 8 symmetric points (spritePlotMode decides set/clear/toggle). Radius 1 gives a filled 3x3 dot. Used for range rings and blob markers ($3471, $3D13, $3D43, overlay B $7190-$71DA).",
   "inputs": "A = sprite number (stored to cursorSpriteIndex $91CC), X = radius index 1-10, spritePlotMode",
   "outputs": "circle plotted; $91CC = A, circleStepsLeft ($91F2) = -1, zp_AD-zp_B0 = table pointers",
   "confidence": "high"
  },
  {
   "addr": "3266",
   "name": "computeCircleOctantPoints",
   "summary": "For circle step Y reads a = (zp_AD),Y and b = (zp_AF),Y and fills circlePointX[0..7] = 12+a,12+b,12-b,12-a,12-a,12-b,12+b,12+a and circlePointY[0..7] = 10+b,10+a,10+a,10+b,10-b,10-a,10-a,10-b (tables at $32DA and $32E2).",
   "inputs": "Y = step index, zp_AD/zp_AE and zp_AF/zp_B0 = dx/dy table pointers",
   "outputs": "$32DA-$32E9 filled",
   "confidence": "high"
  },
  {
   "addr": "32BF",
   "name": "plotCircleOctantPoints",
   "summary": "Plots the 8 points prepared by computeCircleOctantPoints into sprite cursorSpriteIndex via plotSpritePixel, iterating circlePointIndex ($3238) from 7 down to 0.",
   "inputs": "$32DA-$32E9, cursorSpriteIndex ($91CC), spritePlotMode",
   "outputs": "8 pixels plotted; circlePointIndex = $FF",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "2A29",
   "scope": "abs",
   "name": "colourOverride",
   "meaning": "Nonzero = colour byte forced for every character drawn by sub_2AC8 (ignores charColourTable $93B4); 0 = use the per-character default colour, and when that is 0 too, fillColourValue through the patchable colour operation",
   "confidence": "high"
  },
  {
   "addr": "2A2A",
   "scope": "abs",
   "name": "fillColourValue",
   "meaning": "Current colour byte for the rectangle fill (sub_2A64) and for characters whose table colour is 0; combined with the existing colour byte through the self-modified opcode at $2B00/$2A85 (LDA/ORA/EOR/CMP selected by sub_2A47)",
   "confidence": "high"
  },
  {
   "addr": "2A2B",
   "scope": "abs",
   "name": "useFillChar",
   "meaning": "Nonzero makes sub_2B18 draw fillChar in every cell of the block instead of the characters from the source string",
   "confidence": "high"
  },
  {
   "addr": "2A2C",
   "scope": "abs",
   "name": "fillChar",
   "meaning": "Character code used by sub_2B18 when useFillChar is set",
   "confidence": "high"
  },
  {
   "addr": "2A2D",
   "scope": "abs",
   "name": "fillRow",
   "meaning": "Top screen row (0-24) of the current window / block / fill rectangle; set from windowTopTable by drawWindowFrame",
   "confidence": "high"
  },
  {
   "addr": "2A2E",
   "scope": "abs",
   "name": "fillColumn",
   "meaning": "Left screen column (0-39) of the current window / block / fill rectangle; set from windowLeftTable by drawWindowFrame",
   "confidence": "high"
  },
  {
   "addr": "2E0D",
   "scope": "abs",
   "name": "magnifyWidthOdd",
   "meaning": "$FF when the window being magnified has an odd width (middle column drawn half size), else 0",
   "confidence": "high"
  },
  {
   "addr": "2E0E",
   "scope": "abs",
   "name": "magnifyHeightOdd",
   "meaning": "$FF when the window being magnified has an odd height, else 0",
   "confidence": "high"
  },
  {
   "addr": "3237",
   "scope": "abs",
   "name": "spritePlotTemp",
   "meaning": "Scratch of plotSpritePixel: X column, then bit index, then the row for the row*3 multiply",
   "confidence": "high"
  },
  {
   "addr": "3238",
   "scope": "abs",
   "name": "circlePointIndex",
   "meaning": "Loop index 7..0 over the 8 octant points in plotCircleOctantPoints",
   "confidence": "high"
  },
  {
   "addr": "32DA",
   "scope": "abs",
   "name": "circlePointX",
   "meaning": "8 sprite X coordinates of the current circle step (filled by computeCircleOctantPoints)",
   "confidence": "high"
  },
  {
   "addr": "32E2",
   "scope": "abs",
   "name": "circlePointY",
   "meaning": "8 sprite Y coordinates of the current circle step",
   "confidence": "high"
  },
  {
   "addr": "9221",
   "scope": "abs",
   "name": "windowWidth",
   "meaning": "Width in cells of the current window/block (from windowWidthTable or the block header); drawFrameBox and magnifyWindow2x decrement it in place",
   "confidence": "high"
  },
  {
   "addr": "9222",
   "scope": "abs",
   "name": "windowHeight",
   "meaning": "Height in cells of the current window/block; decremented in place by drawFrameBox",
   "confidence": "high"
  },
  {
   "addr": "9025",
   "scope": "abs",
   "name": "spriteBuildBuffer",
   "meaning": "64-byte off-screen sprite shape buffer ($9025-$9064): plotSpritePixel targets it when the sprite number is negative; buildGroupFormationSprite composes the formation there and copies it to sprite 0; overlay B uses it too ($70A6-$70CC)",
   "confidence": "high"
  },
  {
   "addr": "91CC",
   "scope": "abs",
   "name": "cursorSpriteIndex",
   "meaning": "Number of the sprite currently used as cursor/marker (0 = main cursor, 1 = second cursor, 4 = blob, ...); bit 7 set = none (positionCursorSprite does nothing, plotting goes to spriteBuildBuffer). Set by drawCircleInSprite to its A argument",
   "confidence": "high"
  },
  {
   "addr": "91F2",
   "scope": "abs",
   "name": "circleStepsLeft",
   "meaning": "Remaining circle steps (entries of the dx/dy lists) in drawCircleInSprite",
   "confidence": "high"
  },
  {
   "addr": "91F3",
   "scope": "abs",
   "name": "spritePlotMode",
   "meaning": "Pixel operation for plotSpritePixel/drawCircleInSprite: $80 = toggle (EOR), $40 = set (ORA), 0 = clear (AND)",
   "confidence": "high"
  },
  {
   "addr": "9206",
   "scope": "abs",
   "name": "selectedGroupId",
   "meaning": "Group/formation id of the selected unit group; ORed with $40 and compared against unitFlagsTable & $7C to find the members (sub_4B5E, buildGroupFormationSprite)",
   "confidence": "medium"
  },
  {
   "addr": "9209",
   "scope": "abs",
   "name": "selectedGroupSize",
   "meaning": "Number of units in the current selection (1 for a single unit at $5E3D); >= 2 makes $219B build the formation sprite instead of the plain cursor",
   "confidence": "low"
  },
  {
   "addr": "920A",
   "scope": "abs",
   "name": "groupBoxActive",
   "meaning": "Bit 7 set while a group formation box is attached to the cursor (set by buildGroupFormationSprite, cleared at $223E)",
   "confidence": "high"
  },
  {
   "addr": "922E",
   "scope": "abs",
   "name": "groupBoxRightOffset",
   "meaning": "Column offset (>= 0) of the group box right edge from the selected unit / cursor column (max member column - selected unit column). NOTE: the 163A survey swapped this with $922F; sub_4B5E stores the maximum column in zp_1B which buildGroupFormationSprite copies here",
   "confidence": "medium"
  },
  {
   "addr": "922F",
   "scope": "abs",
   "name": "groupBoxLeftOffset",
   "meaning": "Column offset (<= 0, signed) of the group box left edge from the cursor column; mapCellToSpriteXY adds it to position the formation sprite and buildGroupFormationSprite subtracts it from member columns",
   "confidence": "medium"
  },
  {
   "addr": "9230",
   "scope": "abs",
   "name": "groupBoxBottomOffset",
   "meaning": "Row offset (>= 0) of the group box bottom edge from the cursor row (keepGroupBoxInsideMap scrolls up when centre + this >= limit)",
   "confidence": "medium"
  },
  {
   "addr": "9231",
   "scope": "abs",
   "name": "groupBoxTopOffset",
   "meaning": "Row offset (<= 0, signed) of the group box top edge from the cursor row; added to the row by mapCellToSpriteXY",
   "confidence": "medium"
  },
  {
   "addr": "90F8",
   "scope": "abs",
   "name": "selectedUnit",
   "meaning": "Index (0-99) of the selected unit; buildGroupFormationSprite uses its destination position as the box origin",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "Active screen id 0-3 (0 battlefield, 1 overlay-B screen, 2 overlay-B screen drawn by $821F, 3 menu) dispatched by clearCurrentScreenWindows",
   "confidence": "high"
  },
  {
   "addr": "9000",
   "scope": "abs",
   "name": "vicCtrl1Shadow",
   "meaning": "Shadow of $D011 ($3B = bitmap mode) written by initBitmapScreen",
   "confidence": "high"
  },
  {
   "addr": "9001",
   "scope": "abs",
   "name": "vicCtrl2Shadow",
   "meaning": "Shadow of $D016; bit 4 (multicolour) is tested by the character drawer sub_2AC8 to decide whether colour RAM must be written as well",
   "confidence": "high"
  },
  {
   "addr": "9002",
   "scope": "abs",
   "name": "vicMemPtrShadow",
   "meaning": "Shadow of $D018 ($38: screen $8C00, bitmap $A000)",
   "confidence": "high"
  },
  {
   "addr": "9003",
   "scope": "abs",
   "name": "borderColorShadow",
   "meaning": "Shadow of $D020",
   "confidence": "high"
  },
  {
   "addr": "9004",
   "scope": "abs",
   "name": "bgColorShadow",
   "meaning": "Shadow of $D021",
   "confidence": "high"
  },
  {
   "addr": "9008",
   "scope": "abs",
   "name": "spriteColorShadow",
   "meaning": "8 sprite colour shadows ($D027-$D02E); cursor shape setters write 0 (sprite 0) or $0C",
   "confidence": "high"
  },
  {
   "addr": "9010",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "8 sprite X positions written by positionCursorSprite",
   "confidence": "high"
  },
  {
   "addr": "9018",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "8 sprite Y positions written by positionCursorSprite",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "Shadow of $D015; set through enableSprites, cleared by initSpritePointers/clearCurrentScreenWindows",
   "confidence": "high"
  },
  {
   "addr": "9023",
   "scope": "abs",
   "name": "spriteXExpandShadow",
   "meaning": "Shadow of $D01D; bits 0-1 set for the formation sprites 0/1",
   "confidence": "high"
  },
  {
   "addr": "9024",
   "scope": "abs",
   "name": "spriteYExpandShadow",
   "meaning": "Shadow of $D017; bits 0-1 set for the formation sprites 0/1",
   "confidence": "high"
  },
  {
   "addr": "922A",
   "scope": "abs",
   "name": "linkState",
   "meaning": "Bit 6 set blocks enableSprites (sprites stay off)",
   "confidence": "medium"
  },
  {
   "addr": "F76C",
   "scope": "abs",
   "name": "unitStatusTable",
   "meaning": "Per-unit byte (100 entries); bit 7 set = unit slot unused/dead, skipped by the group scans",
   "confidence": "medium"
  },
  {
   "addr": "F7D0",
   "scope": "abs",
   "name": "unitDestCol",
   "meaning": "Per-unit destination/ordered map column (100 entries, low 7 bits; bit 7 is a flag copied into unitFlagsTable at $3DF2); selected instead of unitCol by sub_4B5E when Y bit 7 is set and used for the formation sprite",
   "confidence": "medium"
  },
  {
   "addr": "F834",
   "scope": "abs",
   "name": "unitDestRow",
   "meaning": "Per-unit destination/ordered map row (100 entries, low 7 bits)",
   "confidence": "medium"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitFlagsTable",
   "meaning": "Per-unit flags; bits 2-6 hold the group id (compared with selectedGroupId | $40 after AND $7C)",
   "confidence": "medium"
  },
  {
   "addr": "8A00",
   "scope": "abs",
   "name": "spriteShapes",
   "meaning": "Shape data of sprites 0-7 ($8A00-$8BFF, 64 bytes each; sprite pointers $28-$2F). Sprite 0/1 = cursors and formation ghost, sprite 3 ($8AC0) = path/route line sprite",
   "confidence": "high"
  },
  {
   "addr": "8AC0",
   "scope": "abs",
   "name": "pathSpriteShape",
   "meaning": "Shape of sprite 3, the path/route line drawn by drawLineInPathSprite and plotPathSpritePixel",
   "confidence": "medium"
  },
  {
   "addr": "9E6E",
   "scope": "abs",
   "name": "cursorShapeSquare",
   "meaning": "18-byte (6 rows x 3 bytes) hollow 6x6 square sprite shape, the unit-selected cursor",
   "confidence": "medium"
  },
  {
   "addr": "9E80",
   "scope": "abs",
   "name": "cursorShapeCross",
   "meaning": "18-byte thick plus/cross sprite shape (used when zp_BB > 0)",
   "confidence": "medium"
  },
  {
   "addr": "9E92",
   "scope": "abs",
   "name": "cursorShapeBlock",
   "meaning": "18-byte solid 4x4 block sprite shape, the plain battlefield cursor",
   "confidence": "medium"
  },
  {
   "addr": "9C6E",
   "scope": "abs",
   "name": "terrainTileGlyphs",
   "meaning": "8-byte terrain tile definitions indexed by map byte - $40 (pointer computed by setTerrainTilePtr, expanded 2x at $27C2)",
   "confidence": "medium"
  },
  {
   "addr": "9F37",
   "scope": "abs",
   "name": "circleDxPtrLo",
   "meaning": "10 low bytes of pointers to the dx lists of circle radii 1-10 (lists live at $9F69+)",
   "confidence": "high"
  },
  {
   "addr": "9F41",
   "scope": "abs",
   "name": "circleDxPtrHi",
   "meaning": "10 high bytes (all $9F) of the circle dx list pointers",
   "confidence": "high"
  },
  {
   "addr": "9F4B",
   "scope": "abs",
   "name": "circleDyPtrLo",
   "meaning": "10 low bytes of pointers to the dy lists of circle radii 1-10",
   "confidence": "high"
  },
  {
   "addr": "9F55",
   "scope": "abs",
   "name": "circleDyPtrHi",
   "meaning": "10 high bytes (all $9F) of the circle dy list pointers",
   "confidence": "high"
  },
  {
   "addr": "9F5F",
   "scope": "abs",
   "name": "circleStepCount",
   "meaning": "10 bytes: number of (dx,dy) entries minus 1 for each circle radius",
   "confidence": "high"
  },
  {
   "addr": "9810",
   "scope": "abs",
   "name": "tallDigitPairs",
   "meaning": "10 pairs of character codes ($49/$4A ...) forming the two-row tall digits 0-9 drawn by sub_2BB7",
   "confidence": "medium"
  },
  {
   "addr": "C008",
   "scope": "abs",
   "name": "pixelBitMasks",
   "meaning": "8-byte table $80,$40,...,$01 in the $C000 overlay used by the sprite plotters to select the bit for column & 7",
   "confidence": "high"
  },
  {
   "addr": "18",
   "scope": "zp",
   "name": "cellRow",
   "meaning": "Screen row (0-24) of the cell addressed by sub_2A9F and the character drawer; frame and block drawers use it as their row cursor",
   "confidence": "high"
  },
  {
   "addr": "19",
   "scope": "zp",
   "name": "cellColumn",
   "meaning": "Screen column (0-39) of the cell addressed by sub_2A9F; column cursor of the frame/block drawers",
   "confidence": "high"
  },
  {
   "addr": "1A",
   "scope": "zp",
   "name": "frameRightColumn",
   "meaning": "In drawFrameBox: column of the right frame edge; in buildGroupFormationSprite: unit loop index",
   "confidence": "high"
  },
  {
   "addr": "1B",
   "scope": "zp",
   "name": "frameBottomRow",
   "meaning": "In drawFrameBox: row of the bottom frame edge; row counter in the fill/block routines; group id compare value in buildGroupFormationSprite",
   "confidence": "high"
  },
  {
   "addr": "1C",
   "scope": "zp",
   "name": "frameTileCode",
   "meaning": "Tile character repeated by drawFrameRow/drawFrameColumn (column counter in sub_2B18)",
   "confidence": "high"
  },
  {
   "addr": "20",
   "scope": "zp",
   "name": "magnifyHalfWidth",
   "meaning": "ceil(windowWidth/2) during magnifyWindow2x",
   "confidence": "high"
  },
  {
   "addr": "21",
   "scope": "zp",
   "name": "magnifyHalfHeight",
   "meaning": "ceil(windowHeight/2) during magnifyWindow2x",
   "confidence": "high"
  },
  {
   "addr": "30",
   "scope": "zp",
   "name": "magnifyRow",
   "meaning": "Source row counter of magnifyWindow2x",
   "confidence": "high"
  },
  {
   "addr": "31",
   "scope": "zp",
   "name": "magnifyColumn",
   "meaning": "Source column counter of magnifyWindow2x",
   "confidence": "high"
  },
  {
   "addr": "3D",
   "scope": "zp",
   "name": "textColour",
   "meaning": "Colour byte; sub_2AC8 writes it to colour RAM in multicolour mode, magnifyWindow2x loads it with the source cell's colour, clearCurrentScreenWindows zeroes it",
   "confidence": "high"
  },
  {
   "addr": "3F",
   "scope": "zp",
   "name": "magnifyDestColumn",
   "meaning": "Reused text-cursor byte: left column of the magnified output (fillColumn - half width)",
   "confidence": "high"
  },
  {
   "addr": "40",
   "scope": "zp",
   "name": "magnifyDestRow",
   "meaning": "Reused text-cursor byte: top row of the magnified output (fillRow - half height)",
   "confidence": "high"
  },
  {
   "addr": "41",
   "scope": "zp",
   "name": "flag41",
   "meaning": "Set to 1 by initBitmapScreen (menu-vs-battle state flag per the 0800 survey)",
   "confidence": "low"
  },
  {
   "addr": "7E",
   "scope": "zp",
   "name": "textState7E",
   "meaning": "Cleared by initBitmapScreen; only otherwise touched by the $C000 text renderer ($C187 read, $C1DD write)",
   "confidence": "low"
  },
  {
   "addr": "91",
   "scope": "zp",
   "name": "lineStepsLeft",
   "meaning": "Remaining line steps; drawLineInPathSprite loops until 0",
   "confidence": "high"
  },
  {
   "addr": "92",
   "scope": "zp",
   "name": "lineCurRow",
   "meaning": "Current row of the line stepper = sprite row plotted by plotPathSpritePixel",
   "confidence": "high"
  },
  {
   "addr": "93",
   "scope": "zp",
   "name": "lineCurCol",
   "meaning": "Current column of the line stepper = sprite column plotted by plotPathSpritePixel",
   "confidence": "high"
  },
  {
   "addr": "98",
   "scope": "zp",
   "name": "spriteRowPtr",
   "meaning": "Pointer (lo) to the sprite shape byte row being plotted by plotSpritePixel",
   "confidence": "high"
  },
  {
   "addr": "99",
   "scope": "zp",
   "name": "spriteRowPtrHi",
   "meaning": "Pointer (hi) for plotSpritePixel ($8A or $90)",
   "confidence": "high"
  },
  {
   "addr": "A5",
   "scope": "zp",
   "name": "viewOriginCol",
   "meaning": "Map column of the view's left edge; +3 is the cursor/centre column used by positionCursorSprite",
   "confidence": "high"
  },
  {
   "addr": "A6",
   "scope": "zp",
   "name": "viewOriginRow",
   "meaning": "Map row of the view's top edge; +2 is the cursor/centre row",
   "confidence": "high"
  },
  {
   "addr": "AD",
   "scope": "zp",
   "name": "circleDxPtr",
   "meaning": "Pointer (lo) to the dx list of the current circle radius",
   "confidence": "high"
  },
  {
   "addr": "AE",
   "scope": "zp",
   "name": "circleDxPtrHi",
   "meaning": "Pointer (hi) of circleDxPtr",
   "confidence": "high"
  },
  {
   "addr": "AF",
   "scope": "zp",
   "name": "circleDyPtr",
   "meaning": "Pointer (lo) to the dy list of the current circle radius",
   "confidence": "high"
  },
  {
   "addr": "B0",
   "scope": "zp",
   "name": "circleDyPtrHi",
   "meaning": "Pointer (hi) of circleDyPtr",
   "confidence": "high"
  },
  {
   "addr": "B2",
   "scope": "zp",
   "name": "glyphPtr",
   "meaning": "Pointer (lo) to the 8-byte glyph source: $9426 + char*8 for characters, a bitmap cell during magnification, $9C6E + tile*8 for terrain tiles",
   "confidence": "high"
  },
  {
   "addr": "B3",
   "scope": "zp",
   "name": "glyphPtrHi",
   "meaning": "Pointer (hi) of glyphPtr",
   "confidence": "high"
  },
  {
   "addr": "B4",
   "scope": "zp",
   "name": "bitmapPtr",
   "meaning": "Pointer (lo) to the destination 8x8 cell in the $A000 bitmap (row table $9300/$9319 + column table $9332/$935A)",
   "confidence": "high"
  },
  {
   "addr": "B5",
   "scope": "zp",
   "name": "bitmapPtrHi",
   "meaning": "Pointer (hi) of bitmapPtr",
   "confidence": "high"
  },
  {
   "addr": "B6",
   "scope": "zp",
   "name": "screenPtr",
   "meaning": "Pointer (lo) to the colour byte of the cell in screen RAM $8C00 (row table $9382/$939B + column); +$4C00 reaches colour RAM",
   "confidence": "high"
  },
  {
   "addr": "B7",
   "scope": "zp",
   "name": "screenPtrHi",
   "meaning": "Pointer (hi) of screenPtr",
   "confidence": "high"
  },
  {
   "addr": "B8",
   "scope": "zp",
   "name": "blockPtr",
   "meaning": "Pointer (lo) to the frame tile set (drawFrameBox), the width/height/chars block (sub_2B18) or the source cell colour byte (magnify)",
   "confidence": "high"
  },
  {
   "addr": "B9",
   "scope": "zp",
   "name": "blockPtrHi",
   "meaning": "Pointer (hi) of blockPtr",
   "confidence": "high"
  },
  {
   "addr": "BA",
   "scope": "zp",
   "name": "magnifyEdgeFlags",
   "meaning": "Flags passed to the $C000 double-size renderer sub_C24B: bit 0 skip top half row, bit 1 skip bottom half row, bit 2 skip left half column, bit 3 skip right half column (for odd sizes)",
   "confidence": "medium"
  },
  {
   "addr": "BB",
   "scope": "zp",
   "name": "cursorModeFlag",
   "meaning": "Selects the cursor shape at $218F: 0 block/formation, positive cross, negative block",
   "confidence": "low"
  },
  {
   "addr": "BE",
   "scope": "zp",
   "name": "commPauseFlags",
   "meaning": "Nonzero skips clearing the message window in clearCurrentScreenWindows",
   "confidence": "medium"
  },
  {
   "addr": "48",
   "scope": "zp",
   "name": "fillPtr",
   "meaning": "Pointer (lo) used by initBitmapScreen to clear the bitmap and by initSpritePointers",
   "confidence": "high"
  },
  {
   "addr": "49",
   "scope": "zp",
   "name": "fillPtrHi",
   "meaning": "Pointer (hi) of fillPtr",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "2BDD",
   "length": 27,
   "type": "byteTable",
   "name": "windowLeftTable",
   "description": "Left screen column of windows 0-26 (0 overview panel, 1 battlefield view, 2 status panel, 4, 5-$16 overlay B screens, $18/$19 main menu frames, $1A)"
  },
  {
   "addr": "2BF8",
   "length": 27,
   "type": "byteTable",
   "name": "windowTopTable",
   "description": "Top screen row of windows 0-26"
  },
  {
   "addr": "2C13",
   "length": 27,
   "type": "byteTable",
   "name": "windowWidthTable",
   "description": "Width in cells (including the frame) of windows 0-26; e.g. window 0 = 22x22 around the 20x20 overview map, window 1 = 16x12 around the 7x5 cell (14x10 char) battlefield view"
  },
  {
   "addr": "2C2E",
   "length": 27,
   "type": "byteTable",
   "name": "windowHeightTable",
   "description": "Height in cells of windows 0-26"
  },
  {
   "addr": "2C49",
   "length": 54,
   "type": "addrTable",
   "name": "windowFramePtrTable",
   "description": "27 little-endian pointers to 9-byte frame tile sets in $9824-$987D (sets at $9824, $982D, $9836, $983F, $9848 (solid), $9851 (blank), $985A, $9863, $986C, $9875)"
  },
  {
   "addr": "2E0D",
   "length": 2,
   "type": "byteTable",
   "name": "magnifyOddFlags",
   "description": "magnifyWidthOdd and magnifyHeightOdd work variables"
  },
  {
   "addr": "2F85",
   "length": 16,
   "type": "byteTable",
   "name": "spriteBitMasks",
   "description": "$01,$02,...,$80 (set masks for sprite n) followed by $FE,$FD,...,$7F (clear masks, used by $337D)"
  },
  {
   "addr": "2F95",
   "length": 8,
   "type": "byteTable",
   "name": "spriteOffsetLoTable",
   "description": "Low byte of n*64 for sprite 0-7 ($00,$40,$80,$C0 repeating); added to $8A00"
  },
  {
   "addr": "2F9D",
   "length": 8,
   "type": "byteTable",
   "name": "spriteOffsetHiTable",
   "description": "High byte of n*64 for sprite 0-7 (0,0,0,0,1,1,1,1)"
  },
  {
   "addr": "3237",
   "length": 2,
   "type": "byteTable",
   "name": "spritePlotWork",
   "description": "spritePlotTemp and circlePointIndex work bytes"
  }
 ],
 "misclassified": [
  {
   "addr": "2FA5",
   "length": 13,
   "actual": "code",
   "evidence": "Labelled L_2FA5 but it is a routine (clearAllSpriteData) entered by JMP from the startup code at $0386 and terminated by RTS; it should be a sub_/routine label."
  },
  {
   "addr": "3232",
   "length": 2,
   "actual": "code",
   "evidence": "D_3232 carries a data label but is the self-modified ORA/AND/EOR #imm instruction of plotSpritePixel (opcode written at $31BB/$31C5/$31CF, operand at $3223). Code, not data."
  }
 ],
 "insights": [
  "Screen cell addressing: sub_2A9F takes zp_18 = row (0-24) and zp_19 = column (0-39) and builds bitmapPtr (zp_B4/B5) from the row tables $9300 (lo)/$9319 (hi) plus the column tables $9332/$935A, and screenPtr (zp_B6/B7) from $9382/$939B plus the column. Bitmap $A000, screen RAM $8C00, colour RAM = screen + $4C00. The game font is 8 bytes per character at $9426 (sub_2AC8: glyphPtr = $9426 + char*8); $93B4 is a per-character default colour table (0 = use fillColourValue).",
  "Character drawing modes are self-modified opcodes: sub_2A38 (Y = 0 LDA/1 ORA/2 EOR/3 CMP=keep) patches the bitmap operation at $2AF3 and also the $C000 overlay's copy at $C22B; sub_2A47 patches the colour-byte operation at $2B00 and $2A85 (absolute forms). sub_2A56 resets both to plain copy. sub_2BCE toggles a 2x2 colour block with EOR (cursor highlight).",
  "The window system: 27 windows defined by four parallel byte tables at $2BDD/$2BF8/$2C13/$2C2E plus the pointer table $2C49 to 9-tile frame sets at $9824-$987D (tile order TL, T, TR, L, centre, R, BL, B, BR). Window 0 (col 0,row 0,22x22) frames the overview map, window 1 (col 24,row 0,16x12) frames the 7x5 battlefield view, window 2 (col 24,row 11,16x11) the message/status panel; windows $18/$19 are the main menu frames. sub_2B75 draws the small ring glyph $38 (a rivet) at the four inner corners of the current window; sub_2BB7 draws two-row tall digits from the pair table $9810.",
  "Sprites: initSpritePointers puts $28-$2F in $8FF8-$8FFF so sprite n's shape lives at $8A00 + n*64 ($8A00-$8BFF, the 'work RAM' in the overview). Sprite 0 (and 1) are the battlefield cursor / group formation ghost (Y-expanded, 18-byte shapes at $9E6E square, $9E80 cross, $9E92 block), sprite 3 ($8AC0) holds the path/route line, sprite 4 gets circle blobs. $9025-$9064 is an off-screen 64-byte sprite build buffer selected by a negative sprite number in plotSpritePixel. All sprite register writes go through the shadows $9008/$9010/$9018/$9022/$9023/$9024 copied by the IRQ.",
  "Overview map geometry: mapCellToSpriteXYRaw maps map cell (col,row) to sprite coordinates (col*4+$1F, row*4+$39), i.e. the whole 40x40 battlefield is drawn at 4x4 pixels per cell in the 20x20-character overview panel at screen column 1, row 1 (window 0), while the 7x5-cell zoomed view at column 25 uses 16x16 pixel cells. The cursor/centre cell is (viewOriginCol+3, viewOriginRow+2).",
  "Group move mechanics: buildGroupFormationSprite ($30C0) collects all live units whose unitFlagsTable & $7C equals selectedGroupId | $40, uses their destination positions (unitDestCol $F7D0 / unitDestRow $F834, low 7 bits) to plot a 2x2 dot per unit into a sprite, and stores the box extents relative to the selected unit in $922F (left, <= 0), $922E (right), $9231 (top, <= 0), $9230 (bottom). The existing 163A survey names for $922E/$922F and $9230/$9231 appear swapped (left<->right, top<->bottom); sub_4B5E and keepGroupBoxInsideMap confirm the assignment given here.",
  "Unit record layout seen here (100 units, parallel arrays $64 apart): $F640 unitCol, $F6A4 unitRow, $F76C status (bit 7 = dead/unused), $F7D0 destination column, $F834 destination row, $FB54 flags (bits 2-6 group id).",
  "Circle drawing: drawCircleInSprite ($3239) uses precomputed octant point lists for radii 1-10 at $9F69.. (pointers at $9F37/$9F41 dx, $9F4B/$9F55 dy, counts at $9F5F) and plots the 8 symmetric points about sprite pixel (12,10); spritePlotMode ($91F3: $80 toggle, $40 set, 0 clear) chooses the pixel operation. Radius 1 is a filled 3x3 dot; larger radii are outlines (range rings).",
  "magnifyWindow2x ($2E0F) is an in-place 2x zoom of a cell rectangle about its centre, used by overlay B ($7552) as a repeated zoom-in animation; it relies on the $C000 overlay's double-size glyph renderer sub_C24B (also used by the text printer when zp_44 bit 7 is set) and the edge flags in zp_BA.",
  "The disassembly file's line numbers do not match the chunk specification: lines 4395-5251 hold $29C9-$30A1; the address range $2BDD-$32D9 is at file lines 4714-5603. This survey covers the address range."
 ]
}