Skip to content

Headless app

Cross-platform console app that runs the emulator without any UI, rendering, audio, or user input. Driven entirely by CLI arguments and Lua scripts — useful for automation, scripting, and CI workflows.

Technologies:

  • UI: none.
  • Rendering: none (null renderer).
  • Input: none (null input handler).
  • Audio: none (null audio handler).
  • Scripting: Lua via MoonSharp.

Installation

See Desktop apps installation for package manager and manual download instructions.

Features

Systems

  • C64 — requires ROM files (Kernal, Basic, Chargen). See Systems / C64 / ROMs.
  • Oric Atmos 48K — requires the Atmos BASIC 1.1b ROM. See Systems / Oric / ROMs.
  • Generic computer — built-in example 6502 programs (Snake, Scroll, HelloWorld).

Lua scripting

The same Lua scripting API available in the Avalonia apps is fully supported here. Scripts are the primary way to control the emulator. Example scripts are included in the scripts/ directory:

Script Description
example_emulator_control.lua Query state, pause/resume/reset, state-change event hooks.
example_c64_basic_readwrite.lua Write a BASIC program via keyboard buffer and read it back.
example_c64_border_cycle.lua Cycle C64 border color via memory writes.
example_c64_download_and_run_prg.lua Download and run a .prg file over HTTP.
example_c64_load_d64.lua Load a .d64 disk image.
example_c64_screenshot.lua Save a screenshot to disk.
example_oric_basic_readwrite.lua Write an Oric BASIC program through the keyboard input path and read it back.
example_oric_download_and_run_tap.lua Download, load, and run the Oric Hello World TAP sample.
example_frameadvance.lua Advance frames and read frame count.
example_input_kb.lua Inject keyboard input.
example_input_joystick.lua Inject joystick input.
example_monitor.lua Interact with the machine code monitor.
example_file_io.lua Read and write files from Lua.
example_http.lua Make HTTP requests from Lua.
example_tcp_client.lua Connect to a TCP server from Lua.
example_store.lua Persist data between script runs using the key-value store.
example_quit.lua Quit the emulator from a script.

For the full Lua API reference, see Tools / Scripting / Lua API.

External debug adapter

The headless app supports the Debug Adapter Protocol (DAP) over TCP, allowing VS Code to attach a debugger while the emulator runs headless. See Tools / VSCode debugger / Debugging for the user guide.

Remote control

The headless app can expose a TCP remote control endpoint that lets external processes drive the running emulator. See Tools / Remote control. The remote protocol includes emu.savesnapshot and emu.loadsnapshot. Absolute paths are used as-is on the machine running the headless emulator; relative paths are resolved from the shared snapshot directory.

CLI arguments

The headless app is driven entirely from the command line.

There are two mutually exclusive ways to drive startup from the command line:

  • Scripting mode — a Lua script selects the system and controls start, load, and lifecycle.
  • Automated startup mode — CLI flags select a system, optionally load a program, and start it.

