mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
cli: added simple scripting
This commit is contained in:
1 parent
a4b1d593a9
commit
3e462b0bcd
1 file changed
+263
-24
+263
-24
@@ -6,6 +6,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { initialize, compile, compileSourceFile, preload, listTools, listPlatforms, getToolForFilename, PLATFORM_PARAMS, TOOLS, store } from './testlib';
|
||||
import { isDebuggable } from '../common/baseplatform';
|
||||
import { KeyFlags } from '../common/emu';
|
||||
import { hex } from '../common/util';
|
||||
|
||||
interface CLIResult {
|
||||
@@ -212,7 +213,8 @@ function usage(): void {
|
||||
commands: {
|
||||
'compile': 'compile --platform <platform> [--tool <tool>] [--output <file>] [--symbols] [--save] <source>',
|
||||
'check': 'check --platform <platform> [--tool <tool>] <source>',
|
||||
'run': 'run (--platform <id> | --machine <module:ClassName>) [--frames N] [--output <file.png>] [--memdump start,end] [--info] <rom>',
|
||||
'run': 'run (--platform <id> | --machine <module:ClassName>) [--frames N] [--script "cmds"|file] [--output <file.png>] [--memdump start,end] [--info] <rom>',
|
||||
'script-cmds': RUN_SCRIPT_HELP,
|
||||
'list-tools': 'list-tools',
|
||||
'list-platforms': 'list-platforms',
|
||||
}
|
||||
@@ -338,6 +340,215 @@ async function doCompile(args: { [key: string]: string }, positional: string[],
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Run-script interpreter
|
||||
//
|
||||
// A simple command language for driving headless emulation, one command per
|
||||
// line (separated by newlines or ';'). '#' starts a comment.
|
||||
//
|
||||
// run N | wait N | frames N advance N frames (default 1)
|
||||
// runto ADDR [maxframes] advance until PC == ADDR (default cap 1000 frames)
|
||||
// key KEY press key (down, 3 frames, up)
|
||||
// keydown KEY | keyup KEY raw key events
|
||||
// mem START LEN hexdump memory
|
||||
// screen [START] [COLS] [ROWS] decode screen RAM to text (default $0400 40x25)
|
||||
// pc print PC + disassembly at PC
|
||||
// reset reset the platform/machine
|
||||
// echo TEXT print a message
|
||||
//
|
||||
// KEY can be a single character ('A', '5'), a name (ENTER, SPACE, LEFT, F1...),
|
||||
// or numeric ($41 / 65). Modifier prefixes: SHIFT+, CTRL+
|
||||
// Numbers can be decimal, $hex or 0xhex.
|
||||
|
||||
interface RunScriptContext {
|
||||
advance(): void;
|
||||
frameno(): number;
|
||||
readMem(addr: number): number;
|
||||
getPC(): number | null;
|
||||
sendKey(key: number, flags: number): void;
|
||||
reset(): void;
|
||||
disassemble(addr: number): { line: string; nbytes: number } | null;
|
||||
}
|
||||
|
||||
const RUN_SCRIPT_HELP = [
|
||||
'run N | wait N | frames N - advance N frames (default 1)',
|
||||
'runto ADDR [maxframes] - run until PC==ADDR (cap default 1000 frames)',
|
||||
'key KEY - press key (down, 3 frames, up)',
|
||||
'keydown KEY / keyup KEY - raw key down/up events',
|
||||
'mem START LEN - hexdump memory',
|
||||
'screen [START] [COLS] [ROWS] - decode screen RAM to text (default $0400 40x25)',
|
||||
'pc - print PC + disassembly at PC',
|
||||
'reset - reset platform/machine',
|
||||
'echo TEXT - print message',
|
||||
'(KEY: char, ENTER/SPACE/LEFT/UP/RIGHT/DOWN/F1.., $hex; prefixes SHIFT+, CTRL+)',
|
||||
].join('\n');
|
||||
|
||||
const KEY_NAMES: { [name: string]: number } = {
|
||||
'ENTER': 13, 'RETURN': 13, 'CR': 13,
|
||||
'SPACE': 32, 'ESC': 27, 'TAB': 9, 'BACKSPACE': 8, 'BS': 8,
|
||||
'DELETE': 46, 'DEL': 46, 'INSERT': 45,
|
||||
'LEFT': 37, 'UP': 38, 'RIGHT': 39, 'DOWN': 40,
|
||||
'HOME': 36, 'END': 35, 'PAGEUP': 33, 'PAGEDOWN': 34,
|
||||
'F1': 112, 'F2': 113, 'F3': 114, 'F4': 115, 'F5': 116, 'F6': 117,
|
||||
'F7': 118, 'F8': 119, 'F9': 120, 'F10': 121, 'F11': 122, 'F12': 123,
|
||||
};
|
||||
|
||||
function parseNum(s: string): number {
|
||||
s = s.trim();
|
||||
if (s.startsWith('$')) return parseInt(s.substring(1), 16);
|
||||
if (/^0x[0-9a-f]+$/i.test(s)) return parseInt(s, 16);
|
||||
if (/^\d+$/.test(s)) return parseInt(s, 10);
|
||||
throw new Error(`bad number '${s}'`);
|
||||
}
|
||||
|
||||
function parseKeyValue(tok: string): { key: number; flags: number } {
|
||||
var flags = 0;
|
||||
var t = tok.toUpperCase();
|
||||
for (;;) {
|
||||
if (t.startsWith('SHIFT+')) { flags |= KeyFlags.Shift; t = t.substring(6); }
|
||||
else if (t.startsWith('CTRL+')) { flags |= KeyFlags.Ctrl; t = t.substring(5); }
|
||||
else break;
|
||||
}
|
||||
if (KEY_NAMES[t] != null) return { key: KEY_NAMES[t], flags };
|
||||
if (t.length == 1) return { key: t.charCodeAt(0), flags };
|
||||
if (/^0X[0-9A-F]+$/.test(t)) return { key: parseInt(t, 16), flags };
|
||||
if (/^\d+$/.test(t)) return { key: parseInt(t, 10), flags };
|
||||
throw new Error(`unknown key '${tok}'`);
|
||||
}
|
||||
|
||||
// C64-style screen code -> ASCII (also close enough for VIC-20 et al)
|
||||
function screenCodeToChar(code: number): string {
|
||||
code &= 0xff;
|
||||
if (code >= 0x40) code &= 0x3f; // reverse-video / graphics variants
|
||||
if (code < 0x20) {
|
||||
const special = '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[£]^_';
|
||||
return special[code];
|
||||
}
|
||||
return String.fromCharCode(code); // $20-$3F identical to ASCII
|
||||
}
|
||||
|
||||
function hexdumpMem(readFn: (addr: number) => number, start: number, end: number): void {
|
||||
var len = end - start + 1;
|
||||
for (var ofs = 0; ofs < len; ofs += 16) {
|
||||
var line = `${hex(start + ofs, 4)}:`;
|
||||
var ascii = '';
|
||||
for (var i = 0; i < 16 && ofs + i < len; i++) {
|
||||
if (i === 8) line += ' ';
|
||||
var byte = readFn(start + ofs + i);
|
||||
line += ` ${hex(byte)}`;
|
||||
ascii += (byte >= 0x20 && byte < 0x7f) ? String.fromCharCode(byte) : '.';
|
||||
}
|
||||
process.stdout.write(`${line} ${ascii}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function executeRunScript(script: string, ctx: RunScriptContext): void {
|
||||
var lines = script.split(/\r?\n|;/);
|
||||
for (var ln of lines) {
|
||||
ln = ln.trim();
|
||||
if (!ln || ln.startsWith('#') || ln.startsWith('//')) continue;
|
||||
var tokens = ln.split(/\s+/);
|
||||
var cmd = tokens[0].toLowerCase();
|
||||
try {
|
||||
switch (cmd) {
|
||||
case 'run': case 'wait': case 'frames': {
|
||||
var n = tokens[1] ? parseNum(tokens[1]) : 1;
|
||||
for (var i = 0; i < n; i++) ctx.advance();
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] ran ${n} frame${n == 1 ? '' : 's'}\n`);
|
||||
break;
|
||||
}
|
||||
case 'runto': {
|
||||
if (!tokens[1]) throw new Error('runto requires an address');
|
||||
var addr = parseNum(tokens[1]);
|
||||
var maxf = tokens[2] ? parseNum(tokens[2]) : 1000;
|
||||
var startFrame = ctx.frameno();
|
||||
while (ctx.frameno() - startFrame < maxf) {
|
||||
ctx.advance();
|
||||
var pc = ctx.getPC();
|
||||
if (pc == null || pc === addr) break;
|
||||
}
|
||||
var pc = ctx.getPC();
|
||||
var hit = pc != null && pc === addr;
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] runto $${hex(addr, 4)}: ${hit ? 'HIT' : 'MISSED'} (pc=${pc != null ? '$' + hex(pc, 4) : '?'} after ${ctx.frameno() - startFrame} frames)\n`);
|
||||
break;
|
||||
}
|
||||
case 'key': case 'press': {
|
||||
if (!tokens[1]) throw new Error('key requires a key name');
|
||||
var { key, flags } = parseKeyValue(tokens[1]);
|
||||
ctx.sendKey(key, flags | KeyFlags.KeyDown);
|
||||
for (var i = 0; i < 3; i++) ctx.advance();
|
||||
ctx.sendKey(key, flags | KeyFlags.KeyUp);
|
||||
ctx.advance();
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] pressed ${tokens[1]} ($${hex(key, 2)})\n`);
|
||||
break;
|
||||
}
|
||||
case 'keydown': {
|
||||
if (!tokens[1]) throw new Error('keydown requires a key name');
|
||||
var { key, flags } = parseKeyValue(tokens[1]);
|
||||
ctx.sendKey(key, flags | KeyFlags.KeyDown);
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] keydown ${tokens[1]} ($${hex(key, 2)})\n`);
|
||||
break;
|
||||
}
|
||||
case 'keyup': {
|
||||
if (!tokens[1]) throw new Error('keyup requires a key name');
|
||||
var { key, flags } = parseKeyValue(tokens[1]);
|
||||
ctx.sendKey(key, flags | KeyFlags.KeyUp);
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] keyup ${tokens[1]} ($${hex(key, 2)})\n`);
|
||||
break;
|
||||
}
|
||||
case 'mem': {
|
||||
if (!tokens[1]) throw new Error('mem requires START [LEN]');
|
||||
var start = parseNum(tokens[1]);
|
||||
var len = tokens[2] ? parseNum(tokens[2]) : 16;
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] mem $${hex(start, 4)}+$${hex(len, 4)}:\n`);
|
||||
hexdumpMem(ctx.readMem, start, start + len - 1);
|
||||
break;
|
||||
}
|
||||
case 'screen': {
|
||||
var start = tokens[1] ? parseNum(tokens[1]) : 0x400;
|
||||
var cols = tokens[2] ? parseNum(tokens[2]) : 40;
|
||||
var rows = tokens[3] ? parseNum(tokens[3]) : 25;
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] screen at $${hex(start, 4)} (${cols}x${rows}):\n`);
|
||||
for (var y = 0; y < rows; y++) {
|
||||
var line = '';
|
||||
for (var x = 0; x < cols; x++) line += screenCodeToChar(ctx.readMem(start + y * cols + x));
|
||||
process.stdout.write(`|${line.replace(/\s+$/, '')}|\n`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'pc': {
|
||||
var pc = ctx.getPC();
|
||||
if (pc == null) throw new Error('no PC available');
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] PC=$${hex(pc, 4)}\n`);
|
||||
var addr = pc;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var d = ctx.disassemble(addr);
|
||||
if (!d) break;
|
||||
var bytesStr = '';
|
||||
for (var b = 0; b < d.nbytes; b++) bytesStr += hex(ctx.readMem(addr + b)) + ' ';
|
||||
process.stdout.write(` $${hex(addr, 4)} ${bytesStr.padEnd(12)} ${d.line}\n`);
|
||||
addr += d.nbytes;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'reset': {
|
||||
ctx.reset();
|
||||
process.stdout.write(`[frame ${ctx.frameno()}] reset\n`);
|
||||
break;
|
||||
}
|
||||
case 'echo': {
|
||||
process.stdout.write(ln.substring(cmd.length).trim() + '\n');
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`unknown command (try: ${RUN_SCRIPT_HELP.split('\n')[0].split(' ')[0]}, ...)`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
throw new Error(`script error on '${ln}': ${e.message}\nCommands:\n${RUN_SCRIPT_HELP}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function doRun(args: { [key: string]: string }, positional: string[]): Promise<void> {
|
||||
var platformId = args['platform'];
|
||||
var machine = args['machine'];
|
||||
@@ -359,6 +570,7 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
|
||||
var vid: { width: number; height: number } | null = null;
|
||||
var platformRunner: any = null;
|
||||
var machineInstance: any = null;
|
||||
var runner: any = null;
|
||||
|
||||
if (platformId) {
|
||||
// Platform mode: load platform module, mock video, run via Platform API
|
||||
@@ -366,11 +578,6 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
|
||||
platformRunner = new PlatformRunner(await loadPlatform(platformId));
|
||||
await platformRunner.start();
|
||||
platformRunner.loadROM("ROM", romData);
|
||||
for (var i = 0; i < frames; i++) {
|
||||
platformRunner.run();
|
||||
}
|
||||
pixels = platformRunner.pixels;
|
||||
vid = platformRunner.videoParams;
|
||||
} else {
|
||||
// Machine mode: load machine class directly
|
||||
var parts = machine.split(':');
|
||||
@@ -385,16 +592,58 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
|
||||
var [modname, clsname] = parts;
|
||||
var { MachineRunner, loadMachine } = await import('./runmachine');
|
||||
machineInstance = await loadMachine(modname, clsname);
|
||||
var runner = new MachineRunner(machineInstance);
|
||||
runner = new MachineRunner(machineInstance);
|
||||
runner.setup();
|
||||
machineInstance.loadROM(romData);
|
||||
for (var i = 0; i < frames; i++) {
|
||||
runner.run();
|
||||
}
|
||||
pixels = runner.pixels;
|
||||
vid = pixels ? (machineInstance as any).getVideoParams() : null;
|
||||
}
|
||||
|
||||
// Build the execution context for frame loop / run-script
|
||||
var frameno = 0;
|
||||
var advance = () => {
|
||||
if (platformRunner) platformRunner.run(); else runner.run();
|
||||
frameno++;
|
||||
};
|
||||
var debugTarget: any = platformRunner ? platformRunner.platform : machineInstance;
|
||||
var keyTarget: any = platformRunner ? (platformRunner.platform as any).machine : machineInstance;
|
||||
var readMem = (addr: number): number => {
|
||||
if (platformRunner) return platformRunner.platform.readAddress(addr);
|
||||
var m = machineInstance as any;
|
||||
return typeof m.readAddress === 'function' ? m.readAddress(addr) : m.readConst(addr);
|
||||
};
|
||||
var getPC = (): number | null => {
|
||||
try { return typeof debugTarget.getPC === 'function' ? debugTarget.getPC() : null; }
|
||||
catch (e) { return null; }
|
||||
};
|
||||
var sendKey = (key: number, flags: number) => {
|
||||
if (keyTarget && typeof keyTarget.setKeyInput === 'function') keyTarget.setKeyInput(key, key, flags);
|
||||
else if (typeof debugTarget.setKeyInput === 'function') debugTarget.setKeyInput(key, key, flags);
|
||||
else throw new Error('platform/machine does not support setKeyInput');
|
||||
};
|
||||
var disassemble = (addr: number) => {
|
||||
try { return typeof debugTarget.disassemble === 'function' ? debugTarget.disassemble(addr, readMem) : null; }
|
||||
catch (e) { return null; }
|
||||
};
|
||||
|
||||
if (args['script']) {
|
||||
// --script: inline commands or a path to a script file
|
||||
var scriptText = args['script'];
|
||||
if (fs.existsSync(scriptText)) scriptText = fs.readFileSync(scriptText, 'utf8');
|
||||
executeRunScript(scriptText, {
|
||||
advance,
|
||||
frameno: () => frameno,
|
||||
readMem,
|
||||
getPC,
|
||||
sendKey,
|
||||
reset: () => debugTarget.reset(),
|
||||
disassemble,
|
||||
});
|
||||
} else {
|
||||
for (var i = 0; i < frames; i++) advance();
|
||||
}
|
||||
|
||||
pixels = platformRunner ? platformRunner.pixels : runner.pixels;
|
||||
vid = pixels ? (platformRunner ? platformRunner.videoParams : (machineInstance as any).getVideoParams()) : null;
|
||||
|
||||
output({
|
||||
success: true,
|
||||
command: 'run',
|
||||
@@ -466,18 +715,8 @@ async function doRun(args: { [key: string]: string }, positional: string[]): Pro
|
||||
output({ success: false, command: 'run', error: 'Platform/machine does not support readAddress' });
|
||||
process.exit(1);
|
||||
}
|
||||
var len = end - start + 1;
|
||||
for (var ofs = 0; ofs < len; ofs += 16) {
|
||||
var line = `${c.cyan}$${hex(start + ofs, 4)}${c.reset}:`;
|
||||
var ascii = '';
|
||||
for (var i = 0; i < 16 && ofs + i < len; i++) {
|
||||
if (i === 8) line += ' ';
|
||||
var byte = readFn2(start + ofs + i);
|
||||
line += ` ${hex(byte)}`;
|
||||
ascii += (byte >= 0x20 && byte < 0x7f) ? String.fromCharCode(byte) : '.';
|
||||
}
|
||||
process.stderr.write(`${line} ${c.dim}${ascii}${c.reset}\n`);
|
||||
}
|
||||
process.stdout.write(`memdump $${hex(start, 4)}-$${hex(end, 4)}:\n`);
|
||||
hexdumpMem(readFn2, start, end);
|
||||
}
|
||||
|
||||
// Encode framebuffer as PNG if video is available
|
||||
|
||||
Reference in new issue
Block a user