using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Nodes; using System.Threading.Tasks; namespace X16Debug; /// /// One-shot command implementations for CLI mode. /// public static class Commands { public static async Task RunOneShotAsync(X16DebugClient client, string cmd, string[] args) { try { switch (cmd.ToLower()) { case "step": case "s": return await DoStep(client); case "continue": case "c": return await DoContinue(client); case "break": case "b": return await DoBreak(client); case "status": case "t": return await DoStatus(client); case "reset": return await DoReset(client); case "regs": case "r": return await DoRegs(client); case "setreg": case "p": return await DoSetReg(client, args); case "mem": case "m": return await DoMem(client, args); case "setmem": return await DoSetMem(client, args); case "vram": case "v": return await DoVram(client, args); case "setvram": return await DoSetVram(client, args); case "joy": case "j": return await DoJoy(client, args); case "bp": return await DoBp(client, args); default: Console.Error.WriteLine($"Unknown command: {cmd}"); return 1; } } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } } static async Task DoStep(X16DebugClient client) { var r = await client.StepInAsync(); if (!DapSuccess(r)) return 1; Console.WriteLine("OK (step issued, awaiting stopped notification)"); return 0; } static async Task DoContinue(X16DebugClient client) { var r = await client.ContinueAsync(); PrintDapResult(r, "Running..."); return DapSuccess(r) ? 0 : 1; } static async Task DoBreak(X16DebugClient client) { var r = await client.PauseAsync(); if (DapSuccess(r)) { // After pause, fetch and display registers try { await PrintRegistersFromServer(client); } catch { Console.WriteLine("Paused (registers not yet available)"); } } else PrintDapError(r); return DapSuccess(r) ? 0 : 1; } static async Task DoStatus(X16DebugClient client) { var threads = await client.GetThreadsAsync(); if (DapSuccess(threads)) { var threadList = threads["body"]?["threads"]?.AsArray(); if (threadList != null && threadList.Count > 0) { string name = threadList[0]?["name"]?.GetValue() ?? "CPU"; Console.WriteLine($"Thread: {name}"); } // Try to get stack trace for current position try { var stack = await client.GetStackTraceAsync(1); var frames = stack["body"]?["stackFrames"]?.AsArray(); if (frames != null && frames.Count > 0) { string frameName = frames[0]?["name"]?.GetValue() ?? "unknown"; Console.WriteLine($"Frame: {frameName}"); } await PrintRegistersFromServer(client); } catch { Console.WriteLine("(CPU may be running, registers not available)"); } } else PrintDapError(threads); return 0; } static async Task DoReset(X16DebugClient client) { var r = await client.EvaluateAsync("reset"); PrintDapResult(r); return 0; } static async Task DoRegs(X16DebugClient client) { try { await PrintRegistersFromServer(client); } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } return 0; } /// /// Map the short names a user types onto the names the Registers scope uses. /// static string ResolveRegisterName(string reg) => reg.ToLowerInvariant() switch { "a" => "A", "x" => "X", "y" => "Y", "sp" or "s" => "SP", "pc" => "PC", "p" or "status" or "flags" => "P (Status)", "d" or "dp" => "D", "dbr" => "DBR", "pbr" or "k" => "PBR", "rambank" or "ram" or "bka" => "RAM Bank", "rombank" or "rom" or "bko" => "ROM Bank", _ => reg.ToUpperInvariant(), }; static async Task DoSetReg(X16DebugClient client, string[] args) { if (args.Length < 2) { Console.Error.WriteLine("Usage: setreg (e.g., setreg a FF)"); return 1; } // setVariable, not evaluate: the evaluate expression "A=FF" is parsed as // a read, answers success and changes nothing. string name = ResolveRegisterName(args[0]); var r = await client.SetVariableAsync(name, $"${args[1].TrimStart('$')}"); if (!DapSuccess(r)) { PrintDapError(r); return 1; } string? shown = r["body"]?["value"]?.GetValue(); Console.WriteLine($"OK: {name} = {shown ?? "$" + args[1]}"); return 0; } /// /// Parse the address/length forms both mem and vram accept: /// "1030-1040" (inclusive range), "0800 10" (hex length), or "0800" (16 bytes). /// static bool TryParseRange(string[] args, out uint addr, out int length, out string error) { addr = 0; length = 0; error = ""; try { if (args[0].Contains('-')) { var parts = args[0].Split('-'); uint start = Convert.ToUInt32(parts[0], 16); uint end = Convert.ToUInt32(parts[1], 16); if (end < start) { error = "end address is before the start"; return false; } addr = start; length = (int)(end - start + 1); } else if (args.Length >= 2) { addr = Convert.ToUInt32(args[0], 16); length = (int)Convert.ToUInt32(args[1], 16); } else { addr = Convert.ToUInt32(args[0], 16); length = 16; } } catch (Exception ex) { error = ex.Message; return false; } if (length <= 0) { error = "length must be at least 1"; return false; } return true; } static async Task DoMem(X16DebugClient client, string[] args) { if (args.Length < 1) { Console.Error.WriteLine("Usage: mem - or mem "); return 1; } if (!TryParseRange(args, out uint addrVal, out int length, out string err)) { Console.Error.WriteLine($"Error: {err}"); return 1; } var r = await client.ReadMemoryAsync((int)addrVal, length); return PrintMemoryResponse(r, addrVal, 4); } static async Task DoSetMem(X16DebugClient client, string[] args) { if (args.Length < 2) { Console.Error.WriteLine("Usage: setmem (e.g., setmem 0400 DEADBEEF)"); return 1; } return await WriteBytes(client, $"${args[0]}", args[1]); } // ─── VRAM ─────────────────────────────────────────────────────── // // VERA's video RAM is a separate 17-bit space, reached with a "vram:" // memoryReference. The CPU cannot address it directly -- it goes through the // data port a byte at a time -- so these are not just mem with a different // number. static async Task DoVram(X16DebugClient client, string[] args) { if (args.Length < 1) { Console.Error.WriteLine("Usage: vram - or vram "); return 1; } if (!TryParseRange(args, out uint addrVal, out int length, out string err)) { Console.Error.WriteLine($"Error: {err}"); return 1; } var r = await client.ReadMemoryAsync($"vram:{addrVal:X}", length); return PrintMemoryResponse(r, addrVal, 5); } static async Task DoSetVram(X16DebugClient client, string[] args) { if (args.Length < 2) { Console.Error.WriteLine("Usage: setvram (e.g., setvram 1B000 DEADBEEF)"); return 1; } uint addr; try { addr = Convert.ToUInt32(args[0], 16); } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } return await WriteBytes(client, $"vram:{addr:X}", args[1]); } /// /// Shared tail of setmem/setvram: decode the hex payload and write it, then /// read the same span back so the caller sees what actually landed rather /// than a bare OK. /// static async Task WriteBytes(X16DebugClient client, string reference, string hex) { string clean = hex.Replace(" ", "").Replace("$", ""); if (clean.Length == 0 || clean.Length % 2 != 0) { Console.Error.WriteLine("Error: hex data must be an even number of hex digits"); return 1; } byte[] data; try { data = Convert.FromHexString(clean); } catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; } var w = await client.WriteMemoryAsync(reference, data); if (!DapSuccess(w)) { PrintDapError(w); return 1; } int written = w["body"]?["bytesWritten"]?.GetValue() ?? data.Length; Console.WriteLine($"Wrote {written} byte{(written == 1 ? "" : "s")} to {reference}"); var r = await client.ReadMemoryAsync(reference, data.Length); if (DapSuccess(r)) { uint baseAddr = 0; string numeric = reference.StartsWith("vram:", StringComparison.OrdinalIgnoreCase) ? reference[5..] : reference; numeric = numeric.TrimStart('$'); try { baseAddr = Convert.ToUInt32(numeric, 16); } catch { } PrintMemoryResponse(r, baseAddr, reference.StartsWith("vram:", StringComparison.OrdinalIgnoreCase) ? 5 : 4); } return 0; } // ─── Joystick ─────────────────────────────────────────────────── static readonly string[] JoyButtonNames = { "up", "down", "left", "right", "a", "b", "x", "y", "start", "select", "l", "r" }; static async Task DoJoy(X16DebugClient client, string[] args) { if (args.Length < 1) { Console.Error.WriteLine("Usage: joy [buttons...|release|connect]"); Console.Error.WriteLine($" buttons: {string.Join(", ", JoyButtonNames)}"); Console.Error.WriteLine(" joy 1 query port 1 without changing it"); Console.Error.WriteLine(" joy 1 a right hold A and Right, release everything else"); Console.Error.WriteLine(" joy 1 none release every button, stay connected"); Console.Error.WriteLine(" joy 1 release hand the port back to a real gamepad"); return 1; } // Ports are 1-4 here to match -joy1..-joy4 and BASIC's JOY(n); the DAP // request indexes them from 0. if (!int.TryParse(args[0], out int port) || port < 1 || port > 4) { Console.Error.WriteLine("Error: port must be 1, 2, 3 or 4"); return 1; } int index = port - 1; JsonNode r; if (args.Length == 1) { r = await client.JoystickAsync(index); } else if (args[1].Equals("release", StringComparison.OrdinalIgnoreCase) || args[1].Equals("off", StringComparison.OrdinalIgnoreCase)) { r = await client.JoystickAsync(index, enabled: false); } else if (args[1].Equals("connect", StringComparison.OrdinalIgnoreCase) || args[1].Equals("none", StringComparison.OrdinalIgnoreCase)) { // A connected controller with nothing held. Distinct from an empty // port: the KERNAL can tell the difference, and a game that waits for // a controller needs to see one. r = await client.JoystickAsync(index, buttons: Array.Empty()); } else { var held = args[1..]; foreach (var b in held) { if (Array.IndexOf(JoyButtonNames, b.ToLowerInvariant()) < 0) { Console.Error.WriteLine($"Error: unknown button '{b}'"); Console.Error.WriteLine($" known: {string.Join(", ", JoyButtonNames)}"); return 1; } } r = await client.JoystickAsync(index, buttons: held); } if (!DapSuccess(r)) { PrintDapError(r); return 1; } PrintJoystick(port, r); return 0; } static void PrintJoystick(int port, JsonNode r) { var b = r["body"]; if (b == null) { Console.WriteLine("OK"); return; } int mask = b["mask"]?.GetValue() ?? 0xFFFF; bool driven = b["enabled"]?.GetValue() ?? false; bool bound = b["controllerBound"]?.GetValue() ?? false; var pressed = new List(); var arr = b["buttons"]?.AsArray(); if (arr != null) foreach (var n in arr) if (n != null) pressed.Add(n.GetValue()); string source = driven ? "virtual" : bound ? "gamepad" : "empty"; string held = pressed.Count > 0 ? string.Join(" ", pressed) : "(nothing held)"; Console.WriteLine($" Joystick {port}: {held}"); Console.WriteLine($" mask=${mask:X4} source={source}"); } /// /// Decode a readMemory response and hex-dump it. width is the number of /// address digits: 4 for the CPU map, 5 for VRAM's 17-bit space. /// static int PrintMemoryResponse(JsonNode r, uint baseAddr, int width) { if (!DapSuccess(r)) { PrintDapError(r); return 1; } string? b64Data = r["body"]?["data"]?.GetValue(); if (b64Data == null) { Console.Error.WriteLine("Error: no data in response"); return 1; } PrintMemoryDump(baseAddr, Convert.ToHexString(Convert.FromBase64String(b64Data)), width); return 0; } static async Task DoBp(X16DebugClient client, string[] args) { if (args.Length < 1) { Console.Error.WriteLine("Usage: bp add|remove|list|clear [addr]"); return 1; } switch (args[0].ToLower()) { case "add": case "a": if (args.Length < 2) { Console.Error.WriteLine("Usage: bp add "); return 1; } var ra = await client.AddBreakpointAsync((int)Convert.ToUInt32(args[1], 16)); PrintDapResult(ra); return DapSuccess(ra) ? 0 : 1; case "remove": case "r": if (args.Length < 2) { Console.Error.WriteLine("Usage: bp remove "); return 1; } var rr = await client.RemoveBreakpointAsync((int)Convert.ToUInt32(args[1], 16)); PrintDapResult(rr); return DapSuccess(rr) ? 0 : 1; case "list": case "l": var rl = await client.ListBreakpointsAsync(); if (DapSuccess(rl)) { string? result = rl["body"]?["result"]?.GetValue(); if (string.IsNullOrWhiteSpace(result) || result == "none") Console.WriteLine("No breakpoints set"); else Console.WriteLine($"Breakpoints: {result}"); } else PrintDapError(rl); return 0; case "clear": case "x": var rc = await client.ClearBreakpointsAsync(); PrintDapResult(rc); return 0; default: Console.Error.WriteLine($"Unknown bp action: {args[0]}"); return 1; } } // ─── DAP helpers ──────────────────────────────────────────────── static bool DapSuccess(JsonNode r) => r["success"]?.GetValue() == true; static void PrintDapError(JsonNode r) { string? msg = r["message"]?.GetValue(); if (msg != null) Console.Error.WriteLine($"Error: {msg}"); else Console.Error.WriteLine("Error: request failed"); } static void PrintDapResult(JsonNode r, string? successMsg = null) { if (DapSuccess(r)) Console.WriteLine(successMsg ?? "OK"); else PrintDapError(r); } /// /// Fetch registers via DAP (stackTrace -> scopes -> variables) and print them. /// public static async Task PrintRegistersFromServer(X16DebugClient client) { var varsResponse = await client.GetRegistersAsync(); var variables = varsResponse["body"]?["variables"]?.AsArray(); if (variables == null) { Console.Error.WriteLine("No register data available"); return; } // Build a dictionary from the DAP variables array var regs = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var v in variables) { string? name = v?["name"]?.GetValue(); string? val = v?["value"]?.GetValue(); if (name != null && val != null) regs[name] = val; } PrintRegs(regs); } // ─── Display helpers ──────────────────────────────────────────── /// /// Print registers from a DAP variables dictionary (name -> hex value). /// public static void PrintRegs(Dictionary regs) { // DAP variable values are formatted as "$XX (decimal)" or "$XX [flags]" // Register names from DAP: PC, A, X, Y, SP, "P (Status)", "RAM Bank", "ROM Bank" string a = GetReg(regs, "A", "??"); string x = GetReg(regs, "X", "??"); string y = GetReg(regs, "Y", "??"); string sp = GetReg(regs, "SP", "??"); string pc = GetReg(regs, "PC", "????"); string p = GetReg(regs, "P (Status)", GetReg(regs, "P", "??")); string bka = GetReg(regs, "RAM Bank", GetReg(regs, "BKA", "??")); string bko = GetReg(regs, "ROM Bank", GetReg(regs, "BKO", "??")); // Extract just the hex portion from DAP values like "$02 (2)" -> "02" string aHex = ExtractHex(a); string xHex = ExtractHex(x); string yHex = ExtractHex(y); string pHex = ExtractHex(p); string pcHex = ExtractHex(pc); string spHex = ExtractHex(sp); string bkaHex = ExtractHex(bka); string bkoHex = ExtractHex(bko); byte pByte = 0; try { pByte = Convert.ToByte(pHex, 16); } catch { } string flags = FormatFlags(pByte); try { Console.WriteLine($" PC=${pcHex} A=${aHex} ({Convert.ToByte(aHex, 16),3}) X=${xHex} ({Convert.ToByte(xHex, 16),3}) Y=${yHex} ({Convert.ToByte(yHex, 16),3})"); } catch { Console.WriteLine($" PC=${pcHex} A=${aHex} X=${xHex} Y=${yHex}"); } Console.WriteLine($" SP=${spHex} P=${pHex} [{flags}] RAM Bank=${bkaHex} ROM Bank=${bkoHex}"); } static string GetReg(Dictionary regs, string name, string fallback) { if (regs.TryGetValue(name, out var val)) return val; return fallback; } /// /// Extract hex value from DAP format like "$02 (2)" or "$C1C6 (49606)" or "$1E [---B-I--]" /// Returns just the hex digits (e.g., "02", "C1C6", "1E") /// static string ExtractHex(string val) { if (string.IsNullOrEmpty(val) || val == "??" || val == "????") return val; // Strip leading $ string s = val; if (s.StartsWith("$")) s = s[1..]; if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) s = s[2..]; // Take only hex digits before any space or other delimiter int end = 0; while (end < s.Length && Uri.IsHexDigit(s[end])) end++; return end > 0 ? s[..end] : val; } static string FormatFlags(byte p) { char n = (p & 0x80) != 0 ? 'N' : '-'; char v = (p & 0x40) != 0 ? 'V' : '-'; char b = (p & 0x10) != 0 ? 'B' : '-'; char d = (p & 0x08) != 0 ? 'D' : '-'; char i = (p & 0x04) != 0 ? 'I' : '-'; char z = (p & 0x02) != 0 ? 'Z' : '-'; char c = (p & 0x01) != 0 ? 'C' : '-'; return $"{n}{v}-{b}{d}{i}{z}{c}"; } public static void PrintMemoryDump(uint baseAddr, string hexData, int addrWidth = 4) { int numBytes = hexData.Length / 2; int offset = 0; while (offset < numBytes) { uint lineAddr = baseAddr + (uint)offset; int lineStart = offset; int lineEnd = Math.Min(offset + 16, numBytes); var hexPart = new StringBuilder(); var asciiPart = new StringBuilder(); for (int i = lineStart; i < lineEnd; i++) { string byteHex = hexData.Substring(i * 2, 2); hexPart.Append(byteHex); hexPart.Append(' '); byte b = Convert.ToByte(byteHex, 16); asciiPart.Append(b >= 0x20 && b < 0x7F ? (char)b : '.'); } // Pad if less than 16 bytes for (int i = lineEnd - lineStart; i < 16; i++) { hexPart.Append(" "); asciiPart.Append(' '); } Console.WriteLine($" {lineAddr.ToString($"X{addrWidth}")}: {hexPart} |{asciiPart}|"); offset = lineEnd; } } }