readSprites
Debug C64 sprite issues by reading hardware sprite states including position, color, enable status, and expansion flags to identify visibility, color, or size problems.
Instructions
Read state of all 8 hardware sprites with interpreted values.
Returns for each sprite:
Position (X, Y) with visibility check
Color (with name)
Enable status
Multicolor mode
X/Y expansion (double size)
Priority (in front of / behind background)
Data pointer address
Use this to debug sprite issues like:
"Why is my sprite invisible?" → check enabled, position, pointer
"Wrong colors?" → check multicolor mode and color registers
"Wrong size?" → check expand flags
Options:
enabledOnly: Only return enabled sprites (default: false)
Related tools: readVicState, readMemory (for sprite data)
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| enabledOnly | No | Only return enabled sprites (default: false) |
Implementation Reference
- src/index.ts:1405-1523 (handler)Main handler function for 'readSprites' tool. Reads VIC-II registers ($D000-$D02E), CIA2 bank info, sprite pointers from screen RAM + $3F8. Computes positions, colors, expansion, priority, multicolor mode for each of 8 sprites. Uses helpers for visibility check and data address validation. Returns structured sprite data with issues and hints.try { // Read VIC registers const vicData = await client.readMemory(0xd000, 0xd02e); // Read CIA2 for bank info (needed for sprite pointer calculation) const cia2Data = await client.readMemory(0xdd00, 0xdd00); const bankInfo = getVicBank(cia2Data[0]); // Get screen address for sprite pointers const d018 = vicData[0x18]; const videoAddrs = getVideoAddresses(d018, bankInfo.baseAddress); // Sprite pointers are at screen + $3F8 const spritePointerBase = videoAddrs.screenAddress + 0x3f8; const spritePointers = await client.readMemory( spritePointerBase, spritePointerBase + 7 ); const spriteEnable = vicData[0x15]; const spriteXMsb = vicData[0x10]; const spriteYExpand = vicData[0x17]; const spriteXExpand = vicData[0x1d]; const spriteMulticolor = vicData[0x1c]; const spritePriority = vicData[0x1b]; const sprites = []; for (let i = 0; i < 8; i++) { const enabled = !!(spriteEnable & (1 << i)); // Skip disabled sprites if enabledOnly if (args.enabledOnly && !enabled) continue; // X position (9-bit) const xLow = vicData[i * 2]; const xHigh = (spriteXMsb & (1 << i)) ? 256 : 0; const x = xLow + xHigh; // Y position (8-bit) const y = vicData[i * 2 + 1]; const visibility = isSpriteVisible(x, y, enabled); // Sprite data address with region validation const pointer = spritePointers[i]; const dataAddress = bankInfo.baseAddress + pointer * 64; const addressInfo = validateSpriteDataAddress(dataAddress); sprites.push({ index: i, enabled, position: { x, y, visible: visibility.visible, visibilityReason: visibility.reason, }, color: getColorInfo(vicData[0x27 + i]), multicolor: !!(spriteMulticolor & (1 << i)), expandX: !!(spriteXExpand & (1 << i)), expandY: !!(spriteYExpand & (1 << i)), priority: (spritePriority & (1 << i)) ? "behind" : "front", pointer: { value: pointer, hex: `$${pointer.toString(16).padStart(2, "0")}`, }, dataAddress: { value: dataAddress, hex: `$${dataAddress.toString(16).padStart(4, "0")}`, region: addressInfo.region, severity: addressInfo.severity, warning: addressInfo.warning, }, }); } const enabledCount = sprites.filter((s) => s.enabled).length; const visibleCount = sprites.filter((s) => s.position.visible).length; // Collect visibility issues const visibilityIssues = sprites .filter((s) => s.enabled && !s.position.visible) .map((s) => `Sprite ${s.index}: ${s.position.visibilityReason}`); // Collect data address issues (warnings and errors) const addressIssues = sprites .filter((s) => s.enabled && s.dataAddress.warning) .map((s) => `Sprite ${s.index}: ${s.dataAddress.warning} (${s.dataAddress.hex})`); const allIssues = [...visibilityIssues, ...addressIssues]; // Build hint based on most critical issue let hint: string; if (addressIssues.length > 0) { hint = `⚠️ ${addressIssues.length} sprite(s) with suspicious data address: ${addressIssues[0]}`; } else if (visibilityIssues.length > 0) { hint = `${visibilityIssues.length} enabled sprite(s) not visible: ${visibilityIssues[0]}`; } else if (enabledCount === 0) { hint = "No sprites enabled"; } else { hint = `${enabledCount} sprite(s) enabled, ${visibleCount} visible`; } return formatResponse({ count: sprites.length, enabledCount, visibleCount, sprites, spriteMulticolor0: getColorInfo(vicData[0x25]), spriteMulticolor1: getColorInfo(vicData[0x26]), issues: allIssues.length > 0 ? allIssues : undefined, hint, }); } catch (error) { return formatError(error as ViceError); } } );
- src/index.ts:1374-1523 (registration)Registration of the 'readSprites' tool with MCP server, including full description and Zod input schema allowing optional 'enabledOnly' boolean.server.registerTool( "readSprites", { description: `Read state of all 8 hardware sprites with interpreted values. Returns for each sprite: - Position (X, Y) with visibility check - Color (with name) - Enable status - Multicolor mode - X/Y expansion (double size) - Priority (in front of / behind background) - Data pointer address Use this to debug sprite issues like: - "Why is my sprite invisible?" → check enabled, position, pointer - "Wrong colors?" → check multicolor mode and color registers - "Wrong size?" → check expand flags Options: - enabledOnly: Only return enabled sprites (default: false) Related tools: readVicState, readMemory (for sprite data)`, inputSchema: z.object({ enabledOnly: z .boolean() .optional() .describe("Only return enabled sprites (default: false)"), }), }, async (args) => { try { // Read VIC registers const vicData = await client.readMemory(0xd000, 0xd02e); // Read CIA2 for bank info (needed for sprite pointer calculation) const cia2Data = await client.readMemory(0xdd00, 0xdd00); const bankInfo = getVicBank(cia2Data[0]); // Get screen address for sprite pointers const d018 = vicData[0x18]; const videoAddrs = getVideoAddresses(d018, bankInfo.baseAddress); // Sprite pointers are at screen + $3F8 const spritePointerBase = videoAddrs.screenAddress + 0x3f8; const spritePointers = await client.readMemory( spritePointerBase, spritePointerBase + 7 ); const spriteEnable = vicData[0x15]; const spriteXMsb = vicData[0x10]; const spriteYExpand = vicData[0x17]; const spriteXExpand = vicData[0x1d]; const spriteMulticolor = vicData[0x1c]; const spritePriority = vicData[0x1b]; const sprites = []; for (let i = 0; i < 8; i++) { const enabled = !!(spriteEnable & (1 << i)); // Skip disabled sprites if enabledOnly if (args.enabledOnly && !enabled) continue; // X position (9-bit) const xLow = vicData[i * 2]; const xHigh = (spriteXMsb & (1 << i)) ? 256 : 0; const x = xLow + xHigh; // Y position (8-bit) const y = vicData[i * 2 + 1]; const visibility = isSpriteVisible(x, y, enabled); // Sprite data address with region validation const pointer = spritePointers[i]; const dataAddress = bankInfo.baseAddress + pointer * 64; const addressInfo = validateSpriteDataAddress(dataAddress); sprites.push({ index: i, enabled, position: { x, y, visible: visibility.visible, visibilityReason: visibility.reason, }, color: getColorInfo(vicData[0x27 + i]), multicolor: !!(spriteMulticolor & (1 << i)), expandX: !!(spriteXExpand & (1 << i)), expandY: !!(spriteYExpand & (1 << i)), priority: (spritePriority & (1 << i)) ? "behind" : "front", pointer: { value: pointer, hex: `$${pointer.toString(16).padStart(2, "0")}`, }, dataAddress: { value: dataAddress, hex: `$${dataAddress.toString(16).padStart(4, "0")}`, region: addressInfo.region, severity: addressInfo.severity, warning: addressInfo.warning, }, }); } const enabledCount = sprites.filter((s) => s.enabled).length; const visibleCount = sprites.filter((s) => s.position.visible).length; // Collect visibility issues const visibilityIssues = sprites .filter((s) => s.enabled && !s.position.visible) .map((s) => `Sprite ${s.index}: ${s.position.visibilityReason}`); // Collect data address issues (warnings and errors) const addressIssues = sprites .filter((s) => s.enabled && s.dataAddress.warning) .map((s) => `Sprite ${s.index}: ${s.dataAddress.warning} (${s.dataAddress.hex})`); const allIssues = [...visibilityIssues, ...addressIssues]; // Build hint based on most critical issue let hint: string; if (addressIssues.length > 0) { hint = `⚠️ ${addressIssues.length} sprite(s) with suspicious data address: ${addressIssues[0]}`; } else if (visibilityIssues.length > 0) { hint = `${visibilityIssues.length} enabled sprite(s) not visible: ${visibilityIssues[0]}`; } else if (enabledCount === 0) { hint = "No sprites enabled"; } else { hint = `${enabledCount} sprite(s) enabled, ${visibleCount} visible`; } return formatResponse({ count: sprites.length, enabledCount, visibleCount, sprites, spriteMulticolor0: getColorInfo(vicData[0x25]), spriteMulticolor1: getColorInfo(vicData[0x26]), issues: allIssues.length > 0 ? allIssues : undefined, hint, }); } catch (error) { return formatError(error as ViceError); } } );
- src/index.ts:1398-1403 (schema)Zod input schema for readSprites tool: optional boolean 'enabledOnly' to filter only enabled sprites.enabledOnly: z .boolean() .optional() .describe("Only return enabled sprites (default: false)"), }), },
- src/utils/c64.ts:150-164 (helper)Helper function used in readSprites to check if sprite position (x,y) is within visible screen area (24-343 x, 50-249 y) and enabled. Returns visibility status and reason if invisible.export function isSpriteVisible(x: number, y: number, enabled: boolean): { visible: boolean; reason?: string; } { if (!enabled) { return { visible: false, reason: "Sprite is disabled ($D015)" }; } if (x < SPRITE_VISIBLE_X_MIN || x > SPRITE_VISIBLE_X_MAX) { return { visible: false, reason: `X position ${x} is outside visible range (${SPRITE_VISIBLE_X_MIN}-${SPRITE_VISIBLE_X_MAX})` }; } if (y < SPRITE_VISIBLE_Y_MIN || y > SPRITE_VISIBLE_Y_MAX) { return { visible: false, reason: `Y position ${y} is outside visible range (${SPRITE_VISIBLE_Y_MIN}-${SPRITE_VISIBLE_Y_MAX})` }; } return { visible: true }; }
- src/utils/c64.ts:193-277 (helper)Validates sprite data address against C64 memory map regions, categorizes as ok/warning/error with explanations (e.g., error in I/O, warning in BASIC area). Used in readSprites for dataAddress field.export function validateSpriteDataAddress(address: number): SpriteDataAddressInfo { // Zero page - very wrong for sprite data if (address < 0x0100) { return { region: "Zero page", severity: "error", warning: "Sprite data in zero page - likely wrong pointer", }; } // Stack - definitely wrong if (address < 0x0200) { return { region: "Stack", severity: "error", warning: "Sprite data in stack area - definitely wrong", }; } // BASIC program area - might conflict with code if (address >= 0x0801 && address < 0x2000) { return { region: "BASIC program area", severity: "warning", warning: "Sprite data in BASIC area - may conflict with program", }; } // Default screen RAM - usually wrong if (address >= 0x0400 && address < 0x0800) { return { region: "Default screen RAM", severity: "warning", warning: "Sprite data in screen RAM - probably wrong", }; } // Common sprite data areas - OK if (address >= 0x2000 && address < 0x4000) { return { region: "Common sprite area", severity: "ok", }; } // VIC bank 1 sprite area - OK if (address >= 0x4000 && address < 0x8000) { return { region: "VIC bank 1 sprite area", severity: "ok", }; } // VIC bank 2 - has ROM shadow issues if (address >= 0x8000 && address < 0xc000) { return { region: "VIC bank 2", severity: "ok", }; } // I/O area - cannot store sprite data here if (address >= 0xd000 && address < 0xe000) { return { region: "I/O space", severity: "error", warning: "Cannot store sprite data in I/O space", }; } // High memory - ROM shadow area if (address >= 0xe000) { return { region: "KERNAL ROM area", severity: "warning", warning: "Sprite data may conflict with KERNAL ROM shadow", }; } // Other areas return { region: describeAddress(address) || "RAM", severity: "ok", }; }