C64Script is the scripting language built into C64 Stream.
It is used to automate the parts of a C64 streaming or capture setup that are otherwise repetitive: changing effects, typing commands into the C64, starting recordings, launching programs on the Ultimate 64, waiting for specific timing points, and checking captured output.
The language uses a line-oriented style that will feel natural to anyone who has used C64 BASIC.
This tutorial introduces the practical parts of the language by building from simple scripts to more useful automation examples. For full language rules, please see the C64Script language specification.
Good first uses for C64Script are:
- cycling effects or color palettes while a demo runs
- resetting the machine, typing a short BASIC program, and then executing it
- playing SIDs or running PRGs while advancing through them with precise automated timing
- fetching data from external websites or local programs to feed directly into the C64
- starting and stopping OBS recording
- taking a screenshot and comparing it with a known-good image for tests
- logging each step so you can see what happened later
C64Script runs only when you explicitly start it from the plugin properties, unless you enable the Auto-start script option yourself.
C64Script files use the .c64script extension. The plugin ships with examples in
data/scripts/, including:
| Script | What it demonstrates |
|---|---|
hello_world.c64script |
Variables, logging, labels, IF, GOTO, and WAIT |
demo_palette_cycle.c64script |
Arrays, FOR/NEXT, and PALETTE |
demo_effect_preset_cycle.c64script |
Effect presets and simple timing |
demo_basic_hello_world.c64script |
Resetting the C64 and typing a BASIC program |
demo_sid_playback_cycle.c64script |
Playing SID files from c64u: paths |
You can copy one of those scripts and edit it, or create a new text file with a
.c64script suffix.
- Open the properties for your C64 Stream source or C64 Stream Effects filter.
- In the scripting section, choose a
.c64scriptfile with Browse. - Click Start Script.
- Watch the script status, and check the OBS log if the script uses
PRINT.
Try the shipped palette demo first:
data/scripts/demo_palette_cycle.c64script
It cycles through palette names, waits two seconds between each one, then exits. That is a nice first test because it does not reset your C64 or touch files on the Ultimate.
Create a file called my-first.c64script:
REM My first C64Script
LOG "Starting my first script"
EFFECT "Classic CRT"
WAIT 2s
EFFECT "Green Monitor"
WAIT 2s
EFFECT "Default"
LOG "All done"This script writes a few lines to the script log, changes the effect, waits, and returns to the default look. It is deliberately simple: the first victory should come quickly.
The important ideas are:
REMstarts a comment.- Keywords are case-insensitive, but uppercase reads nicely if you grew up with BASIC.
WAIT 2swaits two seconds. You can also use500ms,2.5m,1h, and so on.- A script normally finishes when it reaches the end. You can use
STOPorENDwhen you want to be explicit.
You can use logging to better understand what's going on whilst a script runs. This is also helpful for debugging if something goes wrong.
LOGFILE "my-session.log" TRUNCATE
LOG "Session started at " + TIME$()
TRON
EFFECT "Vintage TV"
WAIT 2s
EFFECT "Default"
TROFF
LOG "Session finished"Use:
LOGFILEto choose a log file;LOGfor your own messages;TRONandTROFFto trace executed statements automatically;PRINTwhen you want output in the OBS log.
Variables are created when you first assign to them:
COUNT = 3
TITLE$ = "Welcome back to the breadbin"
LOG TITLE$
LOG "Count is " + STR$(COUNT)Some examples:
| Form | Meaning | Example |
|---|---|---|
NAME$ |
String | PATH$ = "c64u:/Games/demo.d64" |
COUNT |
Numeric | COUNT = 5 |
I% |
Integer-style variable | I% = 3 |
ITEMS$() |
String array | DIM ITEMS$(4) |
CONFIG${} |
String map | CONFIG${"host"} = "192.168.1.64" |
Use STR$() to turn a number into text and VAL() to turn text into a number:
COUNT = VAL(ENV("EFFECT_COUNT", "3"))
LOG "Running " + STR$(COUNT) + " effect cycles"That ENV() example is handy when you want the same script to run with different
settings from a test runner or shell.
Line numbers are supported, but you do not need them. Labels are usually easier to read:
COUNT = 0
AGAIN:
COUNT = COUNT + 1
LOG "Cycle " + STR$(COUNT)
EFFECT "Amber Monitor"
WAIT 1s
EFFECT "Green Monitor"
WAIT 1s
IF COUNT < 3 THEN GOTO AGAIN
EFFECT "Default"For counted loops, use FOR and NEXT:
FOR I = 1 TO 3
LOG "Pass " + STR$(I)
WAIT 500ms
NEXT IAnd when you have a list, arrays keep the script tidy:
DIM EFFECTS$(4)
EFFECTS$(0) = "Default"
EFFECTS$(1) = "Classic CRT"
EFFECTS$(2) = "Amber Monitor"
EFFECTS$(3) = "Green Monitor"
FOR I = 0 TO 3
EFFECT EFFECTS$(I)
WAIT 2s
NEXT IEffects and palettes are the safest place to start because they change the OBS presentation without touching the C64 state:
PALETTE "Default"
EFFECT "Classic CRT"
WAIT 3s
EFFECTPARAM "preserve_size" 1
EFFECT "Arcade Cabinet"
WAIT 3s
EFFECT "Default"EFFECTPARAM lets you tune effect parameters directly. The commonly useful
preserve_size parameter keeps the OBS source footprint stable while you change
effects.
You can also adjust individual C64 palette colors:
PALETTE "Default"
PALETTECOLOR 0, 0, 0, 0
PALETTECOLOR 1, 255, 255, 255
PALETTECOLOR 6, 0, 0, 170The color index is 0 to 15; RGB values are 0 to 255.
TYPE sends text through the C64 keyboard buffer. KEY sends a single key or
byte. This is excellent for BASIC loaders, menu selections, and simple startup
sequences:
LOG "Resetting and typing a tiny BASIC program"
RESET
WAIT 5s
TYPE "10 PRINT \"HELLO FROM C64SCRIPT\""
KEY 13
WAIT 500ms
TYPE "20 GOTO 10"
KEY 13
WAIT 500ms
TYPE "RUN"
KEY 13You can also put carriage returns directly into a string:
TYPE "LOAD\"*\",8,1\rRUN\r"
Two practical notes:
TYPEandKEYenqueue keystrokes; they do not prove the C64 has consumed them yet. Add aWAIT, or poll memory withPEEK()for tighter control.- Keyboard injection uses the KERNAL keyboard buffer. Programs that scan the CIA keyboard matrix directly may ignore it.
Ultimate commands can use paths on the device with the c64u: prefix, or local
files from the OBS computer where supported. Device paths are especially useful
for repeatable show setups:
DRIVE_MOUNT "c64u:/your/path/demo.d64"
RUNPRG "c64u:/your/path/intro.prg"
PLAYSID "c64u:/your/path/tune.sid" SONGNR 0DRIVE_MOUNT mounts a disk image, RUNPRG runs a PRG, and PLAYSID plays a
SID.
Use paths that really exist on your device, or use local files from the OBS computer if you prefer.
Some commands also have underscore aliases, such as PLAY_SID, RUN_PRG,
MOUNT_DISK, RUN_LOCAL, and PALETTE_COLOR. The compact forms above match the
language reference and are easy to type.
C64Script can control OBS recording:
LOG "Starting recording"
OBS RECORDING START
EFFECT "Classic CRT"
WAIT 5s
OBS RECORDING STOP
LOG "Recording stopped"Always pair OBS RECORDING START with OBS RECORDING STOP. If a script starts
recording and then finishes without stopping it, OBS keeps recording.
For screenshots:
EFFECT "Default"
OBS WAIT FRAMES 2
OBS SCREENSHOT TARGET SOURCE PATH "current-frame.png"OBS WAIT FRAMES is worth using before screenshots. It waits for fresh rendered
frames so you do not accidentally capture a stale image.
If you include a directory in the screenshot path, that parent directory must already exist.
Sometimes you want to check the video output of the C64 and compare it with a known expected state, e.g. as part of writing tests.
The following script captures a frame and compares the PNG with itself, proving that screenshots and image assertions are wired up:
LOG "Preparing image check"
EFFECT "Default"
OBS WAIT FRAMES 10
OBS SCREENSHOT TARGET SOURCE PATH "current.png"
ASSERT IMAGE_EQUALS "current.png", "current.png" TOLERANCE 0
LOG "Image matched"If the images differ, the assertion stops the script and writes a .diff.png
artifact beside the actual image. For real automated tests, compare the captured
file with a known-good PNG that you have already created and reviewed.
Use a small tolerance only when you genuinely expect harmless pixel differences.
For deterministic source captures, TOLERANCE 0 is the cleanest target.
C64Script can talk to the outside world too. This read-only example asks your Ultimate for its REST API version:
HTTP GET "http://c64u/v1/version" STATUS CODE RESPONSE BODY$
IF CODE = 200 THEN
LOG "Ultimate replied: " + BODY$
ELSE
LOG "Ultimate REST error: " + STR$(CODE)
ENDIFYou can run a local program:
RUNLOCAL "echo" ARGS "Hello from the host" STATUS EXIT_CODE OUTPUT OUT$
LOG "Exit code: " + STR$(EXIT_CODE)
LOG OUT$And read or write local files:
WRITEFILE "show-title.txt", "Welcome back to the breadbin" TRUNCATE
READFILE "show-title.txt", TITLE$
LOG "Title: " + TITLE$
RUN_AT$ = "Ran at " + TIME$()
WRITEFILE "last-run.txt", RUN_AT$ TRUNCATEThese features are powerful. Treat scripts as code: only run scripts you trust, and be careful with paths, HTTP endpoints, and local commands.
The built in small debugger allows you to troubleshoot common script issues:
- Start / Stop to run or halt a script;
- Pause / Resume to inspect what is happening;
- Step to execute one source line at a time;
- Log variables to dump current variable values to the OBS log;
- status fields for the last executed line, next line, and last runtime error.
For a first debugging pass:
- Add
LOGlines around the section you are unsure about. - Start the script.
- Pause before the interesting part.
- Step line by line.
- Use Log variables when a value looks suspicious.
For more detail, see the C64Script debugging guide.
Small pauses and explicit checks improve the reliability of your scripts:
- After
RESET, wait a few seconds before typing. - After mounting a disk, wait briefly before
LOAD,RUN, orAUTOSTART. - Before screenshots, use
OBS WAIT FRAMES. - REST calls can fail or time out. Retry if needed and don't assume success.
- Use
LOGbefore commands that change machine state. - Avoid
POWEROFFin experiments unless you really mean it. - Put recordings in a clear
START/STOPpair. - Keep scripts short at first, then extract repeated work into labels, subroutines, or functions once the flow is proven.
Let's put some of what we've learned to use.
Here is a script that resets the machine, types a tiny BASIC program, changes the stream look, records for a short period, and then leaves the C64 in a known reset state:
REM show-opener.c64script
LOGFILE "show-opener.log" TRUNCATE
LOG "Show opener started at " + TIME$()
RESET
WAIT 5s
TYPE "10 PRINT \"C64 STREAM IS LIVE\""
KEY 13
WAIT 500ms
TYPE "20 GOTO 10"
KEY 13
WAIT 500ms
TYPE "RUN"
KEY 13
EFFECTPARAM "preserve_size" 1
EFFECT "Classic CRT"
OBS RECORDING START
WAIT 5s
OBS RECORDING STOP
EFFECT "Default"
LOG "Show opener finished; resetting machine"
RESETCongratulations! You've reached the end of this tutorial.
Here are a few ideas on how to continue your journey:
- You may want to read the shipped scripts in
data/scripts/and adapt one small thing at a time. - The runnable examples in this tutorial are also covered by
the local hardware E2E scenario
ntsc_script_tutorial. - Finally, when you need the exact grammar, command list, limits, or full reference examples, use the C64Script language specification.
Above all, have fun with C64Script!