using System;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
namespace X16Debug;
///
/// Interactive text-menu mode for live debugging.
///
public static class InteractiveMode
{
// Hold a reference to the client for the event handler to use for register fetching
private static X16DebugClient? _activeClient;
public static async Task RunAsync(X16DebugClient client)
{
_activeClient = client;
client.OnEvent += OnEvent;
PrintBanner();
while (true)
{
Console.Write("> ");
string? input = Console.ReadLine();
if (input == null) break; // EOF
input = input.Trim();
if (string.IsNullOrEmpty(input)) continue;
// Single-char hotkeys
if (input.Length == 1)
{
switch (input[0])
{
case 's': await DoStep(client); continue;
case 'c': await DoContinue(client); continue;
case 'b': await DoBreak(client); continue;
case 'r': await DoRegs(client); continue;
case 't': await DoStatus(client); continue;
case 'l': await DoBpList(client); continue;
case 'x': await DoBpClear(client); continue;
case 'q': return;
case '?':
case 'h': PrintHelp(); continue;
}
}
// Parse full commands
var parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
string cmd = parts[0].ToLower();
string[] args = parts.Length > 1 ? parts[1..] : Array.Empty();
switch (cmd)
{
case "step":
case "s":
await DoStep(client);
break;
case "continue":
case "c":
await DoContinue(client);
break;
case "break":
case "b":
await DoBreak(client);
break;
case "status":
case "t":
await DoStatus(client);
break;
case "reset":
await DoReset(client);
break;
case "regs":
case "r":
await DoRegs(client);
break;
case "setreg":
case "p":
if (args.Length < 2)
Console.WriteLine("Usage: setreg ");
else
await Commands.RunOneShotAsync(client, "setreg", args);
break;
case "mem":
case "m":
if (args.Length < 1)
Console.WriteLine("Usage: mem - or mem ");
else
await Commands.RunOneShotAsync(client, "mem", args);
break;
case "setmem":
if (args.Length < 2)
Console.WriteLine("Usage: setmem ");
else
await Commands.RunOneShotAsync(client, "setmem", args);
break;
case "vram":
case "v":
if (args.Length < 1)
Console.WriteLine("Usage: vram - or vram ");
else
await Commands.RunOneShotAsync(client, "vram", args);
break;
case "setvram":
if (args.Length < 2)
Console.WriteLine("Usage: setvram ");
else
await Commands.RunOneShotAsync(client, "setvram", args);
break;
case "joy":
case "j":
await Commands.RunOneShotAsync(client, "joy", args);
break;
case "bp":
await Commands.RunOneShotAsync(client, "bp", args);
break;
case "quit":
case "q":
return;
case "help":
case "?":
PrintHelp();
break;
default:
Console.WriteLine($"Unknown command: {cmd}. Type 'h' for help.");
break;
}
}
client.OnEvent -= OnEvent;
_activeClient = null;
}
static void OnEvent(JsonNode evt)
{
string eventType = evt["event"]?.GetValue() ?? "unknown";
if (eventType == "stopped")
{
var body = evt["body"];
string reason = body?["reason"]?.GetValue() ?? "unknown";
Console.WriteLine();
Console.WriteLine($"! STOPPED ({reason})");
// DAP stopped events don't include registers; fetch them asynchronously
if (_activeClient != null)
{
_ = Task.Run(async () =>
{
try
{
await Commands.PrintRegistersFromServer(_activeClient);
}
catch { /* registers may not be available yet */ }
Console.Write("> ");
});
}
else
{
Console.Write("> ");
}
}
else if (eventType == "output")
{
var body = evt["body"];
string? output = body?["output"]?.GetValue();
if (output != null)
{
Console.Write(output);
}
}
else
{
Console.WriteLine($"\n! Event: {eventType}");
Console.Write("> ");
}
}
static async Task DoStep(X16DebugClient client)
{
try
{
var r = await client.StepInAsync();
if (r["success"]?.GetValue() != true)
Console.WriteLine($"Error: {r["message"]}");
// The stopped event will arrive via OnEvent and print registers
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoContinue(X16DebugClient client)
{
try
{
var r = await client.ContinueAsync();
if (r["success"]?.GetValue() == true)
Console.WriteLine("Running...");
else
Console.WriteLine($"Error: {r["message"]}");
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoBreak(X16DebugClient client)
{
try
{
var r = await client.PauseAsync();
if (r["success"]?.GetValue() == true)
Console.WriteLine("Pause requested...");
else
Console.WriteLine($"Error: {r["message"]}");
// The stopped event will arrive via OnEvent and print registers
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoStatus(X16DebugClient client)
{
try
{
await Commands.RunOneShotAsync(client, "status", Array.Empty());
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoReset(X16DebugClient client)
{
try
{
var r = await client.EvaluateAsync("reset");
if (r["success"]?.GetValue() == true)
{
Console.WriteLine("Reset OK");
try { await Commands.PrintRegistersFromServer(client); }
catch { /* may not be available immediately */ }
}
else
Console.WriteLine($"Error: {r["message"]}");
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoRegs(X16DebugClient client)
{
try
{
await Commands.PrintRegistersFromServer(client);
}
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
}
static async Task DoBpList(X16DebugClient client)
{
await Commands.RunOneShotAsync(client, "bp", new[] { "list" });
}
static async Task DoBpClear(X16DebugClient client)
{
await Commands.RunOneShotAsync(client, "bp", new[] { "clear" });
}
static void PrintBanner()
{
Console.WriteLine("╔═══════════════════════════════════════╗");
Console.WriteLine("║ X16 Debug Client (DAP) - Connected ║");
Console.WriteLine("╠═══════════════════════════════════════╣");
Console.WriteLine("║ [s] Step [c] Continue ║");
Console.WriteLine("║ [b] Break [r] Registers ║");
Console.WriteLine("║ [m] Memory [v] VRAM ║");
Console.WriteLine("║ [j] Joystick [p] Set register ║");
Console.WriteLine("║ [t] Status [l] List BPs ║");
Console.WriteLine("║ [x] Clear BPs [q] Quit ║");
Console.WriteLine("╠═══════════════════════════════════════╣");
Console.WriteLine("║ Type full command or ? for help ║");
Console.WriteLine("╚═══════════════════════════════════════╝");
}
static void PrintHelp()
{
Console.WriteLine();
Console.WriteLine("Commands:");
Console.WriteLine(" s, step Step one instruction");
Console.WriteLine(" c, continue Continue execution");
Console.WriteLine(" b, break Pause execution");
Console.WriteLine(" r, regs Show registers");
Console.WriteLine(" t, status Show emulator status");
Console.WriteLine(" reset Reset CPU");
Console.WriteLine(" m, mem Read memory (e.g., mem 1030-1040, mem 0800 10)");
Console.WriteLine(" setmem Write memory (e.g., setmem 0400 DEADBEEF)");
Console.WriteLine(" v, vram Read VERA video RAM (e.g., vram 1B000 20)");
Console.WriteLine(" setvram Write VRAM (e.g., setvram 1B000 DEADBEEF)");
Console.WriteLine(" j, joy [btns] Drive a SNES controller port 1-4");
Console.WriteLine(" p, setreg Set register (e.g., setreg a FF)");
Console.WriteLine(" bp add Add breakpoint");
Console.WriteLine(" bp remove Remove breakpoint");
Console.WriteLine(" l, bp list List breakpoints");
Console.WriteLine(" x, bp clear Clear all breakpoints");
Console.WriteLine(" q, quit Disconnect and exit");
Console.WriteLine(" ?, help Show this help");
Console.WriteLine();
Console.WriteLine("VRAM is VERA's 17-bit video RAM. It is not in the CPU map, so `mem 1B000`");
Console.WriteLine("and `vram 1B000` are two different memories at the same number:");
Console.WriteLine(" vram 1B000 20 Dump 32 bytes of the text screen");
Console.WriteLine(" setvram 1F9C0 DEAD Poke two bytes into sprite attributes");
Console.WriteLine();
Console.WriteLine("Joysticks need no gamepad and no -joy1..-joy4 flag. The button list is the");
Console.WriteLine("complete held state, so anything left off it is released:");
Console.WriteLine(" joy 1 Show what port 1 is reporting");
Console.WriteLine(" joy 1 right a Hold Right and A");
Console.WriteLine(" joy 1 start Hold only Start; Right and A let go");
Console.WriteLine(" joy 1 none Connected with nothing held (JOY(1) = $00)");
Console.WriteLine(" joy 1 release Empty the port again (JOY(1) = $FF)");
Console.WriteLine(" buttons: up down left right a b x y start select l r");
Console.WriteLine();
}
}