feat(cli): add CLI entry point

This commit is contained in:
dualfroz committed 2026-08-27 14:40:14 +02:00
1 parent 99458b9d0b
commit 996bcedd0b
1 file changed
+133
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.IO;
using Chip8.Core;
using Chip8.Core.Audio;
using Chip8.Core.Export;
namespace Chip8.Cli;
static class Program
{
static int Main(string[] args)
{
if (args.Length == 0 || args[0] == "-h" || args[0] == "--help")
{
printUsage();
return args.Length == 0 ? 1 : 0;
}
switch (args[0])
{
case "asm":
return AsmCommand.run(args[1..]);
case "disasm":
return DisasmCommand.run(args[1..]);
case "debug":
return TuiDebugger.run(args[1..]);
}
Options options;
try
{
options = Options.parse(args);
}
catch (ArgumentException error)
{
Console.Error.WriteLine($"error: {error.Message}");
printUsage();
return 1;
}
if (!File.Exists(options.romPath))
{
Console.Error.WriteLine($"error: ROM not found: {options.romPath}");
return 1;
}
byte[] rom = File.ReadAllBytes(options.romPath);
var machine = new Machine(options.buildQuirks(), options.seed, options.buildPlatform());
machine.loadRom(rom);
if (options.headless || options.dumpPpmPath is not null || options.dumpWavPath is not null)
{
return runHeadless(machine, options);
}
RaylibFrontend.run(machine, options);
return 0;
}
static int runHeadless(Machine machine, Options options)
{
AudioTrack? track = null;
if (options.dumpWavPath is not null)
{
int frames = Math.Max(1, options.cycles / options.instructionsPerFrame);
track = new AudioRecorder().record(machine, frames, options.instructionsPerFrame);
}
else
{
for (int cycle = 0; cycle < options.cycles; cycle++)
{
machine.step();
if (cycle % options.instructionsPerFrame == options.instructionsPerFrame - 1)
{
machine.tickTimers();
}
}
}
if (machine.platform == Platform.Chip8)
{
Console.Write(AsciiScreen.renderFramed(machine.display));
}
else
{
Console.Write(AsciiScreen.render(machine.video));
}
int litPixels = machine.platform == Platform.Chip8
? AsciiScreen.litPixelCount(machine.display)
: AsciiScreen.litPixelCount(machine.video);
Console.WriteLine(
$"rom={Path.GetFileName(options.romPath)} cycles={options.cycles} " +
$"litPixels={litPixels} soundActive={machine.soundActive}");
if (options.dumpPpmPath is not null)
{
PpmImage.writeFile(options.dumpPpmPath, PpmImage.fromMachine(machine, options.buildPalette(), options.scale));
Console.WriteLine($"wrote frame to {options.dumpPpmPath}");
}
if (options.dumpWavPath is not null && track is not null)
{
WavFile.writeFile(options.dumpWavPath, track.toSamples(), AudioRecorder.defaultSampleRate);
Console.WriteLine($"wrote {track.durationSeconds:0.00}s of audio to {options.dumpWavPath}");
}
return 0;
}
static void printUsage()
{
Console.WriteLine("chip8-sharp - CHIP-8 / SUPER-CHIP / XO-CHIP emulator");
Console.WriteLine();
Console.WriteLine("usage: chip8 <rom> [options]");
Console.WriteLine(" chip8 asm <source.asm> [-o out.ch8] [--listing]");
Console.WriteLine(" chip8 disasm <rom.ch8> [--base 0x200]");
Console.WriteLine(" chip8 debug <rom.ch8> [--platform NAME]");
Console.WriteLine();
Console.WriteLine(" --headless run without a window, dump the framebuffer as ASCII");
Console.WriteLine(" --cycles N instructions to run in headless mode (default 2000)");
Console.WriteLine(" --ipf N instructions per 60Hz frame (default 11)");
Console.WriteLine(" --scale N window / PPM pixel scale (default 14)");
Console.WriteLine(" --seed N deterministic RNG seed");
Console.WriteLine(" --platform NAME chip8 (default), superchip, or xochip");
Console.WriteLine(" --quirks NAME auto (default), modern, cosmac, superchip, xochip");
Console.WriteLine(" --palette NAME octo (default), mono, grayscale, extended16");
Console.WriteLine(" --ppm PATH export the final frame as a PPM image");
Console.WriteLine(" --wav PATH export the buzzer audio as a WAV file");
Console.WriteLine();
Console.WriteLine("keys: 1234/QWER/ASDF/ZXCV map to the CHIP-8 hex pad");
}
}