Parameters are grouped below into General parameters (system-agnostic — interpreted by the shared startup pipeline, valid for any system) and one group per system (interpreted by that system's plugin; currently only C64).

The Depends on column lists each parameter's requirements and any parameters it is mutually exclusive with. Availability differs between the Avalonia Desktop app and the Headless app where noted; an (Avalonia Desktop only) / (Headless only) marker means the flag is ignored by the other app.

Important

--script / --scriptDir are mutually exclusive with all automated-startup parameters (--system, --systemVariant, --start, --waitForSystemReady, --loadPrg, --loadPrgUrl, --runLoadedProgram, and the C64-specific parameters such as .d64, .crt, and BASIC paste startup). In scripting mode the Lua script is responsible for all emulator setup and lifecycle; combining the two modes is an error.

General parameters (system-agnostic)

These are interpreted by the shared startup pipeline and are valid for any system.

Updates

Handled before the emulator/UI starts; each prints its result and exits immediately. Update checks require a package-manager install (Homebrew / Scoop) — manual-download and development builds report not managed. For how detection works and the on/off setting, see Staying up to date.

Parameter Description Depends on Example
--version Print the app version and exit. — --version
--check-update Check for a newer release and print the result. Ignores the UpdateCheckEnabled setting and the CI / DOTNET6502_NO_UPDATE_CHECK suppression. — --check-update
--update Check and, if an update is available on a package-manager install, run the brew/scoop upgrade in the foreground, then exit. — --update

Host-specific notes: on Avalonia Desktop these flags run before the GUI window opens and, on Windows, attach to the invoking console so their output is visible. On Headless the output goes to stdout. The Remote Client exposes the same flags (see its Global options) but never checks automatically.

Scripting

Parameter Description Depends on Example
--script <path> Load and run a Lua script. Can be specified multiple times. Exclusive with all automated-startup parameters. --script scripts/demo.lua
--scriptDir <path> Override the effective script directory. When omitted, desktop hosts use the configured directory or ~/Documents/Highbyte/DotNet6502/scripts. Exclusive with all automated-startup parameters. --scriptDir ./scripts

System selection & lifecycle

Parameter Description Depends on Example
--system <name> Select a system (e.g. C64, Generic). — --system C64
--systemVariant <name> Select a system variant (e.g. C64PAL, C64NTSC). Requires --system. --systemVariant C64PAL
--start Auto-start the emulator after selection. Requires --system. --start
--waitForSystemReady Wait until the system reports ready (e.g. C64 BASIC prompt) before continuing. Requires --start. --waitForSystemReady
--loadPrg <path> Load a local .prg file into memory. Requires --start. Exclusive with --loadPrgUrl and C64 --loadD64 / --loadD64Url / --loadCrt / --loadCrtUrl. --loadPrg game.prg
--loadPrgUrl <url> (Avalonia Desktop only.) Fetch a .prg over HTTP(S) and load it into memory. Requires --start. Exclusive with --loadPrg and C64 --loadD64 / --loadD64Url / --loadCrt / --loadCrtUrl. --loadPrgUrl https://example.com/game.prg
--runLoadedProgram Run the loaded program after loading. Requires --start and a load source (--loadPrg / --loadPrgUrl, or C64 --loadD64 / --loadD64Url). --runLoadedProgram
--load-snapshot <path> Restore a full .d6502snap emulator-state snapshot. Relative paths are resolved from the shared snapshot directory. The snapshot's manifest determines the system, so no --system is needed. The machine is left paused after restore; add --start to resume. Exclusive with --system, --systemVariant, --loadPrg / --loadPrgUrl, --runLoadedProgram, and --script. --waitForSystemReady requires --start. --load-snapshot state.d6502snap

--loadPrg / --loadPrgUrl copy the file's bytes to the load address in its 2-byte header — but how a loaded .prg is interpreted and run is system-specific. The systems documented below may add behavior on top of this (e.g. based on the load address); see the relevant system's parameter group. Cartridge images use the C64-specific .crt startup flow instead of PRG loading.

Logging

Parameter Description Depends on Example
--console-log / -c (Avalonia Desktop only — Headless always logs to console.) Enable console logging output. — --console-log
--log-level <level> / -l <level> Console log level (Trace / Debug / Information / Warning / Error / Critical). — --log-level Debug

Diagnostics & auto-exit

Parameter Description Depends on Example
--show-storage-paths Print effective storage paths and exit without starting the app. Also supported by Headless. — --show-storage-paths
--stats-interval <seconds> (Avalonia Desktop only.) Log an instrumentation snapshot every N seconds after startup completes. Requires --start. --stats-interval 5
--exit-after <seconds> (Avalonia Desktop only.) Quit the app N seconds after startup completes. Requires --start. --exit-after 60

Debug adapter

For the user guide on attaching VS Code, see Tools / VSCode debugger / Debugging.

Parameter Description Depends on Example
--enableExternalDebug Start the VS Code debug adapter (DAP) over TCP. This is the flag that enables the server. — --enableExternalDebug
--debug-port <port> TCP port for the debug adapter (default 6502). Requires --enableExternalDebug. --debug-port 6502
--debug-bind-address <ip> IP address to bind the debug adapter to (default 127.0.0.1). Requires --enableExternalDebug. --debug-bind-address 0.0.0.0
--debug-wait Wait for a debug client to connect before starting. Requires --enableExternalDebug. --debug-wait

Remote control

For protocol and command list, see Tools / Remote control.

Parameter Description Depends on Example
--remote-port <port> Start the TCP remote control server on this port. — --remote-port 6510
--remote-bind-address <ip> IP address to bind the remote control server to (default 127.0.0.1). Requires --remote-port. --remote-bind-address 0.0.0.0
--allow-remote-quit (Headless only.) Allow the emu.quit remote control command to terminate the app. Requires --remote-port. --allow-remote-quit

C64 parameters (system-specific — Avalonia Desktop only)

These extend the general automated-startup flow and are interpreted by the C64 Avalonia shell plugin. They are not parsed by the Headless app today (it has no C64 shell plugin). They build on the general --system C64, --start, and --waitForSystemReady flags.

Loading a C64 .prg (BASIC vs machine-language)

The general --loadPrg / --loadPrgUrl flags copy the file's bytes to the load address in its 2-byte header. No separate parameter is needed for C64 BASIC programs; the C64 plugin adapts automatically based on the load address:

  • C64 BASIC program (load address $0801): after the load, the BASIC variable pointers are initialized automatically, and with --runLoadedProgram the program is started by typing RUN. This path requires --waitForSystemReady so the load and RUN happen after the BASIC prompt is up.
  • Machine-language program (any other load address): --runLoadedProgram sets the CPU program counter to the load address instead.

So a C64 BASIC .prg is loaded and run with --system C64 --start --waitForSystemReady --loadPrg prog.prg --runLoadedProgram.

BASIC source paste

Parameter Description Depends on Example
--basicText <text> Paste inline C64 BASIC source (plain text) after BASIC is ready. Requires --system C64, --start, --waitForSystemReady. Exclusive with --basicFile, --basicUrl, and any PRG / .d64 / .crt load. --basicText "10 print \"hi\":goto 10"
--basicFile <path> Paste C64 BASIC source read from a local file. Same as --basicText. --basicFile hello.bas
--basicUrl <url> Fetch C64 BASIC source over HTTP(S) and paste it. Same as --basicText. --basicUrl https://example.com/hello.bas
--runBasic Queue run + Return after the BASIC source is pasted. Requires --basicText / --basicFile / --basicUrl. --runBasic

Disk image (.d64)

Parameter Description Depends on Example
--loadD64 <path> Load a local C64 .d64 disk image. ZIP archives are also accepted; by default the first .d64 entry is used. Requires --system C64, --start, --waitForSystemReady, and exactly one of --d64Program / --diskMount. Exclusive with --loadPrg / --loadPrgUrl / --loadD64Url / --loadCrt / --loadCrtUrl. --loadD64 game.d64
--loadD64Url <url> (Avalonia Desktop only.) Fetch a .d64 over HTTP(S). ZIP archives are also accepted; by default the first .d64 entry is used. Same as --loadD64. Exclusive with --loadD64, the PRG loads, and the CRT loads. --loadD64Url https://example.com/game.d64
--loadD64ZipEntry <entry> Select an exact .d64 entry inside a ZIP archive. Use forward slashes for directories. Requires --loadD64 / --loadD64Url and the source must be a ZIP archive. --loadD64ZipEntry side-b/game.d64
--d64Program <name\|*> Direct-load a PRG from the disk image into memory (no disk mount). * selects the first directory entry. Requires --loadD64 / --loadD64Url. Exclusive with --diskMount. --d64Program "*"
--diskMount Mount the image in drive 8 and prepare LOAD"*",8,1 + RUN via the keyboard buffer. Requires --loadD64 / --loadD64Url. Exclusive with --d64Program. --diskMount

When loading a .d64, the general --runLoadedProgram flag controls whether the disk's run commands are pasted after the load / mount: LOAD"*",8,1 + RUN for --diskMount, just RUN for --d64Program.

Cartridge image (.crt)

Parameter Description Depends on Example
--loadCrt <path> Attach a local C64 .crt cartridge image at startup. ZIP archives containing exactly one .crt are also accepted. Requires --system C64, --start. --waitForSystemReady is not required. Exclusive with PRG, BASIC, and .d64 startup loads. --loadCrt fc3.crt
--loadCrtUrl <url> (Avalonia Desktop only.) Fetch and attach a .crt over HTTP(S). ZIP archives containing exactly one .crt are also accepted. Same as --loadCrt. Exclusive with --loadCrt. --loadCrtUrl https://example.com/fc3.crt
--loadCrtZipEntry <entry> Select an exact .crt entry inside a ZIP archive. Use forward slashes for directories. Requires --loadCrt / --loadCrtUrl and the source must be a ZIP archive. Allows archives with multiple .crt files when the entry is explicit. --loadCrtZipEntry carts/fc3.crt

Cartridge startup attaches the image after the C64 instance has been started, and the cartridge attach operation resets / boots the machine into the cartridge. The general --runLoadedProgram flag does not apply to .crt images.

Runtime config

These knobs override the C64 host config before the system starts. They apply for any C64 start path — plain --start, --loadPrg / --loadPrgUrl, BASIC paste, --loadD64 / --loadD64Url, or --loadCrt / --loadCrtUrl.

Parameter Description Depends on Example
--keyboardJoystickEnabled Force-enable the C64 keyboard-emulated joystick. Requires --system C64. --keyboardJoystickEnabled
--keyboardJoystickNumber <1\|2> C64 joystick port the keyboard emulates (and which gamepad port drives). Requires --system C64. Implies --keyboardJoystickEnabled. --keyboardJoystickNumber 2
--audioEnabled <true\|false> Override the C64 audio-enable config before start. Omit to keep the existing value. Requires --system C64. --audioEnabled false

Note

Several automated-startup parameters are currently wired only in the Avalonia Desktop app and are ignored by the headless app (each is marked (Avalonia Desktop only) in the table above):

  • URL load sources: --loadPrgUrl, --loadD64Url, and --loadCrtUrl (the headless app loads only a local --loadPrg).
  • C64 BASIC paste: --basicText / --basicFile / --basicUrl / --runBasic.
  • C64 .d64 startup: --loadD64 / --loadD64Url / --loadD64ZipEntry / --d64Program / --diskMount.
  • C64 .crt startup: --loadCrt / --loadCrtUrl / --loadCrtZipEntry.
  • C64 runtime config: --keyboardJoystickEnabled / --keyboardJoystickNumber / --audioEnabled.
  • Diagnostics & auto-exit: --stats-interval / --exit-after.

The C64-specific flows need a C64 shell plugin (to apply C64-runtime config and run the disk-mount / direct-load / BASIC-paste flow), which the headless app does not host. The shared helpers are host-agnostic, so adding equivalent flags to the headless app is possible but is out of scope for the initial release.

Examples

Examples below use dotnet-6502-headless as installed via Homebrew or Scoop.

  • Manual download (self-contained binary): replace with ./Highbyte.DotNet6502.App.Headless (or Highbyte.DotNet6502.App.Headless.exe on Windows).
  • Running from source: replace with dotnet run --project src/apps/Highbyte.DotNet6502.App.Headless --.

Start a C64 and run a Lua script (script owns all setup and lifecycle):

dotnet-6502-headless --script scripts/example_c64_basic_readwrite.lua

Start with debug adapter listening on 127.0.0.1:6502, waiting for client (no script):

dotnet-6502-headless --system C64 --start --enableExternalDebug --debug-port 6502 --debug-wait

Start with debug adapter listening on all interfaces at port 6502 (trusted networks only):

dotnet-6502-headless --system C64 --start --enableExternalDebug --debug-port 6502 --debug-bind-address 0.0.0.0 --debug-wait

Start with remote control server on port 6510 (loopback only):

dotnet-6502-headless --system C64 --start --remote-port 6510

Start an Oric Atmos with remote control enabled:

dotnet-6502-headless --system Oric --systemVariant ATMOS48K --start --remote-port 6510

Start with remote control server accessible from the network (trusted networks only):

dotnet-6502-headless --system C64 --start --remote-port 6510 --remote-bind-address 0.0.0.0

Start with remote control and allow emu.quit command:

dotnet-6502-headless --system C64 --start --remote-port 6510 --allow-remote-quit

Restore an emulator-state snapshot and resume running it (the snapshot's manifest determines the system — no --system needed):

dotnet-6502-headless --load-snapshot state.d6502snap --start

Restore a snapshot but leave it paused, with remote control so an external process can step/screenshot it:

dotnet-6502-headless --load-snapshot state.d6502snap --remote-port 6510

Example console output:

09:12:01 info: Program[0] Starting headless emulator.
09:12:01 info: Program[0] Creating configuration object.
09:12:01 info: Program[0] Initializing logging.
09:12:01 info: Program[0] Reading emulator config.
09:12:01 info: HeadlessHostApp[0] Creating headless host app.
09:12:01 info: HeadlessHostApp[0] Headless host app initialized.
09:12:01 info: HeadlessHostApp[0] Headless emulator running. Press Ctrl+C to exit.
09:12:01 info: Script[0] Waiting for C64 emulator to start...
09:12:02 info: Script[0] C64 emulator started. Waiting for BASIC to initialize...
09:12:03 info: Script[0] BASIC ready. Typing BASIC program...
09:12:05 info: Script[0] Retrieved BASIC source:
09:12:05 info: Script[0] 10 PRINT "HELLO FROM LUA"
                          20 GOTO 10
09:12:05 info: Script[0] Round-trip check PASSED: both lines found in retrieved source.

Configuration

The shipped appsettings.json contains packaged defaults. User overrides are read from the Headless host's appsettings.user.json under the OS local application data directory:

  • macOS/Linux: ~/.local/share/Highbyte/DotNet6502/Headless/appsettings.user.json
  • Windows: %LOCALAPPDATA%\Highbyte\DotNet6502\Headless\appsettings.user.json

When the ROM and script directory settings are empty, Headless uses the shared user content directories under ~/Documents/Highbyte/DotNet6502 (or the Windows Documents equivalent):

  • ROMs: roms/[SYSTEM]
  • Lua scripts: scripts
  • Snapshots: snapshots

Headless does not currently have a SnapshotDirectory setting. Startup --load-snapshot and TCP remote-control emu.savesnapshot / emu.loadsnapshot use absolute paths as-is and resolve relative paths from the shared snapshots directory above.

Run dotnet-6502-headless --show-storage-paths to print the effective user content, scripts, snapshots, settings, cache, and per-system ROM directories without starting the emulator.

Example overlay:

{
  "Highbyte.DotNet6502.Scripting": {
    "Enabled": true,
    "ScriptDirectory": ""
  },
  "Highbyte.DotNet6502.C64.Headless": {
    "SystemConfig": {
      "ROMDirectory": ""
    }
  },
  "Highbyte.DotNet6502.Oric.Headless": {
    "SystemConfig": {
      "ROMDirectory": ""
    }
  }
}

How to run locally for development

For development system requirements, see Development.

Visual Studio (Windows)

Open solution dotnet-6502.slnx. Set project Highbyte.DotNet6502.App.Headless as startup and start with F5.

VSCode

A launch configuration is included. Open the repo in VSCode and use the Headless launch profile.