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--runLoadedProgramthe program is started by typingRUN. This path requires--waitForSystemReadyso the load andRUNhappen after the BASIC prompt is up. - Machine-language program (any other load address):
--runLoadedProgramsets 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
.d64startup:--loadD64/--loadD64Url/--loadD64ZipEntry/--d64Program/--diskMount. - C64
.crtstartup:--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(orHighbyte.DotNet6502.App.Headless.exeon 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):
Start with debug adapter listening on 127.0.0.1:6502, waiting for client (no script):
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):
Start an Oric Atmos with remote control enabled:
Start with remote control server accessible from the network (trusted networks only):
Start with remote control and allow emu.quit command:
Restore an emulator-state snapshot and resume running it (the snapshot's manifest determines the system — no --system needed):
Restore a snapshot but leave it paused, with remote control so an external process can step/screenshot it:
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.