Add semantic layer tools for interpreted C64 debugging
New tools: - readScreen: Screen RAM as ASCII text with non-empty line summary - readColorRam: Color RAM with color names and usage statistics - readVicState: Full VIC-II state with interpreted values - Graphics mode detection - Memory bank/address calculation - Sprite enable summary - readSprites: All 8 sprites with position, visibility, colors - Visibility checks with explanations - Data pointer address resolution - enabledOnly filtering option Utilities (src/utils/c64.ts): - PETSCII screen code to ASCII conversion - C64 color palette names - VIC bank and memory address calculation - Graphics mode detection - Sprite visibility range checks AX patterns applied: - Summary modes (readColorRam summary option) - Filtering (readSprites enabledOnly option) - Rich hints explaining issues (sprite visibility) - Proactive issue detection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
84b10689d0
commit
37f53616ff
3 changed files with 608 additions and 0 deletions
427
src/index.ts
427
src/index.ts
|
|
@ -4,6 +4,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getViceClient, ViceError } from "./protocol/index.js";
|
import { getViceClient, ViceError } from "./protocol/index.js";
|
||||||
|
import {
|
||||||
|
getColorInfo,
|
||||||
|
screenToText,
|
||||||
|
getVicBank,
|
||||||
|
getVideoAddresses,
|
||||||
|
getGraphicsMode,
|
||||||
|
isSpriteVisible,
|
||||||
|
} from "./utils/index.js";
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "vice-mcp",
|
name: "vice-mcp",
|
||||||
|
|
@ -647,6 +655,425 @@ Related tools: setBreakpoint, deleteBreakpoint`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SEMANTIC LAYER TOOLS - Interpreted output for autonomous debugging
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Tool: readScreen - Get screen contents as text
|
||||||
|
server.registerTool(
|
||||||
|
"readScreen",
|
||||||
|
{
|
||||||
|
description: `Read the C64 screen memory and return it as interpreted text.
|
||||||
|
|
||||||
|
Converts PETSCII screen codes to readable ASCII. Returns 25 lines of 40 characters.
|
||||||
|
|
||||||
|
Use this instead of readMemory($0400) when you want to see what's displayed on screen.
|
||||||
|
|
||||||
|
Note: This reads from the current screen RAM location (may not be $0400 if the program moved it).
|
||||||
|
In bitmap modes, the data won't represent text.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- includeRaw: Also return raw screen codes (default: false)
|
||||||
|
|
||||||
|
Related tools: readColorRam, readVicState, readMemory`,
|
||||||
|
inputSchema: z.object({
|
||||||
|
includeRaw: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Include raw screen codes array (default: false)"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
// First get VIC bank from CIA2
|
||||||
|
const cia2Data = await client.readMemory(0xdd00, 0xdd00);
|
||||||
|
const bankInfo = getVicBank(cia2Data[0]);
|
||||||
|
|
||||||
|
// Get screen address from $D018
|
||||||
|
const d018Data = await client.readMemory(0xd018, 0xd018);
|
||||||
|
const videoAddrs = getVideoAddresses(d018Data[0], bankInfo.baseAddress);
|
||||||
|
|
||||||
|
// Read screen RAM (1000 bytes)
|
||||||
|
const screenData = await client.readMemory(
|
||||||
|
videoAddrs.screenAddress,
|
||||||
|
videoAddrs.screenAddress + 999
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to text
|
||||||
|
const textLines = screenToText(screenData);
|
||||||
|
|
||||||
|
// Find non-empty lines for summary
|
||||||
|
const nonEmptyLines = textLines
|
||||||
|
.map((line, idx) => ({ line: idx, content: line }))
|
||||||
|
.filter((l) => l.content.trim().length > 0);
|
||||||
|
|
||||||
|
const response: Record<string, unknown> = {
|
||||||
|
screenAddress: {
|
||||||
|
value: videoAddrs.screenAddress,
|
||||||
|
hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`,
|
||||||
|
},
|
||||||
|
vicBank: bankInfo.bank,
|
||||||
|
lines: textLines,
|
||||||
|
summary: {
|
||||||
|
nonEmptyLines: nonEmptyLines.length,
|
||||||
|
preview:
|
||||||
|
nonEmptyLines.length > 0
|
||||||
|
? nonEmptyLines.slice(0, 3).map((l) => `Line ${l.line}: "${l.content}"`)
|
||||||
|
: ["Screen appears empty"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.includeRaw) {
|
||||||
|
response.raw = Array.from(screenData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check graphics mode and add hint
|
||||||
|
const d011Data = await client.readMemory(0xd011, 0xd011);
|
||||||
|
const d016Data = await client.readMemory(0xd016, 0xd016);
|
||||||
|
const graphicsMode = getGraphicsMode(d011Data[0], d016Data[0]);
|
||||||
|
|
||||||
|
response.graphicsMode = graphicsMode.mode;
|
||||||
|
|
||||||
|
if (graphicsMode.bitmap) {
|
||||||
|
response.hint =
|
||||||
|
"Warning: VIC-II is in bitmap mode - screen RAM contains bitmap data, not text.";
|
||||||
|
} else if (nonEmptyLines.length === 0) {
|
||||||
|
response.hint = "Screen appears empty or contains only spaces.";
|
||||||
|
} else {
|
||||||
|
response.hint = `Screen has ${nonEmptyLines.length} non-empty line(s). First: "${nonEmptyLines[0]?.content || ""}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError(error as ViceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tool: readColorRam - Get color RAM state
|
||||||
|
server.registerTool(
|
||||||
|
"readColorRam",
|
||||||
|
{
|
||||||
|
description: `Read color RAM ($D800-$DBE7) and return color values with names.
|
||||||
|
|
||||||
|
Color RAM determines the foreground color of each character on screen.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- 25x40 grid of color values (0-15) with names
|
||||||
|
- Summary of colors used
|
||||||
|
|
||||||
|
Related tools: readScreen, readVicState`,
|
||||||
|
inputSchema: z.object({
|
||||||
|
summary: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Return only color usage summary, not full grid (default: false)"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
// Color RAM is always at $D800
|
||||||
|
const colorData = await client.readMemory(0xd800, 0xd800 + 999);
|
||||||
|
|
||||||
|
// Count color usage
|
||||||
|
const colorCounts = new Map<number, number>();
|
||||||
|
for (const byte of colorData) {
|
||||||
|
const color = byte & 0x0f;
|
||||||
|
colorCounts.set(color, (colorCounts.get(color) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by frequency
|
||||||
|
const colorUsage = Array.from(colorCounts.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([color, count]) => ({
|
||||||
|
color: getColorInfo(color),
|
||||||
|
count,
|
||||||
|
percentage: Math.round((count / 1000) * 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response: Record<string, unknown> = {
|
||||||
|
address: { value: 0xd800, hex: "$D800" },
|
||||||
|
summary: {
|
||||||
|
uniqueColors: colorUsage.length,
|
||||||
|
dominantColor: colorUsage[0]?.color || null,
|
||||||
|
usage: colorUsage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!args.summary) {
|
||||||
|
// Convert to 25 lines of 40 color values
|
||||||
|
const colorLines: Array<Array<{ value: number; name: string }>> = [];
|
||||||
|
for (let row = 0; row < 25; row++) {
|
||||||
|
const line: Array<{ value: number; name: string }> = [];
|
||||||
|
for (let col = 0; col < 40; col++) {
|
||||||
|
const offset = row * 40 + col;
|
||||||
|
line.push(getColorInfo(colorData[offset]));
|
||||||
|
}
|
||||||
|
colorLines.push(line);
|
||||||
|
}
|
||||||
|
response.grid = colorLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.hint =
|
||||||
|
colorUsage.length === 1
|
||||||
|
? `Entire screen uses ${colorUsage[0].color.name} (${colorUsage[0].color.value})`
|
||||||
|
: `${colorUsage.length} colors used. Dominant: ${colorUsage[0]?.color.name} (${colorUsage[0]?.percentage}%)`;
|
||||||
|
|
||||||
|
return formatResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError(error as ViceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tool: readVicState - Full VIC-II chip state
|
||||||
|
server.registerTool(
|
||||||
|
"readVicState",
|
||||||
|
{
|
||||||
|
description: `Read the full VIC-II state with interpreted values.
|
||||||
|
|
||||||
|
Returns all VIC-II registers with semantic meaning:
|
||||||
|
- Border and background colors (with names)
|
||||||
|
- Graphics mode (text, bitmap, multicolor, etc.)
|
||||||
|
- Screen and character memory locations
|
||||||
|
- Scroll values
|
||||||
|
- Raster position
|
||||||
|
- Sprite enable bits
|
||||||
|
|
||||||
|
This is the high-level view of the video chip. Use for understanding display configuration.
|
||||||
|
|
||||||
|
Related tools: readScreen, readSprites, readMemory (for $D000-$D02E)`,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// Read all VIC registers $D000-$D02E (47 bytes)
|
||||||
|
const vicData = await client.readMemory(0xd000, 0xd02e);
|
||||||
|
|
||||||
|
// Read CIA2 for bank info
|
||||||
|
const cia2Data = await client.readMemory(0xdd00, 0xdd00);
|
||||||
|
const bankInfo = getVicBank(cia2Data[0]);
|
||||||
|
|
||||||
|
const d011 = vicData[0x11];
|
||||||
|
const d016 = vicData[0x16];
|
||||||
|
const d018 = vicData[0x18];
|
||||||
|
|
||||||
|
const graphicsMode = getGraphicsMode(d011, d016);
|
||||||
|
const videoAddrs = getVideoAddresses(d018, bankInfo.baseAddress);
|
||||||
|
|
||||||
|
// Raster position (9-bit)
|
||||||
|
const rasterLine = vicData[0x12] | ((d011 & 0x80) << 1);
|
||||||
|
|
||||||
|
// Sprite enable
|
||||||
|
const spriteEnable = vicData[0x15];
|
||||||
|
const enabledSprites = [];
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
if (spriteEnable & (1 << i)) enabledSprites.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display enable
|
||||||
|
const displayEnabled = !!(d011 & 0x10);
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
// Colors
|
||||||
|
borderColor: getColorInfo(vicData[0x20]),
|
||||||
|
backgroundColor: [
|
||||||
|
getColorInfo(vicData[0x21]),
|
||||||
|
getColorInfo(vicData[0x22]),
|
||||||
|
getColorInfo(vicData[0x23]),
|
||||||
|
getColorInfo(vicData[0x24]),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Graphics mode
|
||||||
|
graphicsMode: graphicsMode.mode,
|
||||||
|
displayEnabled,
|
||||||
|
bitmap: graphicsMode.bitmap,
|
||||||
|
multicolor: graphicsMode.multicolor,
|
||||||
|
extendedColor: graphicsMode.extendedColor,
|
||||||
|
|
||||||
|
// Screen geometry
|
||||||
|
rows: d011 & 0x08 ? 25 : 24,
|
||||||
|
columns: d016 & 0x08 ? 40 : 38,
|
||||||
|
scrollX: d016 & 0x07,
|
||||||
|
scrollY: d011 & 0x07,
|
||||||
|
|
||||||
|
// Memory setup
|
||||||
|
vicBank: {
|
||||||
|
bank: bankInfo.bank,
|
||||||
|
baseAddress: {
|
||||||
|
value: bankInfo.baseAddress,
|
||||||
|
hex: `$${bankInfo.baseAddress.toString(16).padStart(4, "0")}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
screenAddress: {
|
||||||
|
value: videoAddrs.screenAddress,
|
||||||
|
hex: `$${videoAddrs.screenAddress.toString(16).padStart(4, "0")}`,
|
||||||
|
},
|
||||||
|
charAddress: {
|
||||||
|
value: videoAddrs.charAddress,
|
||||||
|
hex: `$${videoAddrs.charAddress.toString(16).padStart(4, "0")}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Raster
|
||||||
|
rasterLine,
|
||||||
|
|
||||||
|
// Sprites summary
|
||||||
|
spriteEnable: {
|
||||||
|
value: spriteEnable,
|
||||||
|
binary: spriteEnable.toString(2).padStart(8, "0"),
|
||||||
|
enabledSprites,
|
||||||
|
count: enabledSprites.length,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sprite multicolor registers
|
||||||
|
spriteMulticolor0: getColorInfo(vicData[0x25]),
|
||||||
|
spriteMulticolor1: getColorInfo(vicData[0x26]),
|
||||||
|
|
||||||
|
hint: !displayEnabled
|
||||||
|
? "Display is blanked (DEN=0) - screen shows border color only"
|
||||||
|
: enabledSprites.length > 0
|
||||||
|
? `${graphicsMode.mode} mode, ${enabledSprites.length} sprite(s) enabled. Use readSprites() for sprite details.`
|
||||||
|
: `${graphicsMode.mode} mode, no sprites enabled.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return formatResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError(error as ViceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tool: readSprites - Get detailed sprite state
|
||||||
|
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
|
||||||
|
const pointer = spritePointers[i];
|
||||||
|
const dataAddress = bankInfo.baseAddress + pointer * 64;
|
||||||
|
|
||||||
|
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")}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledCount = sprites.filter((s) => s.enabled).length;
|
||||||
|
const visibleCount = sprites.filter((s) => s.position.visible).length;
|
||||||
|
const issues = sprites
|
||||||
|
.filter((s) => s.enabled && !s.position.visible)
|
||||||
|
.map((s) => `Sprite ${s.index}: ${s.position.visibilityReason}`);
|
||||||
|
|
||||||
|
return formatResponse({
|
||||||
|
count: sprites.length,
|
||||||
|
enabledCount,
|
||||||
|
visibleCount,
|
||||||
|
sprites,
|
||||||
|
spriteMulticolor0: getColorInfo(vicData[0x25]),
|
||||||
|
spriteMulticolor1: getColorInfo(vicData[0x26]),
|
||||||
|
issues: issues.length > 0 ? issues : undefined,
|
||||||
|
hint:
|
||||||
|
issues.length > 0
|
||||||
|
? `${issues.length} enabled sprite(s) not visible: ${issues[0]}`
|
||||||
|
: enabledCount === 0
|
||||||
|
? "No sprites enabled"
|
||||||
|
: `${enabledCount} sprite(s) enabled, ${visibleCount} visible`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return formatError(error as ViceError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|
|
||||||
180
src/utils/c64.ts
Normal file
180
src/utils/c64.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// C64 Constants and Utilities
|
||||||
|
|
||||||
|
// Color palette names
|
||||||
|
export const C64_COLORS = [
|
||||||
|
"black",
|
||||||
|
"white",
|
||||||
|
"red",
|
||||||
|
"cyan",
|
||||||
|
"purple",
|
||||||
|
"green",
|
||||||
|
"blue",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"brown",
|
||||||
|
"light red",
|
||||||
|
"dark gray",
|
||||||
|
"gray",
|
||||||
|
"light green",
|
||||||
|
"light blue",
|
||||||
|
"light gray",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type C64Color = (typeof C64_COLORS)[number];
|
||||||
|
|
||||||
|
export function getColorName(value: number): C64Color {
|
||||||
|
return C64_COLORS[value & 0x0f];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorInfo(value: number): { value: number; name: C64Color } {
|
||||||
|
return {
|
||||||
|
value: value & 0x0f,
|
||||||
|
name: getColorName(value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// PETSCII to ASCII conversion (screen codes, not PETSCII character codes)
|
||||||
|
// Screen codes are different from PETSCII - this handles the screen memory values
|
||||||
|
export function screenCodeToAscii(code: number): string {
|
||||||
|
// Screen codes 0-31: @, A-Z, [, £, ], ↑, ←
|
||||||
|
if (code <= 31) {
|
||||||
|
if (code === 0) return "@";
|
||||||
|
if (code <= 26) return String.fromCharCode(64 + code); // A-Z
|
||||||
|
if (code === 27) return "[";
|
||||||
|
if (code === 28) return "£";
|
||||||
|
if (code === 29) return "]";
|
||||||
|
if (code === 30) return "^"; // up arrow
|
||||||
|
if (code === 31) return "<"; // left arrow
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32-63: space, !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, 0-9, :, ;, <, =, >, ?
|
||||||
|
if (code >= 32 && code <= 63) {
|
||||||
|
return String.fromCharCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 64-95: horizontal line, graphics chars - use placeholders
|
||||||
|
if (code >= 64 && code <= 95) {
|
||||||
|
return "#"; // graphics placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 96-127: more graphics
|
||||||
|
if (code >= 96 && code <= 127) {
|
||||||
|
return "#"; // graphics placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 128-255: reverse video versions of 0-127
|
||||||
|
if (code >= 128) {
|
||||||
|
// Just show the base character (non-reversed)
|
||||||
|
return screenCodeToAscii(code - 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert screen RAM to text lines
|
||||||
|
export function screenToText(screenData: Buffer | number[]): string[] {
|
||||||
|
const data = Buffer.isBuffer(screenData) ? screenData : Buffer.from(screenData);
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
for (let row = 0; row < 25; row++) {
|
||||||
|
const offset = row * 40;
|
||||||
|
let line = "";
|
||||||
|
for (let col = 0; col < 40; col++) {
|
||||||
|
if (offset + col < data.length) {
|
||||||
|
line += screenCodeToAscii(data[offset + col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim trailing spaces but keep the line
|
||||||
|
lines.push(line.trimEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIC-II memory bank calculation
|
||||||
|
export function getVicBank(cia2PortA: number): { bank: number; baseAddress: number } {
|
||||||
|
// CIA2 port A bits 0-1 (inverted) select the bank
|
||||||
|
const bankBits = (~cia2PortA) & 0x03;
|
||||||
|
return {
|
||||||
|
bank: bankBits,
|
||||||
|
baseAddress: bankBits * 0x4000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen and character base from $D018
|
||||||
|
export function getVideoAddresses(
|
||||||
|
d018: number,
|
||||||
|
bankBase: number
|
||||||
|
): { screenAddress: number; charAddress: number } {
|
||||||
|
const screenOffset = ((d018 >> 4) & 0x0f) * 0x0400;
|
||||||
|
const charOffset = ((d018 >> 1) & 0x07) * 0x0800;
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenAddress: bankBase + screenOffset,
|
||||||
|
charAddress: bankBase + charOffset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graphics mode from D011 and D016
|
||||||
|
export function getGraphicsMode(d011: number, d016: number): {
|
||||||
|
mode: string;
|
||||||
|
bitmap: boolean;
|
||||||
|
multicolor: boolean;
|
||||||
|
extendedColor: boolean;
|
||||||
|
} {
|
||||||
|
const ecm = !!(d011 & 0x40);
|
||||||
|
const bmm = !!(d011 & 0x20);
|
||||||
|
const mcm = !!(d016 & 0x10);
|
||||||
|
|
||||||
|
let mode = "standard text";
|
||||||
|
if (ecm && !bmm && !mcm) mode = "extended background color";
|
||||||
|
else if (!ecm && !bmm && mcm) mode = "multicolor text";
|
||||||
|
else if (!ecm && bmm && !mcm) mode = "standard bitmap";
|
||||||
|
else if (!ecm && bmm && mcm) mode = "multicolor bitmap";
|
||||||
|
else if (ecm) mode = "invalid (ECM + other modes)";
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
bitmap: bmm,
|
||||||
|
multicolor: mcm,
|
||||||
|
extendedColor: ecm,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprite position visible range helpers
|
||||||
|
export const SPRITE_VISIBLE_X_MIN = 24;
|
||||||
|
export const SPRITE_VISIBLE_X_MAX = 343;
|
||||||
|
export const SPRITE_VISIBLE_Y_MIN = 50;
|
||||||
|
export const SPRITE_VISIBLE_Y_MAX = 249;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory region descriptions
|
||||||
|
export function describeAddress(address: number): string {
|
||||||
|
if (address < 0x0100) return "Zero page";
|
||||||
|
if (address < 0x0200) return "Stack";
|
||||||
|
if (address >= 0x0400 && address < 0x0800) return "Default screen RAM";
|
||||||
|
if (address >= 0x0800 && address < 0x1000) return "Default char ROM shadow";
|
||||||
|
if (address >= 0xa000 && address < 0xc000) return "BASIC ROM / RAM";
|
||||||
|
if (address >= 0xd000 && address < 0xd400) return "VIC-II registers";
|
||||||
|
if (address >= 0xd400 && address < 0xd800) return "SID registers";
|
||||||
|
if (address >= 0xd800 && address < 0xdc00) return "Color RAM";
|
||||||
|
if (address >= 0xdc00 && address < 0xdd00) return "CIA1 registers";
|
||||||
|
if (address >= 0xdd00 && address < 0xde00) return "CIA2 registers";
|
||||||
|
if (address >= 0xe000) return "KERNAL ROM / RAM";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
1
src/utils/index.ts
Normal file
1
src/utils/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./c64.js";
|
||||||
Loading…
Reference in a new issue