Crusty Studio is a focused desktop studio for building small retro-computer programs with crustyBASIC. It brings source editing, project files, native toolchains, build output, emulator launching, and pixel-and-sound workshops into one place.
This guide explains the complete workflow: how projects are organised, how the editor decides what to build, which tools work with which targets, and how the asset workshops feed data back into your game.
main.cbs fileOn the very first launch a short setup wizard appears before anything else. Its first page lists the tools a project needs — crustyBASIC, VASM (and NASM for an x86 target), and an emulator — with links to get them; choose I will install these later to carry on, or Continue once you have them. The second page offers Create a project…. Nothing is searched for until the wizard is answered, which is why a fresh install starts here rather than with an empty editor. After that, a workspace with no project shows a Start a project card with New project…, Open project… and your recent projects; close it with its × and the editor area shows a quiet hint instead, which says whether it has no project or a project with no file open.
.crusty manifest name./Volumes/Data2/Programming when that folder exists; on other machines the default is a Programming folder inside Documents. Once you have created a project this session the dialog opens at the folder that project went into, so a run of projects meant for the same place - several NES games inside one NES folder, for example - does not have to be re-pointed at it every time; closing Crusty Studio returns the dialog to the default. Browse… opens where the field points.Crusty Studio creates the project folder, a src folder containing main.cbs, and folders for sprites, charsets, tiles, shapes, sound, music, generated, and build. The project is remembered and restored the next time the desktop app opens.
Create an empty project leaves out src/main.cbs. The folders are still created and the manifest records no entry point, so the project starts with no source file at all. That is what you want when the source already exists and is about to be imported, or when the first file should be named by you rather than inherited from the starter. The editor opens on the "no file open" hint, a build with nothing to build says so, and the first .cbs a build finds is adopted as the entry point and recorded in the manifest.
The browser preview can demonstrate the interface and a simulated C64 build, but native file browsing, real compilation, and emulator launching require the installed desktop application.
The app is divided into four working areas:
| Area | What it does |
|---|---|
| Menu bar | Project, file, edit, run, tools, and help commands, with the dark/light switch at the right. |
| Project Explorer | Open source tabs, project assets, target information, and tool status. |
| Code editor | Monaco-based crustyBASIC editor with syntax colours, completion, diagnostics, and tabs. |
| Output and Problems | Build messages, compiler output, warnings, errors, and clickable source locations. |
| Search | Project-wide text search results, one row per matching line, each one clickable. |
The window itself remembers its size and place between launches, maximised or not, so it does not have to be resized on every start. A window that was maximised when it closed reopens maximised, and unmaximising it returns the size it had before, not the screen's; a window whose saved place is no longer on a connected screen opens where the platform decides rather than out of reach.
The sidebar and bottom panel are resizable. Drag the vertical divider between the Explorer and editor, or the horizontal divider above Output, to give a panel more room. Both panels scroll internally, so increasing the interface scale does not hide their contents.
The source editor has a two-row header. The top row contains the Project, File, Edit, Run, Tools, and Help menus. The row below keeps the build target, Options, Build, and Build and run controls together, followed by the independent Edit toolbar and the Go to procedure picker. Menu and Options popovers stay attached to their controls and are repositioned to remain visible inside the window. When the window is narrow, the Edit toolbar scrolls independently; its thin scrollbar appears when you hover over that toolbar, so the Target and build controls do not move with it.
The Dark/Light switch sits at the right-hand end of the menu bar, on the same line as the menus, and stays there while the menu bar scrolls. It switches the whole interface between the dark theme and a light one; the moon names the theme you are in, and the choice is remembered, so the studio reopens the way you left it. Both themes use the same highlight colour.
The status bar shows the current build phase, target, encoding, runtime, the editor code size, and the version of the build at the far right. Select the code-size indicator (it reads Code 14px at the default size) to reset the code editor to its default size.
The Explorer mirrors the project on disk:
My Game/
├── My Game.crusty project manifest
├── src/
│ ├── main.cbs main source file
│ └── other.cbi optional include/source file
├── sprites/ C64, NES, Atari 8-bit, Atari 7800, or Apple II graphics assets
├── charsets/ C64 character sets and Atari 8-bit tile glyphs
├── tiles/ C64 and NES tile-map projects
├── shapes/ machine-neutral shape sets (Portable Shapes)
├── sound/ sound recipes
├── music/ music projects: native songs, SID tunes, imported modules and samples
├── generated/ generated include data
└── build/ build products and intermediate files
Double-click a source file to edit it. Double-click a recognised asset to open its workshop. Right-click an open document or a recognised asset for Edit, Rename, or Delete; a plain project file such as a configuration or listing offers Delete only, and right-clicking a folder offers New File, which creates untitled.cbi in that folder (a path picker confirms where). Renames update the project manifest when the asset is part of the current project.
.crusty project manifestThe manifest is a small JSON file that records the project identity and asset locations. A typical manifest looks like this:
{
"format": "crusty-project",
"version": 1,
"name": "My Game",
"target": "c64",
"targets": ["c64", "nes", "amiga"],
"main": "src/main.cbs",
"folder": "/Users/me/CrustyFiles/My Game",
"assets": {
"sprites": [],
"charsets": [],
"tilemaps": [],
"shapes": [],
"sounds": [],
"music": []
}
}
main and the paths in assets are relative to the project folder — the folder the .crusty file itself is in. That folder is where the project's files are found, always, so moving the project or changing the Projects folder in Toolchain settings cannot make an existing project look somewhere else; Projects folder only decides where the file dialogs open and where a new project is created.
folder records the folder the project was last opened from. It is written for the app rather than for the build: if a project is moved, a Recent projects entry that still points at the old location follows this record to the project's new home. It is optional, and a manifest without it opens normally.
target is the active machine: the one the editors describe and the one Build and Run use. targets is optional and lists every machine the project builds for — see Multi-machine projects below. A manifest without it means exactly one machine, the active target, which is what every project written before the field existed says.
assets.shapes lists the machine-neutral shape sets in shapes/; see Portable Shapes Editor.
You normally do not need to edit this file by hand: creating, saving, importing, renaming, and deleting assets keeps it in sync.
A project can be built for more than one computer. target stays the machine being worked on and targets is the list of machines the project answers for:
"target": "c64",
"targets": ["c64", "nes", "atari800", "amiga"]
The Target menu in the toolbar leads with the project's own machines, each marked in project unless it is the active one, and offers every other machine under Add a machine. Choosing a machine makes it active and records it in the list, so there is no separate step to add one. The active machine decides which workshop an asset opens in, which per-machine includes a build selects, and what Run launches.
This exists because crustyBASIC's own multi-target examples are written that way: one program whose target hooks live in @INCLUDE "{program_name}/{target}.cbi", with a fallback file for machines that need nothing special. Machine-specific art is kept beside the shared art as its own asset kind, and the per-machine include chooses which one the game loads. Art that is not one machine's format at all goes in a shape set instead, so one copy of it serves every target — see Portable Shapes Editor.
main.cbs filesrc/main.cbs is the default entry source file. It is not a special language dialect; it is simply the source file named by the project manifest and shown first when a project opens. You can create additional .cbs, .cbi, or .bas files and switch between them using editor tabs.
The first lines normally declare the target:
@OPTION TARGET c64
@OPTION TARGET (or the compatible @OPTION SYSTEM) controls the target label, target-aware workshop availability, emulator adapter, and default launch behaviour. The editor reads this directive live, so changing it changes the target card in the Explorer.
You can also select an output format:
@OPTION OUTPUT_TYPE crt
When present, Crusty Studio passes that output type to the compiler. Without it, crustyBASIC chooses its normal default for the selected target.
The code editor is powered by Monaco and is aware of the crustyBASIC language.
/* … */ and /' … '/) are highlighted across their whole span, including blocks nested inside each other. They are also understood rather than merely coloured: procedures, constants, labels, calls, and @INCLUDE lines inside one stay out of the outline, the include graph, and the problems list, and the formatter leaves the comment text exactly as written. A block is only ended by the delimiter that opened it, matching the compiler. Block comments need a crustyBASIC build that supports them./* and */, and removes them again when the selection already carries them. Line comments keep using '.@OPTION and @INCLUDE.PROC, IF, FOR, WHILE, REPEAT, and SELECT CASE blocks.PROC and FUNCTION declarations in the current editor file. It is deliberately compact and does not add file names to the picker entries.↻ beside the tab name and on the Explorer row, next to the amber ● that means unsaved edits. Both can show at once, and the … tab menu gains Reload N changed files while any are waiting.Use the … button at the right side of the tab strip for tab management:
The tab scrollbar is separate from the code editor scrollbar. The Edit toolbar also has its own horizontal scroll area when the window is too narrow for all of its buttons. Hover over that toolbar to reveal the thin scrollbar and reach actions such as indent, outdent, duplicate line, delete line, and Find.
The lightweight live checker catches common mistakes immediately, such as an untyped DIM declaration. After a short pause, the desktop app also asks crustyBASIC to check the source against the project's build target — not the machine of whichever tab is on screen — and reports the compiler's diagnostics.
Use .cbs for a normal source file and .cbi for reusable include data or generated declarations. @INCLUDE is handled by crustyBASIC; the Studio does not rewrite your source. Asset workshops can generate include-style data for workflows that need byte arrays in code.
Procedure navigation understands include paths relative to the source file, including nested includes and target-specific paths such as machine/{target}.cbi. This means a declaration opened in an include file can find its calls in main.cbs, and a call in main.cbs can return to the correct declaration in an included file. When more than one target or file provides the same procedure name, choose the intended definition from the chooser.
Choose Build in the top toolbar or Run → Build. Crusty Studio:
@OPTION OUTPUT_TYPE and the current target. The Color Computer is the one family with a forced default: a CoCo build is a cartridge, so the studio passes --set output-type=cart when the source leaves it unset, rather than the compiler's own default.dos16, h120, pcjr, tandy1000, linux64, win64).If Generated assembly is enabled in Settings, the compiler receives --emit-asm and writes an .asm file into an assembly folder beside the source.
The Options control beside Target opens common crustyBASIC build parameters. Optimization, array indexing, and video region use --set; Debug mode, warnings, timings, and the size report add their matching compiler flags. Unchanged options are left out so the compiler keeps its normal defaults. These choices are remembered locally for future builds. The same popover holds Startup file, which picks which src/*.cbs a build compiles when a project has more than one; every other .cbs in the Explorer has a ▶ button that makes it the startup file, and the current one carries a ▶ badge. A manifest whose main names a file that is not there is rewritten to the file that answers to it.
Choose Run or Run → Build and run. A successful build is followed by target-aware emulator selection. Starting a run while another emulator started by the studio is still running stops that process before launching the new artifact. Fujisan on macOS is reused through its local service when possible, so the studio cannot terminate it the way it terminates a child process.
The family is the text before a dot, so c64.demo still uses the C64 adapter. Which machine is built is the active target in the Target menu, which is stored in the project manifest and passed to the compiler as --set target; that beats any @OPTION TARGET in a source, which is why a multi-machine source can select its per-machine hooks with {target} and declare no target of its own. A source directive still decides the target shown on the Explorer's target card, and is what crustyBASIC uses when the source is built by hand without --set target. The editor's own error check compiles for the same active machine, so its inline diagnostics describe what Build and Run will produce.
| Target family | Adapter | Executable names searched | Launch behaviour |
|---|---|---|---|
c64 |
VICE C64 | x64sc, x64 |
-autostart for normal images; -cartcrt for .crt. |
c128 |
VICE C128 | x128 |
VICE autostart. |
plus4 |
VICE Plus/4 | xplus4 |
VICE autostart. |
vic20 |
VICE VIC-20 | xvic |
VICE autostart. |
amiga |
WinUAE, FS-UAE, Amiberry | winuae, winuae64 / fs-uae, fsuae / amiberry |
Mounts and boots the .adf crustyBASIC writes beside the executable, on the machine the Amiga system names. See WinUAE, FS-UAE, Amiberry and Hatari. |
atari800 |
Atari 8-bit | atari800, Fujisan |
Atari800 launch; Fujisan uses its local TCP service on macOS. |
atari5200 |
Atari 5200 | atari800, Fujisan |
Adds the 5200 mode and cartridge flags for .car/.a52. |
atarist |
Hatari | hatari |
Hatari autostarts the TOS .prg given as an argument. The target itself is not yet supported. |
atari2600 |
Atari 2600 | stella |
Direct artifact launch. |
atari7800 |
Atari 7800 | a7800, EMU7800 |
A7800 (macOS and Linux) is a MAME fork: a7800 a7800 -cart <file>. EMU7800 (Windows) takes -r <file> and reads the machine and cartridge type from the .a78 header. |
apple2 |
Apple II | linapple*, applewin, microM8 |
LinApple gets --autoboot --d1, because a bare path leaves it at the Applesoft prompt with the disk unread; AppleWin and microM8 take the build directly, and microM8 also receives its Apple II profile. |
coco |
MAME, XRoar | mame, mame64 / xroar |
A CoCo build is a cartridge, so MAME (coco -cart) comes first and loads it; XRoar is the alternative, launched directly on the artifact. |
zxspectrum |
MAME, Retro Virtual Machine | mame, mame64 / Retro Virtual Machine* |
MAME takes the .tap apart and puts the code into memory, then enters it; see MAME. Retro Virtual Machine can only be started on the machine. |
amstradcpc |
MAME, Retro Virtual Machine | as above | MAME cpc6128 -flop1. |
msx |
MAME, Retro Virtual Machine | as above | MAME hx10 -cart. |
colecovision |
MAME, Retro Virtual Machine | as above | MAME coleco -cart. |
sms |
MAME, Retro Virtual Machine | as above | MAME sms -cart. |
zx81 |
MAME | mame, mame64 |
MAME zx81 -cass. |
gamegear |
MAME | mame, mame64 |
MAME gamegear -cart. |
gameboy |
MAME | mame, mame64 |
MAME gameboy -cart. |
aquarius |
MAME | mame, mame64 |
MAME aquarius -cart1. |
astrocade |
MAME | mame, mame64 |
MAME astrocde -cart. |
nes |
Nintendo Entertainment System | mesen, fceux, nestopia, tetanes |
Direct artifact launch. |
nes, snes, gameboy, gamegear, sms, colecovision, msx, atari2600 |
ares | ares |
--system <name> and the build's path; ares runs it. See ares and BizHawk. |
nes, snes, gameboy, gamegear, sms, colecovision, msx, atari2600 |
BizHawk | EmuHawk |
The build's path alone, through EmuHawkMono.sh on Linux. See ares and BizHawk. |
The Retro Virtual Machine and LinApple entries match their executable by prefix, because those names carry a version or a frontend suffix (Retro Virtual Machine 2.1, linapple-sdl3); the rest are matched exactly. A family served by both names both, and the emulator you pick in the Target menu decides how Run behaves.
ares and BizHawk appear once for every family they can run a build for, which is why the last two rows name eight of them. A family already served by MAME or Stella keeps that emulator as the one Automatic chooses; ares and BizHawk are there to be picked, or to answer when nothing else is installed. snes had no emulator adapter at all before they arrived.
VICE is one emulator per machine, so the family picks the executable: x64sc (or x64) for the C64, x128 for the C128, xvic for the VIC-20, xplus4 for the Plus/4. Run hands the build over with -autostart, which loads and runs it the way typing LOAD and RUN would; a .crt cartridge goes over with -cartcrt instead. Nothing else has to be configured, because VICE's own defaults are the machine the target builds for.
The C128 is also given a 1571, because it has no drive of its own by default. Autostarting a PRG means building a temporary disk image and booting from it, and a machine with no drive gives VICE no geometry to build that image from: it stops with AUTOSTART: Error - No idea what disk image format to use, closes without ever opening a window, and looks exactly like a broken emulator. Run therefore passes -drive8type 1571 for the c128 family — the drive a C128 shipped with, and the one VICE's own drag-and-drop picks when a file is dropped on the window. The C64, VIC-20 and Plus/4 already default to a usable drive.
On Linux and BSD, VICE has no ROM images until you supply them. The distribution packages leave them out — they are copyrighted, and Debian's own README.ROMs says so — so an emulator installed from a package can find neither its KERNAL, its BASIC nor its character set. It exits immediately instead of opening a window, which from a studio that started it from a menu looks like nothing happening at all.
The images live one folder per machine under VICE's system-file directory — C64, C128, PLUS4 or VIC20, beside the shared DRIVES and PRINTER folders. VICE prints the folders it searched on every start-up, and Run quotes it:
Main: VICE system file search path: '/home/you/.local/share/vice:/usr/bin:/usr/share/vice'.
Main: VICE system file directory: '/usr/share/vice'.
Use the per-user folder, the first entry, because it needs no root: /home/you/.local/share/vice/C128 for the C128, for example. VICE names each image it wants as it starts — Couldn't load basic ROM `basiclo-318018-04.bin' — and Run prints that, so a partial set can be completed a file at a time. Its own log, holding everything it printed, is at ~/.local/state/vice/vice.log.
The C128 is the awkward one: it wants the whole set before it will start at all. Where the other machines load the images the machine actually uses, the C128's own start-up check looks for every language variant of its KERNAL and character ROM — the international files plus the German, Swiss, Swedish, Finnish, French, Italian and Norwegian ones, eight KERNALs and six character ROMs. Missing the German KERNAL, for instance, stops the English machine with Couldn't load kernal ROM kernal-315078-03.bin'` before any window appears, which is exactly what a wrong emulator would look like. Copy the machine's complete ROM folder rather than the international files alone.
A C64 that runs while another Commodore target does not is this cause rather than a studio one: the C64 folder is populated and the machine's own folder is not. macOS is unaffected, because the VICE builds there carry the images.
The Amiga and the Atari ST are both Motorola 68000 machines, so both assemble with vasmm68k_mot rather than one of the 6502 builds. They are otherwise unrelated, and so are their emulators.
An Amiga Run mounts a disk image, not the executable. crustyBASIC writes two files for every Amiga build: a HUNK .exe, and beside it an 880 KB bootable .adf floppy whose startup-sequence runs the program. The .adf is what Run hands over, because it is the only thing FS-UAE and Amiberry can start — both boot media, and a program given on their command lines would be read as a configuration file — and WinUAE mounts it just as happily. FS-UAE is told the machine the Amiga system named with --amiga-model (amiga.500, amiga.600, amiga.1200) and is given the disk on --floppy-drive-0; Amiberry gets --model and the disk in drive 0 with -G, so it starts emulation instead of opening its own GUI; WinUAE is given the boot disk and left to its own model configuration. A build with no .adf beside it falls back to handing WinUAE the .exe, which it can run directly; FS-UAE boots the machine and Amiberry opens its GUI, each saying plainly that it has nothing to mount.
All three Amiga emulators need Kickstart ROM images, which are copyrighted and are not shipped with any of them. They look in their own ROM folders and say so when one is missing; the studio does not supply or check them.
Hatari autostarts the build. Hatari takes an Atari program as a plain argument — hatari game.prg is its own -d <dir> --auto C:<program> shorthand — so Run hands it the .prg and it starts. Hatari ships EmuTOS, so it boots without a TOS ROM of its own, though a real TOS image is more compatible with some programs. It is detected from its macOS bundle as well as a plain executable.
The Atari ST target is a preview: crustyBASIC 1.6.0 defines atarist.tos and atarist.gem but gives neither a tile backend, so a build stops while reading the target's own metadata. The machine is offered, marked not yet supported, and a failure is reported as what it is rather than in the compiler's internal terms. Hatari is there for when the target lands — and for a program that does not need tiles.
MAME is the only adapter here that runs a build outright, because it takes the machine, the media and the ROM folder on its command line. Run passes:
spectrum, cpc6128, hx10, coleco, sms, zx81, gamegear, gameboy, aquarius, astrocde, and coco for a Color Computer build);-cart, -cart1, -cass, -flop1);-window, because MAME is full screen by default and on macOS a full screen window is what jumps to another display;-skip_gameinfo, so the "this machine is not working" screen does not stand in front of the game on every run;-rompath, the MAME ROM folder from Toolchain settings, or the roms folder beside the MAME executable.Cartridges start on their own, because the cartridge is the machine's boot medium.
A ZX Spectrum tape cannot be started by mounting it, so the program is put into memory instead. Nothing can type LOAD "" at a Spectrum under MAME — the driver has no natural keyboard, so -autoboot_command types nothing at all (with it set, the screen three seconds in is byte-identical to a run without it) — and the driver overrides the cassette's default state to stopped, so a mounted tape does not start either. Its quickload slot rejects a raw build outright. So for zxspectrum, Run takes the built .tap apart, finds the machine-code block and the address it loads at, and writes a short Lua script that MAME runs: it waits for the ROM to finish its RAM test, puts the code at that address, sets RAMTOP the way the tape's CLEAR would, leaves a jr $ for the program to return into, and enters it.
That is what the tape's BASIC loader was doing, done directly. Verified against MAME 0.289 with the studio's own generated script: the program ran and printed to the screen, then returned cleanly into the trampoline.
The ZX-81 works the same way, though its build is not a tape at all. A .p is the machine's RAM: it starts at the system variables with the compiled code already at the address it runs from, so Run writes the image back where it came from and enters the documented entry ($43AB), which holds a stub that calls the program and then parks. The stack is left where the ROM put it — the image's own stack end sits below the display file and would leave a program almost no room. The image is checked for that stub before it is trusted: if crustyBASIC ever moves it, the build is mounted as media instead of being entered blind.
Anything that is not a .tap or .p is mounted on the medium the target names: -cart for a cartridge build, -cart1 for the Aquarius, -flop1 for an Amstrad CPC disk, and -cass for a cassette image. A cartridge is the machine's boot medium, so it starts on its own; a disk or tape that needs a command after mounting is what the studio's own launch_note says.
MAME ships no ROMs, and every system listed above needs a BIOS set except the cartridges themselves. When the set is missing it exits immediately — which, from a studio that started it from a GUI, means a window that never appears. Crusty Studio checks before it launches and says so instead:
MAME has no spectrum ROM set, so it would exit without opening a window. Put one in
/Volumes/Data2/Emulators/MAME/roms as spectrum.zip. The first ROM MAME lists for this machine is
spectrum.rom; the rest of `mame spectrum -listroms` are alternative ROMs for other variants of the
same machine, which are not needed and are chosen with -bios.
Do not be alarmed by mame <machine> -listroms. It prints every ROM the driver knows, and for a machine with several variants most of that list belongs to the other variants — the ZX Spectrum lists sixty entries and needs one. So only a set that is completely absent blocks the launch. A set that is present but incomplete is reported as a note and the launch goes ahead, because MAME itself runs a Spectrum with nothing but spectrum.rom in place, warning rather than refusing. Let MAME be the judge of its own ROMs.
The ZX-81 is a special case. MAME's driver carries three ROM revisions as BIOS options and needs only the one selected, but its default is the third — and the ZX80/ZX81 copyright is not Amstrad's, so the permission that covers the Spectrum ROMs does not reach it. The first and second revisions are freely distributable: they are the ROMs Nine Tiles released under the GPL as Open81, byte-identical to the revisions MAME checksums. Run therefore asks for -bios 2nd, the revised ROM with the arithmetic bug fixed, so zx81.zip needs only zx81a.rom in it. Adding zx81.rom as well lets you pick the first revision by hand.
What matters is the set name: one archive per machine, roms/<machine>.zip — or .7z, or the same name unpacked as a folder. If MAME still will not start, it names the ROM it wants. Sets go where the MAME ROM folder in Toolchain settings points, or in the roms folder beside MAME.
Retro Virtual Machine is a graphical emulator with almost no command line: its only useful switch is -b <machineid>, which boots a machine, and it accepts no program at all. A file named on the command line is ignored, and handing it a document through the system is refused before the emulator starts. Its only way in with a program is to drop the file on the running window.
So Run does the part the studio can do — it starts the emulator on the machine the target needs, which is otherwise a choice from a list of sixty — and then says in the output panel which built file to drag onto the window. Nothing is wrong with the build when this message appears. For anything Retro Virtual Machine and MAME both cover, pick MAME if you want Run to load the program itself.
Some machines have no adapter yet, and Run ends with No emulator adapter is available for target …. That covers the TRS-80 family, the Coleco Adam, NABU, the Sorcerer, the Tandy 100, MEGA65, Pac-Man (arcade), the x86 targets (dos16, h120, pcjr, tandy1000, linux64, win64) — the Target card names the adapter each is waiting for (Z80 emulator adapter, MAME adapter, DOSBox adapter, Native Linux adapter), which is where the wiring will go. MC-10 is not one of the studio's machines: crustyBASIC has a target id for it, but it is not in the project list.
Several rows above describe a launch for a machine that is still a preview in Which targets actually build — most of the Z80 and console families, for instance. The recipe is what Run will do once the target lands; until then the build stops before the assembler, and the Target menu and New Project mark the machine not yet supported.
ares and BizHawk are multi-system emulators: one installation each, covering far more machines than crustyBASIC can build for today. They are offered for the families whose build is a cartridge — nes, snes, gameboy, gamegear, sms, colecovision, msx and atari2600 — and deliberately not for the Spectrum, ZX-81 or Amstrad CPC, where a build is a tape or a disk that neither emulator can start without someone typing at the machine. MAME already does that job properly.
Both are handed the built file and run it, so there is nothing to drag onto a window and nothing to type.
Run passes ares the file and the system name:
ares --system "Super Famicom" --no-file-prompt /path/to/main.sfc
ares decides what a file is by its extension before it looks at anything else, and two of its own guesses are wrong for a studio build: a .bin is an Atari 2600 to ares before it is a Mega Drive, and a .gb is a Game Boy before it is a Game Boy Color. Naming the system settles it. The spelling is ares' own and is compared exactly — it is the list ares --help prints — and a name ares does not recognise is not an error to it: it quietly falls back to the extension. --no-file-prompt is passed as well, so ares never stops to ask about optional extra media that a Run does not have.
BizHawk is handed the file as its only argument.
BizHawk on Linux is a Mono application. Its EmuHawk.exe is a .NET assembly rather than a Linux program, and the release starts it through the EmuHawkMono.sh beside it. Run uses that launcher when it is there. If the mono runtime is missing it says so instead of starting a script that dies in the background:
BizHawk on Linux is a Mono application, and the mono runtime was not found on PATH. Install Mono —
from the project's own downloads, or from your distribution, where the package is `mono-complete`
on older Debian and Ubuntu releases and `mono-devel` on newer ones — and run again.
On Windows EmuHawk.exe is the program itself and no runtime is needed. ares ships a launcher of its own, ares.sh, beside the Linux binary, which points the library path at the copies of SDL2, Xv and libpng it bundles; Run uses that too when it is there, and the plain binary when it is not.
Some of these systems need a BIOS or firmware file — an FDS BIOS, a ColecoVision BIOS, an MSX BIOS — and neither emulator ships one. Each says so in its own window when one is missing. An NES cartridge, which is what the nes target builds, needs none.
The Target menu contains each target and its detected emulators as child options. A target with emulators is marked with a ›: hover over it, or click it, and its emulators appear in a panel beside the menu. Clicking a target's own row, rather than its emulator panel, chooses that machine — so adding a second machine to a multi-machine project is one click on the row. If exactly one compatible emulator is found, it is selected automatically; when several are available, choose one there. The selection is remembered per target family. Choose Automatic to use the first detected emulator.
crustybasic targets lists every target the compiler carries a definition for — fifty-eight of them — which is a much longer list than the systems its own README calls supported. crustyBASIC 1.5.0 implements:
apple2, atari800, atari5200, atari2600, atari7800, c64, c128, plus4, vic20, coco, dos16, win64, linux64, nes, zxspectrum and zx81. crustyBASIC 1.6 added the Amiga (amiga) and defines the Atari ST (atarist) without implementing it, so the Amiga is supported here and the Atari ST is marked preview.
Everything else in the target list is marked preview. A preview target fails while reading its own metadata, before the assembler is reached, so it cannot be built whatever is done to the toolchain or the settings — Crusty Studio says so plainly instead of passing on the compiler's internal message. They are listed because they are what is coming.
That list can only be as current as the crustyBASIC release it was taken from, so it is not the last word. Build for a preview target and succeed, and it is promoted to new for that machine — the badge changes, and the output panel says so:
MSX builds with this compiler, so it is no longer a preview target.
A build that succeeded is better evidence than any list, so this is how a newer crustyBASIC is noticed without the studio being updated first. The promotion is remembered per machine, and an older compiler chosen in Toolchain settings — which fails the same way — takes it back.
New Project lists every machine crustyBASIC defines, annotated with its support level. Additional crustyBASIC target IDs can still be edited in source, but Run needs a matching adapter and emulator.
Open Tools → Settings or select the gear in the Explorer. Settings has two sections.
The window's size and place are remembered as you move and resize it — written to the app's own settings folder, never into a project — and put back when the studio is opened again. Nothing has to be saved by hand for that, and the remembered size is ignored if it is smaller than the app's minimum.
Interface size scales the complete UI: text, controls, spacing, menus, and panels. Presets are Default (100%), Comfortable (115%), Large (130%), and Extra large (150%). The preview is immediate; choose Save settings to persist it.
The code editor has an independent zoom. Hold Command or Control while scrolling over the code editor. On macOS, Command is the recommended modifier; ⌘+, ⌘−, and ⌘0 are reliable keyboard fallbacks. On Windows and Linux use the equivalent Control shortcuts.
Theme is switched from the Dark/Light control at the right of the menu bar rather than from here, so it applies at once. The light theme restates every colour in the interface at light-theme weights, including the asset workshops and the code editor's own colour scheme, so nothing is left half-dark when you switch.
Highlight colour changes the accent used across the studio: the active tab, the build and run buttons, badges, selection outlines, editor caret, and the highlighted panels in every asset workshop. Drag the wheel's outer ring to choose a hue and the inner square to set how strong and how bright it is, type a hex value, or pick one of the presets. The change is previewed immediately; Reset returns to the stock amber and Save settings keeps the choice. The default amber is used until a custom colour is saved. The light theme deepens the accent so it stays legible both as text on a pale panel and under white type on a filled button.
PATH.vasm6502_oldstyle) covers the Commodore, Atari, Apple II, NES, SNES and the other 6502-derived families, the Z80 build (vasmz80_oldstyle) covers the Spectrum, ZX81, Amstrad CPC, MSX, ColecoVision, Game Boy and the other Z80 and 8080 targets, the 6809 build (vasm6809_oldstyle) covers the Color Computer, and the 68000 build (vasmm68k_mot) covers the Amiga and the Atari ST. The resolver also knows vasm6800_oldstyle (the MC-10) and vasmarm_std (Archimedes and GBA) for target ids crustyBASIC defines but the studio does not offer, so a hand-edited target still finds its assembler. Point the field at the folder that holds them — tools/assemblers/vasm/<platform>/ beside the compiler, or wherever you unpacked the VASM release — and Crusty Studio picks the build each target names, looking a few folders deep so an unpacked release with a folder per platform works too. A path to a single VASM executable is also accepted and is used through its folder. Either spelling of a file name resolves: the bare vasm6502_oldstyle and the platform-suffixed name the downloads on this site arrive with (vasm6502_oldstyle_linux-x86_64 and the like), though a copy built for another platform is passed over. The tool check reports every CPU build it found, so a folder holding only the 6502 build is visible before a Z80 build is attempted.dos16, h120, pcjr, tandy1000, linux64 and win64. A missing copy is a warning rather than an error, because the other targets build without it.assembly/<source>.asm.Select Check installation to refresh the green, yellow, and missing tool indicators. The same scan can be run from the status bar's Find tools button, which turns into Cancel while it works; the status bar also carries the version and build commit of the running build. The desktop app does not bundle crustyBASIC, VASM, NASM, emulators, ROMs, or games; install those separately and point Settings at them.
On macOS, Gatekeeper can quarantine downloaded compiler helper tools. The tool check and build output explain the exact helper that needs to be reviewed and cleared if this occurs.
Workshops are opened from Tools or by double-clicking a recognised asset in the Explorer. Each workshop has its own scrollable panels and a Back to code action. In the desktop app, New and Save write into the current project; in browser preview, Save downloads a JSON project file.
Availability comes from the project's machines, not from the active source file alone. An editor is offered once for every machine the project builds for — the targets list in the manifest, with the active machine always included — and the active machine's entry comes first, so a single-machine project sees exactly one entry and a C64/Amiga project sees the editor once per machine. Choosing an entry opens that machine's version of the workshop and seeds a new asset from that machine. A target family is the text before the dot in a target id, so c64.demo still counts as C64-family.
| Tools menu entry | Enabled for | What opens |
|---|---|---|
| Sprite Editor | C64 family, NES, Atari 8-bit, Atari 7800, Apple II | C64 Sprite Workshop for the C64 family, NES Sprite Workshop for NES, Atari Sprite Workshop for the Atari 8-bit and 5200, Atari 7800 Sprite Workshop for the Atari 7800, Apple II Graphics Workshop for Apple II. |
| Charset/Tile Editor | C64 family, Atari 8-bit and 5200 | The C64 Charset Editor for the C64 family (standard and multicolour character sets), the Atari Tile Editor for the Atari 8-bit and 5200 (8 × 8 one-bit tile glyphs). |
| Tilemap Editor | C64 family, NES, Atari 7800 | C64 tile-map editor for the C64 family, NES tile-map editor for NES, MARIA tile-map editor for the Atari 7800. |
| Sound Editor | C64 family, NES, Atari 7800, Amiga | The sound editor, adapting to SID, the NES 2A03, the Atari 7800's TIA, or the Amiga's Paula according to the target. |
| Music Workshop | Every target | The portable four-lane tracker, which uses the target-neutral CrustyBasic sound API. |
| SID Music Workshop | C64 family | The three-voice SID tracker with chip-model, filter, and PAL/NTSC controls. |
A greyed-out entry means the project has no machine that uses that editor; a note under the menu says so and points at Target, where a machine is added. Disabling is only about creating a new asset: opening an existing asset from the Explorer always works, because the saved asset records the platform it was authored for. A NES tile map opened from a C64 project still opens in the NES tile editor. The Sprite Editor covers the C64 family, NES, the Atari 8-bit and 5200, the Atari 7800, and Apple II; the Charset/Tile Editor covers the C64 family and the Atari 8-bit and 5200; the Tilemap Editor covers C64, NES and the Atari 7800; Portable Shapes is always offered, because it is machine-neutral.
Sound follows the same split, and it is worth knowing what each family actually gets. The C64, C128, and Plus/4 use the MOS 6581/8580 SID with three voices, a shared multimode filter, and PAL or NTSC timing; the Sound Editor and SID Music Workshop configure those registers directly. NES uses the 2A03 APU with two pulse channels, a triangle, and noise, which is what the Sound Editor's NES controls map onto. The Amiga's Paula is four DMA voices with a period and a level each and one square waveform, which the Sound Editor's Amiga side and the Music Workshop's Amiga player drive with PAULA.SOUND. Every other target has only the portable Music Workshop and the generic CrustyBasic sound API, resolved by the target's own runtime — there is no chip-specific editor for Atari, Apple II, CoCo, or the other targets.
Nothing here is available in the browser preview beyond drawing and downloading a project file; saving into the project, importing binaries, and exporting includes are desktop-only.
Available for the C64, C128, and Plus/4 targets in the current Tools menu. It edits a VIC-II sprite as one or more animation frames.
.cbi with one block per selected sprite: CONST <SYM>_FRAME_COUNT, _MODE, _COLOR, _SHARED_0, _SHARED_1, _EXPAND_X, _EXPAND_Y, then the _DATA array with one row of 63 bytes per frame. Several sprites can share one include, and the include name must end in -sprites.cbi..c64sprite.json format and are stored in sprites/.Available for NES targets. It edits 8 × 8 indexed tiles with two bitplanes.
$21 · Light blue). What the editor stores and the export writes is the PPU index itself, so the colour on the machine is the colour chosen here.CONST <SYM>_TILE_INDEX, _PALETTE, _FRAME_COUNT and _DATA; the include name must end in -sprites.cbi.CONST <SYM>_PALETTE_COUNT = 4, the twelve _PALETTE_DATA bytes, and PROC LOAD_<SYM>_PALETTES, which issues one SPRITE_PALETTE_SET slot, c1, c2, c3 per slot. The sprites in one export normally share one table, so it is written once. If the selected sprites disagree, the include says so and names each loader, because only one palette loader belongs in a program — the last one to run would win..nessprite.json project. A project written before the palettes existed loads with the default four sub-palettes rather than failing. There is no image import, so NES artwork is drawn in the editor, imported as project data, or produced by the tooling that generates sprite tables..nessprite.json and live in sprites/, and the include is marked target=nes so an export cannot replace another machine's sprite include.Available for the Atari 8-bit and Atari 5200 targets. The Atari has no sprite chip: ANTIC and GTIA give it four players and four missiles, and the editor is built around that rather than around the C64 model.
PMG_PLAYER0–PMG_PLAYER3 or PMG_MISSILE0–PMG_MISSILE3. Switching between a player and a missile reshapes the frames, because the two do not share a pixel width.COLPMn, $D012–$D015), so a missile cannot be a different colour from its player unless fifth-player mode is on, where all four missiles draw in GTIA.COLPF3 instead. Both facts are stated in the panel and in the exported include.PMSIZE. Width stretches the bits horizontally; it does not add detail. Missile width is the shared SIZEM register, so it applies to every missile.SPRITE_DATA, missiles through PMG_STRIP_PAINT. The header names the calls that are chip-wide rather than per sprite (PMPRIORITY, PMFIFTH_PLAYER, SIZEM)..atari8sprite.json and live in sprites/. One asset serves both the 800 and the 5200, and the generated include is marked target=atari8 for that reason.Available for the Atari 7800 (atari7800) target. It edits MARIA sprites in two forms, chosen when the asset is created:
API sprite — a fixed 8 × 8 shape at one bit per pixel, stored as 8 bytes per frame, one byte per row with the most significant bit leftmost, and installed with SPRITE_DATA_8X8. This is the simplest path and the one crustyBASIC's own sprite API describes.
Kernel sprite — any grid of 8 × 8 MARIA cells (a 16 × 16 object is 2 × 2 cells), at one or two bits per pixel, in one of the six screen formats MARIA can read: 160A, 160B, 320A, 320B, 320C, or 320D. In a 160 mode a fetched byte is four 2-bit cells and each cell is two screen pixels wide, so a pixel is 2:1 rather than square; the canvas draws the cell boundaries and shows the shape at the mode's real ratio. The mode also decides how many colours an object reaches — two in 320A/320C, three plus transparent in the others. The export is raw row data, not the 8 × 8 API: CONST <SYM>_MODE, _CELLS_W, _SCREEN_W, _FRAME_BYTES and _DEPTH describe what a kernel has to copy. docs/ATARI7800_SPRITE_FORMAT_V2.md is the format reference behind this panel.
Colour is not per sprite on the 7800. The chip has eight 3-colour MARIA palettes, written with SET_PALETTE index, c1, c2, c3, and each sprite selects one of them with SPRITE_PALETTE id, palette. Those are one set of chip-wide registers shared by tiles and sprites, so the table belongs to the project, not to an asset: the current manifest holds it under atari7800ColourRam as eight rows of four — a shared background entry plus the palette's three colours (a project written before that field carries atari7800Palettes, eight 3-colour rows, which is read and promoted). Whichever 7800 editor is open edits that one table — the Tilemap Editor edits the same one — and a change is stamped into every 7800 sprite and tile map asset of the project, so each file stays complete on its own. The exported include therefore always carries a single _SET_PALETTES loader; two loaders in one program would write the same registers and the last call would win.
An 8 × 8 shape shows one colour. crustyBASIC reports SPRITE_COLORS_PER_SPRITE as 1 for this target, and a one-bit shape shows entry 2 — the middle colour — of the palette it selects, so the sprite's own colour belongs there. Keep entry 2 of palette 0 as the tile and text colour, as most projects do. The editor still stores and exports all three entries of every palette, because entries 1 and 3 are real palette state a program may use with richer object formats.
Animation frames can be added, deleted, and played in the preview. Draw, erase, and fill tools are provided, alongside a picker for which of the eight palettes the asset selects, a preview, and an editable byte read-out.
The Shared palettes panel is the project's 8 × 3 editor, and its colour pickers offer the full 256-entry MARIA palette: hue in the high nibble, luminance in the low nibble, with rows as hues and columns as luminance in the picker.
Export for an API sprite writes a .cbi include into the project's src/. It carries, per sprite, CONST <SYM>_PALETTE, CONST <SYM>_FRAME_COUNT, the packed CONST <SYM>_DATA(...) AS U8 array, and PROC LOAD_<SYM>(ID AS U8, FRAME AS U8), which does SPRITE_DATA_8X8 ID, ADDR <SYM>_DATA + FRAME * 8 followed by SPRITE_PALETTE ID, <SYM>_PALETTE. A kernel sprite is exported as raw rows with the geometry constants above and no LOAD_ routine, because a custom-width object is driven by the game's own display list. The palette table itself is written once, as CONST <INCLUDE>_PALETTE_COUNT = 8, its _PALETTE_DATA bytes and one PROC <INCLUDE>_SET_PALETTES with the eight SPRITE_PALETTE_SET calls, where <INCLUDE> is the include's own file name without .cbi — lander-sprites.cbi gives LANDER_SPRITES_SET_PALETTES. Naming it after the file the game includes, rather than after whichever sprite happens to be exported first, keeps two sprite includes in one program apart. Include names must end in -sprites.cbi.
A sprite asset written before the table moved to the project carries only four rows. Opening it pads the table out to eight with the defaults and keeps the four rows it had, so older art keeps the colours it was drawn against.
The include's asset marker records target=atari7800, so an export refuses to replace a generated sprite include that belongs to a different machine.
Colour swatches are an emulator NTSC preview using MAME's Atari 7800 table; the export writes the colour byte, so the machine remains the authority.
Assets use .atari7800sprite.json (and .atari7800sprite) and live in sprites/, listed in the project manifest's sprites.
This is deliberately a separate editor from the Atari Sprite Workshop. The 8-bit machines have no sprite chip and use Player/Missile Graphics: 8 × 16 players, four players plus four missiles, a colour register per object, and PMSIZE/PMHPOS/PMPRIORITY with fifth-player mode. The 7800 has none of those — its shapes are 8 × 8 and its colours come from shared palettes — and the two share a colour-byte encoding and nothing else.
Available for Apple II targets. It provides Apple II-oriented graphics presets and pixel editing for low-resolution and high-resolution sprite-style assets. The preview is an editing guide; composite colour for hi-res art also depends on the screen X position when the program runs.
Apple II graphics assets use .apple2graphics.json or .apple2lores.json and are stored with other sprite assets in sprites/. Open and Save read and write those project files; there is no image import.
Export CrustyBASIC data writes the asset out as CrustyBASIC source — the graphics bytes and their metadata — into a read-only panel and copies it to the clipboard. It is not a .cbi include and it does not touch src/: paste it into the source file that should own the data.
Opened from Tools → Tilemap Editor, or by double-clicking a tile map in the Explorer. The C64 tile editor opens for the C64 family and the NES tile editor opens for NES, but the workflow is the same three layers in both:
| C64 | NES | |
|---|---|---|
| Atomic tile | An 8 × 8 glyph from the charset you load for the map | An 8 × 8 CHR pattern edited in the workshop, with pixel values 0–3 (the two CHR bitplanes, not fixed colours) |
| Colour | Comes from the charset the map refers to | Four background sub-palettes you edit from the complete 64-entry PPU colour table; palette slot 0 is the shared universal background colour |
| Metatile meaning | Collision (Empty, Solid, One way, Ladder, Hazard) and Gameplay role (Platform, Wall, Floor, Ceiling, Pickup, Checkpoint, Spawn, Goal, Decoration) | An attribute palette of 0–3 |
| Default map size | 40 × 25 metatiles, matching one character-mode screen at 1 × 1; editable up to 160 × 100 | 32 × 30 metatiles, matching one nametable screen at 1 × 1; editable up to 256 × 120 |
| Exported include | Metatile arrangements, map values, collision values, and gameplay-role values, as <NAME>_MAP, _COLLISION, _ROLE, _METATILE_OFFSETS, and _METATILE_CELLS |
CHR pattern bytes, the four background palettes as 16 PPU colour bytes, the map table, attribute bytes, and a generated LOAD_<NAME>_CHR procedure |
Metatile sizes come from a fixed menu of 1 × 1, 2 × 1, 1 × 2, 2 × 2, 3 × 2, 2 × 3, and 4 × 2 tiles in both editors. Delete a metatile and every map cell using it is cleared, with no confirmation and no undo. The C64 starter project ships Empty, Solid, and Pickup; the NES one ships Blank, Solid, Edge, and Corner. Paint an Empty or Blank metatile over a cell to clear it, and remember that an untouched cell and an Empty cell both export as metatile 0, so your renderer should treat metatile 0 as nothing.
Three consequences are worth planning for:
$00–$3F). It changes only when a project file is loaded, so treat the exported CHR range as fixed by the file you started from rather than as an editor setting. Importing a map whose chrStart is not zero re-saves it from zero.Both editors scroll their own canvas with zoom buttons, a Fit action, and a Grid toggle; there is no pan tool and no undo, so save before reshaping a map. Resizing discards any cells that fall outside the new bounds.
Tile maps use .c64tilemap.json or .nestilemap.json and are stored in tiles/. Both editors open the editor's own JSON project; neither imports PNG or binary graphics. The C64 editor (and the Atari 7800 one) also offers Import rows from text, which fills the map from a pasted block of rows so a level authored elsewhere can come in as text; the NES editor has no text import. The C64 charset is chosen from the charsets already in the project. Help → Tilemap Editor Guide covers the step-by-step workflow and the NES attribute-grid behaviour in more depth.
Opened from Tools → Tilemap Editor in an Atari 7800 project. It is a different editor from the C64 and NES ones because the hardware is different, not a port of them.
What it edits. MARIA 320A indirect ("character") screens: a fixed 40 × 24 area of 8 × 8 one-bit tiles, exactly what crustyBASIC's hardware tile display gives you with TILE_BEGIN 1, 1. A project holds several named screens, so a set of levels is one asset. Tile 0 is the blank tile and is not editable: an undefined slot in the character bank reads as background, which is what makes an empty map cell empty.
Colour is per row, not per tile, and the palettes belong to the project. The chip has eight palettes of three colours each, shared with sprites, and a 320A tile shows entry 2 — the middle colour — of the palette it selects, so a tile's colour belongs in the middle slot; entries 1 and 3 are still real palette state. The table lives in the project manifest (<project>.crusty) under atari7800ColourRam as eight rows of four entries: entry 0 is the one shared background register ($20), and entries 1 to 3 are the bytes SET_PALETTE index, c1, c2, c3 writes. The panel shows the background once, above the eight rows, because the rows cannot disagree about a register the chip holds only one of. The Sprite Workshop edits the same table, so a colour change applies to the whole project and is stamped into every 7800 sprite and tile map asset. A project written before entry 0 was stored carries atari7800Palettes, eight 3-colour rows, which is read and promoted by filling the shared background in; a 7800 asset written before the table moved to the project carries four rows, which are padded out to eight with the defaults and kept. Because a tile cannot carry its own palette into the display list, the row is split into palette runs; the editor computes them from the palettes your tiles ask for, and marks the rows that need more than one run with a colour band above them - a single-palette row has no boundary to show, so it stays clean. Palette 0 is usually the tile and text palette for the rest of the screen, so leave it alone unless you mean to change every tile, text and sprite that uses it. Entry 0 is project state that the panel previews with, but the generated include writes only the eight rows of three colours through SET_PALETTE; it does not write $20, so set the shared background register in the program.
The display-list budget is the constraint to watch. A row has 26 bytes for tile and sprite objects together. A palette run costs 5 bytes and reaches 32 columns, so a plain 40-column row costs 10 bytes and leaves room for four sprites — exactly the machine's SPRITE_MAX_COUNT. Extra runs eat that headroom. The Budget panel reports the busiest row's bytes, the sprite room left on it, and the ROM each screen costs, and the editor flags a row whose runs will not fit. Nothing stops an export, because only you know how many sprites that row will show.
Stamps are reusable rectangles of tiles (up to 4 × 4) painted with one click. They are expanded to plain tile IDs when you paint, so the saved map and the export contain only tile numbers and the game needs no metatile renderer — on the 7800 the map is copied straight to the screen with TILE_BLIT.
Markers place objects in tile coordinates: spawn, goal, enemy, pickup, and checkpoint. They are exported as an x, y, kind table per screen with a CONST per kind, so a game asks for the spawn point instead of agreeing on a magic number. Markers are never written into the map itself.
The export is one .cbi include in src/. It carries the tile pictures as CONST glyphs with TILE_DEFINE calls, the project's eight palettes as data with an INSTALL procedure, one map per screen with its palette runs and markers, and a <SCREEN>_SHOW procedure per screen. Its symbols are named after the include file, as the sprite include's are — lunar-levels-tilemap.cbi gives LUNAR_LEVELS_TILEMAP_INSTALL and LUNAR_LEVELS_TILEMAP_LEVEL_1_SHOW — so two maps exported from one project stay apart. Because one program should have exactly one writer of the palette registers, a game that uses both 7800 includes calls the tile map's INSTALL and never the sprite include's <INCLUDE>_SET_PALETTES. A game needs:
@OPTION TILE_ACTIVE_COUNT 16
@INCLUDE "my-map-tilemap.cbi"
PROC MAIN
DISPLAY MARIA_320A
TILE_BEGIN 1, 1
ATARI7800_TILEMAP_MY_MAP_INSTALL
ATARI7800_TILEMAP_MY_MAP_LEVEL_1_SHOW
ENDPROC
The include reports the @OPTION TILE_ACTIVE_COUNT the program needs, which is the number of tiles in the bank. Set it yourself: it is a compile option, so the include cannot set it for you.
Limits worth knowing. The tile area is fixed at 40 × 24, so PAL's extra rows are not addressable and the 160 modes are not available — a phase-2 job that needs compiler support. The editor imports its own JSON, not PNG tile sheets, but Import rows from text fills the map from a pasted block of rows. Tile maps use .atari7800tilemap.json and live in tiles/.
One tool serves two sides, and the active build target decides which one opens: the Charset Editor on the C64 family, and the Tile Editor on the Atari 8-bit and 5200. Both sides edit 8 × 8 glyphs and both save into charsets/; they differ in what those glyphs mean and how they reach the machine. A glyph is the 8 × 8 shape data, a tile is a game-level ID that pairs a glyph with a colour, a tileset is a collection of glyphs, and a tilemap is the layout of tile IDs.
Available for the C64, C128, and Plus/4 targets. It edits the computer's character memory as 256 glyphs, each 8 × 8 pixels, and it is a C64-family tool throughout: the palette it writes names VIC-II registers, so the same asset is expected on a C128 or Plus/4 as on a C64. The NES equivalent is the CHR editor inside the NES Tilemap Editor; there is no charset editor for Apple II or the other targets.
Choose Standard or Multicolour before drawing, because the two modes are different shapes:
Switching mode converts all 256 glyphs at once, and the conversion is lossy: standard to multicolour keeps only whether a pixel pair was lit, and multicolour to standard loses which of colours 1, 2, or 3 each cell used. There is no undo, so choose the mode before investing in art.
$hex, or 0x values separated by commas or spaces, applying them when you leave the field. Copy glyph bytes puts that text on the clipboard.Import. Open charset accepts a classic C64 .64c binary dump or a saved charset project. A binary's two-byte load address is read and shown, eight bytes per glyph are decoded, and a shorter set is padded to 256 glyphs with blanks. The parser rejects anything under one glyph, data that is not a whole number of eight-byte records, and more than 256 glyphs, each with its own message. A binary is converted into an editable .c64charset.json project rather than being edited in place, and the load address is not kept in the project. The editor's Open dialog decodes a binary in the mode you are currently in, while importing the same file from the Explorer or by dropping it always decodes as standard, so set the mode first if you are importing a multicolour dump. Charset projects require exactly 256 glyphs; a hand-edited file with fewer will be rejected as invalid.
Export. Export charset include writes a .cbi into the project's src/ folder for the glyph range you choose. The defaults are glyphs 64 to 127, and the help beside the fields explains why the range matters: exporting all 256 would blank the letters and digits the ROM font still supplies. The generated file starts with the asset marker, then one packed array for the whole range — CONST <SYM>(<count × 8 - 1>) AS U8, wrapped at sixteen bytes a line so it stays readable and diffable — and <SYM>_INSTALL_CHARSET, which calls CHARSET_COPY_DEFAULT and then CHARSET_INSTALL_RANGE first, count, ADDR <SYM>. Multicolour charsets additionally get the stored palette as <SYM>_COLOR_BACKGROUND, <SYM>_COLOR_1, <SYM>_COLOR_2, and <SYM>_COLOR_CELL constants, a <SYM>_SET_PALETTE routine that writes $D021–$D023, and <SYM>_FILL_CELL_COLOR, which writes the cell colour into colour RAM once for the whole screen (a scrolled display swaps two screen pages that share one colour RAM, so a per-cell colour would be left behind). A standard charset emits no palette block, because its two colours come from the screen registers.
Assets use .c64charset.json and are stored in charsets/. Help → Charset/Tile Editor Guide covers the drawing workflow, the screen palette, the .64c import, and how to choose an export range in more depth.
Available for the Atari 8-bit and Atari 5200 targets. The same editor opens with the heading Tile Editor and edits tile glyphs rather than a text character set:
.atari8tiles.json in tiles/ (format crustystudio-atari8-tiles, with a target field), so the Explorer shows it under TILES beside the tile maps. The editor still reads the earlier .atari8charset.json name and format.TILE_COLOR id, fg, bg in the game, not from the glyph bytes, so the same glyph can be drawn in different colours at different tile IDs.Export. Export tile include writes a .cbi into the project's src/ folder. It contains one packed glyph array — eight bytes per glyph, one bit per pixel — and a <ARRAY>_DEFINE_TILES(FIRST_TILE AS U8) procedure that emits TILE_DEFINE FIRST_TILE + n, ADDR <ARRAY> + n*8 for each glyph. There is deliberately no charset-install code and no palette code for Atari: the include only binds glyphs to tile IDs, and the game sets their colours.
Glyph order must match tile-ID order. Glyph n in the asset is the glyph that _DEFINE_TILES binds to FIRST_TILE + n, so keep the pictures in the order the game expects. The Atari side is used with @OPTION TILE_BACKEND TILE_BITMAP and the TILE_DEFINE, TILE_COLOR, and TILE_PLOT calls.
A charset include records the machine it was generated for in its @crusty-asset marker (target=c64 or target=atari8), and an export refuses to replace a generated include that belongs to a different machine.
Assets use .atari8tiles.json and are stored in tiles/; the earlier .atari8charset.json spelling still opens. Help → Charset/Tile Editor Guide covers the tile workflow and the C64 charset side in more depth.
Tools → Portable Shapes opens an editor for the one kind of art that is not tied to a machine: a named set of 8 × 8 one-bit pictures, saved as shapes/*.shapes.json. It is offered for every target, because there is nothing machine-specific about it.
Every other art asset is a machine format — a C64 sprite is 63 bytes of a 24 × 21 grid, a NES sprite is a tile plus attribute bytes, an Atari glyph is eight bytes of one bit per pixel — so none of them can be shared by a project that builds for several machines. crustyBASIC has a machine-neutral form underneath those: TILE_DEFINE id, ADDR shape and SPRITE_DATA_8X8 id, ADDR shape take the address of eight plain bytes, and each target's own runtime lays them out for its hardware. A shape set is exactly those bytes, with names.
SPRITE_DATA_8X8 call reads.art holding a shape called SHIP_A exports CONST SHAPES_ART_SHIP_A[7] AS U8 = % followed by eight rows of X and .. Two shapes with the same name are kept apart by a numeric suffix rather than quietly sharing one symbol.TILE_DEFINE in one place and SPRITE_DATA_8X8 in another.Export writes a .cbi into the project's src/ folder with one CONST array per shape and nothing else — no install routine, no palette code, no TILE_DEFINE. That is the point of it: the include compiles on every target. The generated file records target=portable in its @crusty-asset marker, so a portable export replaces its own previous output from whichever machine is active and never a machine-specific charset or sprite include.
A machine that wants art the portable set cannot express keeps its own asset kind beside it — sprites/c64-ship.c64sprite.json next to shapes/art.shapes.json, say — and that machine's per-target include chooses which one the game loads. See Multi-machine projects.
Available for the C64 family, NES, the Atari 7800, and the Amiga. One project is one sound effect, and the editor adapts its controls to the sound hardware of the current target, which the header names: C64 SID for the C64 family, NES APU for NES, Atari 7800 TIA for the 7800, or Amiga Paula for the Amiga. The target is not switchable inside the editor, and opening a sound asset adopts the asset's own target, so a C64 recipe opened from a multi-machine project is still edited as a C64 sound.
The Amiga side is a work in progress and switched off by default. It is behind Amiga editors in Settings → Toolchain; turn that on to use it. A shipped build does not offer it, so an Amiga project never leads you into an editor that is still being built.
APU.SOUND as the eleven-bit APU timer, not as hertz.△ ╱ ▯ ▦ ∅ buttons, and what a plus sign between them means are decoded in the oscillator waveform picker.PAULA.SOUND receives. There is no waveform, filter, envelope, or noise control: Paula replays a two-byte square wave, and the period sweep is the whole instrument.SFX_PLAY_<NAME> procedure behind shared SFX_PLAYER_INIT, SFX_PLAYER_STOP, SFX_PLAYER_APPLY, and SFX_PLAYER_UPDATE routines, and each row has a ▶ button so you can audition a sound from the export list. A player holds a single target family, so C64, NES, Atari 7800, and Amiga sounds cannot be mixed in one include, and the export refuses that with a message. On C64 the player also carries per-tick frequency and pulse-width tables plus a SFX_PLAYER_MODULATE routine that applies the patch's modulation; on NES each step is a single APU.SOUND call with no tables; on the Amiga each step is a per-frame table of Paula periods which the player pokes straight into AUDxPER, so a sweep is a real pitch slide and the voice's DMA is not restarted once per frame.Import reads the editor's own JSON project only — .c64sound.json, .nessound.json, or .amigasound.json. There is no .sid, GoatTracker, or raw-binary import here. Fields missing from an older file are filled with defaults, so it loads rather than failing, and an invalid file reports Invalid sound project file.
Assets use .nessound.json for NES, .c64sound.json for C64, and .amigasound.json for the Amiga, and are stored in sound/. Only the JSON forms are editable projects: a dropped .c64sound, .sound, or .sfx file is copied into the folder and listed in the manifest, but it does not open in the editor.
Available from Tools → Music Workshop for every target, because it is a portable four-lane tracker built around the generic CrustyBasic sound API rather than one machine's sound chip. That is also its limit: only the SFX player and the two chip-specific editors get at real hardware registers.
On an Amiga project the Workshop is a work in progress and switched off by default. The Paula instrument designer, the sample import and the Amiga player inside it are still being finished, so the menu greys the Workshop out for that machine until Amiga editors is turned on in Settings → Toolchain. Every other machine's side of the Workshop is unaffected, and so is creating, building and running an Amiga game.
A47, V26, U12, D12, T24, and W23. The effect column is a written record of intent, not part of the sound in this build: the preview never reads it, and the exported player carries note, lane, waveform shape, and volume per event — it does not emit the ADSR, gate, duty, arpeggio, vibrato, or tremolo settings either. Keep a note of the patch settings you rely on. The instrument panel's Delay is a vibrato delay in ticks, not a delay before the note, and Tremolo is stored but not applied by the preview or the exporters.PAULA.SOUND player rather than the portable one: four voices, one per lane, each note's period taken from PAULA.NOTE_PERIOD so it follows the machine's PAL or NTSC table, and a master volume from 0 to 64 that is written straight into AUDxVOL so a held note changes level without clicking. See The Amiga player in the Music Workshop Guide for the details and the playable range, and demos/Amiga Music Demo — Beethoven's Ode to Joy as a four-part hymn — for a worked example with its exported player and a program that plays it..txt) and converts 2A03 pulse, triangle, and noise material into portable lanes, reporting each unsupported feature it dropped — expansion-chip data, DPCM, and effects it could not map. Import → FamiTracker module reads FamiTracker's own .ftm file with the same result, so a module does not have to be re-exported by hand first; modules up to FamiTracker 0.4.6 are read, and one saved by a 0.5.0 beta or by Dn-FamiTracker is refused by name rather than mis-read. There is no ProTracker .mod export, but there is an import: Import → ProTracker module reads a four-channel Amiga module directly — notes, order list, per-channel instruments, and the song's speed and tempo — converts the effects with an exact equivalent here (arpeggio, portamento, vibrato, speed/tempo, and an immediate note cut as OFF), and names the rest in its report. The module's samples are imported too, with their loop points and tunings, so an exported player replays the module's own instruments through Paula's DMA and the result sounds like the module. A sampled song exports as two files: the .cbi player and the <song>-samples.bin bank it embeds, written into src/ beside it. A six- or eight-channel module is accepted and reduced to its first four channels, with the loss named..crustymusic.json into music/. Export player include writes a MUSIC_* player into src/, with the same include-name and overwrite rules as the other workshops.The portable song can be tested against NES, C64, and other CrustyBasic targets. The separate SID Music Workshop remains the place for full SID register authoring, filter design, and chip-model-specific composition. Help → Music Workshop Guide walks through building a song here, its imports, and what the exported player carries.
Available for the C64, C128, and Plus/4 targets. It is a three-voice tracker designed around the SID's musical workflow, and it is the only editor here that writes real SID register behaviour.
C-4. The picker offers all sixteen states the selector can be in, and the oscillator waveform picker decodes its glyphs and says what the combinations do.Axy, Vxy, Uxx, Dxx, and Fxx, and the instrument tab lists what each does.src/: an asset marker, a commented quick-start block showing the @INCLUDE and the SIDM_INIT/SIDM_TICK calls, then SIDM_ROWS, SIDM_SPEED, SIDM_LOOP, the per-voice note, instrument, effect, and parameter arrays, the per-instrument tables, a frequency table for the project's clock, the enabled-voice table, and the SIDM_INIT, SIDM_START_ROW, SIDM_MODULATE, and SIDM_TICK procedures. A patch's wave program is exported as well: each step rewrites the control register at its own tick, so a programmed waveform change sounds on the machine the way it does in the preview, and a step after the gate has closed is written without the gate bit so it cannot restart the envelope. A patch's vibrato is exported as well, as a sine on the note's frequency built from a 32-step table and one phase per voice; the import measures a tune's vibrato and fills it in where playing it back beats holding the note. Tremolo, pulse-motion speed, and hard restart remain preview-only. Your game calls the player each frame; the include is data plus routine, not a finished game soundtrack driver. The dialog names the include after the file the tune was opened from — caverns-theme.c64music.json suggests caverns-theme.cbi, so a re-export lands on the same file — and lists the project's existing music includes so one can be selected and replaced, the same list-and-replace contract the other editors use. Only generated includes carrying a CrustyStudio music marker are offered..sng file. Only the first subtune is converted, the file's own tempo and speed are not imported, and the status message lists every feature that needed approximation — wavetable and pulse-table commands, filter modulation, per-row ADSR and waveform changes, portamento, and anything past the pattern or row limits. Import → MIDI converts in the same spirit: every instrument is replaced by one of four fixed patches and notes are ranked by pitch into the three voices, with the same 64-pattern ceiling. Import → SID file converts a PSID tune by running its own 6502 player and transcribing the SID registers it writes, so the tune's notes, patches and wave programs arrive as editable data; pitch and pulse movement inside a held note, a second SID chip, and off-key notes are listed rather than guessed at, RSID files are refused because they need the C64 ROM, and a tune longer than 64 patterns is truncated. All three can also be dropped straight onto the Workshop: while it is the editor on screen, a .sng, .mid, .midi or .sid file dropped anywhere on it is converted the same way, with a cue naming the formats it takes. Importing always starts a new asset, so Save asks for a name rather than replacing the original tune.A project is data plus a player; your game still has to include and call the player routine. The companion technical notes in docs/SID_MUSIC_WORKSHOP.md explain the hardware model and player design in more depth, and Help → SID Music Guide is the step-by-step tutorial.
The Oscillator waveform grid is the same control in the Sound Editor's C64 patch and in the SID Music Workshop's instrument tab, and it is the part of the interface that raises the most questions. It is not a menu of sixteen unrelated sounds. It is the SID's waveform selector, and each button is one of the sixteen states its four waveform bits can be in, with each bit switching one generator on. The glyphs are shorthand for those four generators:
| Button | Generator | Control bit | Register | What it does |
|---|---|---|---|---|
△ |
Triangle | 4 | $10 |
Soft and hollow, the quietest of the four. The only waveform ring modulation can reach. |
╱ |
Sawtooth | 5 | $20 |
Bright and buzzy: every harmonic at once, which is why it bites. |
▯ |
Pulse | 6 | $40 |
Hollow and reedy; Pulse width decides which harmonics are missing, and $800 is a square. |
▦ |
Noise | 7 | $80 |
A hiss with no pitch. This is the chip's percussion, not a note. |
∅ |
None | - | $00 |
Silent: no generator selected, so the gate opens and the envelope shapes nothing. |
A + between glyphs means those generators run at the same time, not one after another. The chip combines their outputs — it ANDs them bit by bit — so △ + ╱ is a shape that neither a triangle nor a sawtooth makes on its own, which is where the SID's characteristic hybrid timbres come from. The grid is always in the same order: the four plain waveforms, then the eleven combinations, then ∅ Silent:
△ · ╱ · ▯ · ▦ · △ + ╱ · △ + ▯ · ╱ + ▯ · △ + ╱ + ▯ · △ + ▦ · ╱ + ▦ · ▯ + ▦ · △ + ╱ + ▦ · △ + ▯ + ▦ · ╱ + ▯ + ▦ · △ + ╱ + ▯ + ▦ · ∅ Silent
As control-register bytes, in that same order: $10 $20 $40 $80 $30 $50 $60 $70 $90 $A0 $C0 $B0 $D0 $E0 $F0 $00. Those are the values the Live register view shows while a tune plays, and the values a .sid import reads back into a patch, so a byte in a register dump can be matched to the button that produced it.
Combinations are not a gimmick. Measured over 117 classic tunes and 1.26 million gated voice-frames, 2.3% of gated frames select more than one waveform, 19% of the tunes do it somewhere, and the commonest are △ + ▯ ($50) and ╱ + ▯ ($60). A lead you cannot reproduce with a single waveform is usually a combination.
Four things are worth knowing before you pick:
▯. On a patch with no pulse bit the slider moves a register the oscillator is not listening to, and the sound does not change.△. With ring modulation ticked and no triangle selected, the bit is set and nothing hears it, and the patch panel says so. Hard sync works whatever the waveform is.∅ Silent is a real setting, not an off switch. The gate still opens and the envelope still runs; there is simply no oscillator for it to shape. That is how a player takes a voice away for a frame, and how a hard-restarted hit begins, and it is a legitimate step in a wave program rather than an empty one.Every workshop that writes crustyBASIC data writes it into src/ as a .cbi include, and they share one safety rule. A generated include starts with a marker comment such as:
' @crusty-asset kind=tilemap name=platform.cbi target=c64
When you export over an existing .cbi, Crusty Studio reads that marker and decides:
Charset includes also record the machine they were generated for (target=c64 or target=atari8), and an export refuses to replace a generated include that belongs to a different machine.
Two exporters do not write a marker yet, and their output is therefore treated as hand-written on a second export. The NES Tilemap Editor's include and the SID Music Workshop's exact register player (SIDEX_*) both carry a normal header comment but no @crusty-asset line, so exporting over the same file again is refused. Delete the old file or export under a new name; the guard is behaving as designed, it is the marker that is missing.
That rule exists because a generated include once overwrote a hand-written level.cbi. If you want to own a file by hand, use a name you never export to. Refusals are reported in Output rather than inside the workshop, so look there if an export appears to do nothing.
This walkthrough makes a short original tune called Neon Patrol. It uses the same practical ideas associated with classic C64 players—small reusable patches, three-channel counterpoint, rapid arpeggios, and parameter motion—without copying a commercial melody or data.
Before touching the tracker, decide what each voice must contribute:
| Voice | Job | Register | Rhythmic idea |
|---|---|---|---|
| 1 | Bass and occasional kick | C-2 to A-2 | Root on beats, rests between phrases |
| 2 | Lead | C-4 to A-4 | Four-note call and answer |
| 3 | Harmony, echo, and percussion | C-3 to G-4 | Chord stabs on alternate rows |
Set the tune to 125 BPM, Speed 6, PAL, and start with the 6581 model. Use a 32-row pattern as one phrase. The important constraint is not to fill every cell: rests make room for transients and make the three lines readable.
sid-tune.c64music.json name or enter neon-patrol.c64music.json.Neon Patrol, add your name in Author, set BPM to 125, and set Speed to 6.neon-patrol-starter.c64music.json from the Explorer.Select Instrument 1 and set:
Neon Bass$480 (a narrower pulse is brighter than the square midpoint)0, Decay: 4, Sustain: 9, Release: 25 ticks180, Motion speed: 1.5 HzSelect Audition patch. If the note is too soft, raise sustain; if it blurs into the next note, lower gate length or release. The pulse motion is the animated part of the patch—keep it subtle on bass so the rhythm remains solid.
Select Instrument 2 and choose:
Neon Lead2, Decay: 6, Sustain: 10, Release: 46 ticks0,4,7 for a major colour12¢, Vibrato speed: 6 HzAudition the patch again. Vibrato is deliberately delayed by the instrument's vibrato-delay setting, so the start of a note stays clear before the pitch begins to move.
Select Instrument 3, choose Noise, and make it short:
Noise Snap0, Decay: 2, Sustain: 0, Release: 12 ticks0:noise, 1:pulseThe wave program turns the noise transient into a pulse almost immediately. That is a compact way to imply a drum without spending another oscillator or importing a sample.
In the tracker, each row has a note, instrument number, and optional effect for each of the three voices. The following is a useful first phrase; use the controls to enter the notes and instrument numbers, leaving unlisted cells as rests (···).
Row Voice 1 Voice 2 Voice 3
00 C-2 · 1 C-4 · 2 C-3 · 3
04 G-2 · 1 E-4 · 2 G-4 · 3
08 A#2 · 1 G-4 · 2 A47 ··· · 3
0C G-2 · 1 E-4 · 2 C-4 · 3
10 C-2 · 1 C-4 · 2 ··· · 3
14 G-2 · 1 D-4 · 2 G-4 · 3
18 A#2 · 1 G-4 · 2 V26 ··· · 3
1C G-2 · 1 E-4 · 2 C-4 · 3
The table shows the accented rows. Fill the rows between them with rests first, then add passing notes only when the phrase needs more motion. A47 means a rapid root/major-third/perfect-fifth arpeggio on the lead; V26 adds a modest vibrato override to one longer note.
Voice 3 does not need to play harmony continuously. Put the chord stabs on rows 00, 04, 0C, 14, 1C, then use short noise snaps on rows 08, 18, and 1E. This is the time-sharing principle: the ear hears a chord, then a drum, while the chip still has only three voices at any instant.
If the arrangement feels crowded, delete notes before changing instruments. Silence is an arrangement tool, not an unfinished cell.
Choose + Pattern, rename it Neon Patrol B, and duplicate the phrase's bass and lead. Make three changes:
D-2 and A-2 passing notes to the bass on rows 06 and 16.The new pattern is appended to Song order automatically. The current UI gives one order entry per pattern you create or duplicate; to repeat an earlier pattern, duplicate it or edit the order array in the saved JSON project. Turn Loop on while composing; turn it off when you want the generated player to stop and call SID.ALL_OFF at the end.
Open the SID chip tab. Start with Low-pass, cutoff around 1180, resonance 7, and master volume 13–15. Only instruments with Route through filter participate in the filtered signal. Raise cutoff for a brighter chorus; lower it for an intro or a darker bass.
Switch between MOS 6581 and MOS 8580 while the song plays. The 6581 preview is warmer and more nonlinear; the 8580 is cleaner and more predictable. If the melody only works at one exact cutoff, simplify it—the physical chips vary.
Choose Play song and listen for three things: can you follow the bass pulse, does the lead answer itself, and does the third voice add colour without masking the melody? Use Audition patch while a cell is selected to fix an instrument without replaying the whole tune.
Save the tune to the project's music/ folder. The .c64music.json file stores notes, pattern order, instruments, effects, filter settings, chip model, clock, and loop state. Saving also adds the relative asset path to the .crusty manifest.
Choose Export .cbi. The generated include contains the flattened song arrays and a small player:
@INCLUDE "neon-patrol.cbi"
PROC MAIN
SIDM_INIT
WHILE 1 = 1
SIDM_TICK
DELAY 1
ENDWHILE
ENDPROC
Call SIDM_INIT once. Call SIDM_TICK at the target's display refresh cadence—50 Hz for PAL or 60 Hz for NTSC, as noted in the generated file. The include writes SID.FREQ, SID.ADSR, SID.CTRL, SID.PULSE_WIDTH, gate, filter, and master-volume values. Your game loop decides when to tick the player and when to pause it for menus, sound effects, or gameplay events.
Play the song in both chip models, then test it beside the game's sound effects. Leave space in the arrangement for priority sounds. A good game cue has a recognisable contour after one listen, a strong rhythm even with the lead muted, and a safe ending when its loop is disabled.
The SID is three oscillators plus a shared filter, but a player can rewrite those registers every refresh. Classic composers used that time dimension as part of the instrument: a patch could change waveform, pitch, pulse width, envelope, or filter after the note began. The Workshop exposes those decisions directly so you can design the player behaviour and the note data together.
For further reading, compare the Workshop's controls with the Commodore 64 Programmer's Reference Guide sound chapter, Rob Hubbard's April 1986 Electronics & Music Maker interview, and Martin Galway's released Wizball music source. These references explain the hardware and historical player techniques; Neon Patrol and the Workshop's starter arrangement are original.
Saving an asset updates the manifest so it appears in the Explorer. The compiler does not automatically draw every asset just because it exists: source code must load, include, or reference the generated data according to the target's crustyBASIC APIs.
One C64 compatibility path is built in for the Space Invaders asset set. When the expected C64 sprite and charset files exist, a build can generate generated/space-invaders-3-assets.cbi containing byte arrays for those assets. Other projects can copy the same idea with their own include files and code.
The one rule worth following: the machine belongs in the asset's extension and in the export's symbol prefix, so the file name itself should name the thing, not the machine. sprites/c64-ship.c64sprite.json says C64 three times over — in the extension, in the generated @crusty-asset … target=c64 marker, and in the C64_SPRITE_ prefix — and exports C64_SPRITE_C64_SHIP. Called sprites/ship.c64sprite.json, it exports C64_SPRITE_SHIP, which is what the game's code wants to read.
Two machines needing different art for the same thing are simply two files, and they cannot collide because both the extension and the prefix differ: ship.c64sprite.json gives C64_SPRITE_SHIP and ship.atari8sprite.json gives ATARI_SPRITE_SHIP.
Each editor derives the symbol from a different place, which is why a name that looks fine in one is doubled in another:
| Asset | Symbol comes from | Example |
|---|---|---|
| C64 sprite | the asset file name | ship.c64sprite.json → C64_SPRITE_SHIP |
| Atari 8-bit sprite | the asset file name | ship.atari8sprite.json → ATARI_SPRITE_SHIP |
| Atari 7800 sprite | the asset file name (its palette loader uses the include's name instead) | ship.atari7800sprite.json → ATARI7800_SPRITE_SHIP |
| Charset / Atari tiles | the export file name | art.c64charset.json exported as art.cbi → C64_CHARSET_ART |
| Tile map | the tile map project's own name field |
— |
| Portable shapes | the asset file name plus each shape's name | art.shapes.json holding SHIP_A → SHAPES_ART_SHIP_A |
| SID music | always SIDM_*; the file name appears only in the header comment |
— |
Because the charset and tile symbols come from the export file name rather than the asset name, a charset asset is worth exporting under a name that matches it — charsets/art.c64charset.json to src/art.cbi — or the symbols stop describing the asset they came from. Sprite includes have their own rule: the name must end in -sprites.cbi.
A new asset's default name carries no machine prefix — sprite.c64sprite.json, sprite.nessprite.json, sprite.atari8sprite.json, sprite.atari7800sprite.json — because the machine is already in the extension, in the generated @crusty-asset marker, and in the exporter's own symbol prefix. Naming a file c64-sprite.c64sprite.json would export C64_SPRITE_C64_SPRITE.
Renaming an asset changes the symbols it exports, so rename it before the code uses the include, or update the code in the same pass. Re-exporting after a rename writes a new include; the old one is left alone, because it is a different generated file.
The Project menu remembers the ten most recently opened or saved projects. Choose one from Recent projects to reopen it; dirty files are still protected by the save-before-switching dialog. Rename project… changes the project's display name, which is stored in the manifest and shown in the Explorer and the recent-projects list; it does not rename the project folder or the .crusty file. The project title in the Explorer can also be double-clicked to rename it.
The magnifier in the Explorer heading searches the whole project; see the crustyBASIC editor for the results workflow.
In the desktop app, choose File → Import into project or drop files onto the editor. Source files and .cbi includes go into src/ and open as tabs; a source file that is already inside the project is moved there after a confirmation naming both paths, because the build only reads source under src/. Recognised assets are copied into the appropriate project folder and opened in their workshop. Other files are copied to the project root.
A module or sample dropped on the Music Workshop is imported, not copied. While the Music Workshop is on screen, dropping a .mod (or, for an Amiga project, a WAV or IFF 8SVX sample) hands the bytes to the Workshop, which owns the parse: the file is stored in music/ and the song on screen is replaced by the imported one. A second module dropped over the same window replaces the song again, and a module that cannot be read is reported above the editor with the reason instead of leaving the old song silently in place. Dropping a module does not need a project open first.
A music file dropped on the SID Music Workshop is imported instead of copied. While that Workshop is the one on screen, dropping a .mid, .midi, .sng or .sid file anywhere on it runs the matching converter and opens the result as a new tune, the same as Import in its header. The drop cue appears while the file is over the Workshop, and says which formats it takes. Everywhere else a dropped file keeps the routing in the table below — a .sng dropped on the editor is still copied into music/ as an asset, and .mid and .sid are not project assets at all.
Dropping a project file is not an import. A .crusty is the project itself, so dropping one anywhere on the window closes what is open and opens that project — the same thing Project → Open does (and File → Open file… also opens a .crusty as a project), and it is the shortest way to move between projects. The drop overlay names the project it is about to open while the file is over the window. If files are dropped alongside the project they are left where they are, and Output reports how many were ignored; import them afterwards if you want them in the new project. A .crusty chosen through File → Import into project is still imported rather than opened, because that command asked for an import.
Neither route gets past unsaved edits: the switch asks about them first, as described below.
Recognised source extensions are .cbs, .cbi, and .bas. Everything else is routed by extension into the matching project folder and opened in its workshop where there is one:
| Family | Extensions accepted | Destination |
|---|---|---|
| C64 sprites | .c64sprite.json, .c64sprite, .sprite.json, .sprite |
sprites/ |
| NES sprites | .nessprite.json, .nessprite |
sprites/ |
| Atari 8-bit sprites | .atari8sprite.json, .atari8sprite |
sprites/ |
| Atari 7800 sprites | .atari7800sprite.json, .atari7800sprite |
sprites/ |
| Apple II graphics | .apple2graphics.json, .apple2graphics, .apple2lores.json, .apple2lores |
sprites/ |
| C64 charsets | .64c, .c64charset.json, .c64charset, .charset.json, .charset |
charsets/ |
| Atari 8-bit tile glyphs | .atari8tiles.json, .atari8tiles (earlier .atari8charset.json, .atari8charset) |
tiles/ |
| Tile maps | .c64tilemap.json, .c64tilemap, .nestilemap.json, .nestilemap, .tilemap.json, .tilemap |
tiles/ |
| Portable shapes | .shapes.json, .shapes |
shapes/ |
| Sound | .c64sound.json, .c64sound, .nessound.json, .amigasound.json, .sound, .sfx |
sound/ |
| Music | .crustymusic.json, .c64music.json, .c64music, .music, .mod, .sng, .ftm, .fmt |
music/ |
| Samples (Music Workshop) | .wav, .8svx, .svx |
music/ |
| Source | .cbs, .cbi, .bas |
src/ |
| Anything else | project root |
An imported file from elsewhere is copied to the destination folder under its own name. An asset that is already inside the project keeps its path, but a .cbs or .cbi is different: project source belongs under src/, so a source file imported from elsewhere in the project folder is moved there after a confirmation naming both paths, and an open tab follows it. Sprite, charset, and tile-map imports then open in their workshop, so you can carry on editing straight away. Two kinds of file are converted on the way in: a C64 .64c charset binary becomes an editable .c64charset.json project with its load address read and its glyphs padded to 256, and a module or sample dropped on the Music Workshop is read and opened rather than merely copied (see above). Other music files are copied as they are and listed in the manifest; the GoatTracker .sng conversion is a separate step performed from inside the SID Music Workshop. Only a .cbs can become a project's entry point, so an imported .cbi is never recorded as main.
Folder pickers remember where they were last used, so importing a second file from the same folder opens the picker there rather than at the projects root. A save inside a project is the exception: it stays within the project's own folder, because a source file written elsewhere would be outside the tree the build reads.
When switching projects, the app asks whether dirty source files should be saved first. The files are listed one per row, each with its own tick box and all of them ticked, because the answer is often neither "save everything" nor "save nothing": a demonstration you mean to keep goes in beside an experiment you do not. The button says how many will be written, files left unticked are discarded when the switch goes ahead, and Cancel abandons the switch entirely — nothing is written and the open project stays exactly as it is. A file that has never been saved has no path to show, so its row says so, and ticking it asks where to put it; cancelling that file chooser cancels the switch too. Project → Open, Recent projects, dropping a .crusty and Project → New all ask the same question in the same way. The Explorer rescans every project subfolder, so files added externally appear automatically; source files added under folders such as src/includes/ are available as editor tabs. If an open file changes outside the app, the editor offers a choice to reload the disk version or keep the current in-app version, and the tab is marked with a blue ↻ while the choice is outstanding. Files that are not open are compared against the project as it was when it was opened, so an external edit to a text file such as crustybasic.config.toml or an .asm listing is marked in the Explorer too; opening or reloading the file clears the mark. The app's own saves, exports and builds never count as external changes, and hidden files such as the compiler's transient check sources are ignored.
| Action | macOS | Windows/Linux |
|---|---|---|
| New file | ⌘N |
Ctrl+N |
| Open file | ⌘O |
Ctrl+O |
| Save file | ⌘S |
Ctrl+S |
| Close tab | ⌘W |
Ctrl+W |
| Undo | ⌘Z |
Ctrl+Z |
| Redo | ⇧⌘Z |
Ctrl+Shift+Z |
| Find | ⌘F |
Ctrl+F |
| Settings | ⌘, |
Ctrl+, |
| Build | ⌘B |
Ctrl+B |
| Build and run | ⌘R |
Ctrl+R |
| Format casing | ⌘⇧U or ⌘⇧P |
Ctrl+Shift+U or Ctrl+Shift+P |
| Go to declaration or usages | Hold ⌘ and click, or F12 |
Hold Ctrl and click, or F12 |
| Increase code size | ⌘+ or ⌘= |
Ctrl+Plus or Ctrl+= |
| Decrease code size | ⌘− |
Ctrl+− |
| Reset code size | ⌘0 |
Ctrl+0 |
You can also hold Command or Control and scroll over the code editor to zoom code without changing the rest of the interface. The status bar's code-size button (for example Code 14px) shows the current editor size and resets it when clicked.
Help → Crusty Studio Guide opens this guide in an editor tab. Four companion guides cover one workshop each in depth:
.64c import, choosing an export range, and the Atari tile side.Headings in each guide are clickable through the table of contents, and the help tab has its own scroll area. Help → Crusty Basic Help opens the language documentation shipped beside the configured compiler. Use the Studio Guide for app workflow and the Crusty Basic reference for language syntax and runtime APIs.
Help → About Crusty Studio shows the version, the build date and commit the running copy was made from, the versions of the external tools it can drive, and the emulators it knows about — the quickest way to tell which build you are actually running and why an emulator is not being found.
Open Tools → Settings → Toolchain, choose the compiler executable, and run Check installation. If the compiler is installed beside the project tools, leave the field empty and let the automatic search try common locations and PATH.
Install the VASM CPU builds the targets need and point VASM folder at the folder holding them. The VASM page has the 6502 build under Last release binaries — take the build for your operating system from there — and this site publishes the 6502, Z80 and 6809 builds for macOS and Linux, and the 6502 and Z80 ones for Windows, in Getting VASM. A ZX Spectrum, MSX or Game Boy build needs vasmz80_oldstyle; a C64, NES or Atari build needs vasm6502_oldstyle; a Color Computer build needs vasm6809_oldstyle, which has no Windows download because the author publishes none, so on Windows build it from the source archive. An Amiga or Atari ST build needs vasmm68k_mot, which is the Motorola-syntax module rather than one of the oldstyle CPU builds, so it is built from the same source archive with CPU=m68k SYNTAX=mot; it is not published as a download here. Put them in tools/assemblers/vasm/<platform>/ beside the compiler, or in any folder of your own, and select that folder as VASM folder; keep the names they arrive with, because the downloads carry the platform they were built for (vasm6502_oldstyle_linux-x86_64, vasm6502_oldstyle_macos-universal, vasm6502_oldstyle_windows-x64.exe) and only this platform's file is accepted. If only one CPU build is present, Run reports the missing one by name. It rejects versions below the supported 2.x line.
Only the x86 targets need NASM: dos16, h120, pcjr, tandy1000, linux64 and win64. Install NASM 2.x from nasm.us, or with brew install nasm on macOS or sudo apt install nasm on Debian and Ubuntu, then select the executable as NASM assembler. The other targets build with VASM alone, so a missing NASM does not stop a C64, NES or Atari build.
Add the folder containing the emulator in Emulator folders, not only a shortcut or unrelated ROM folder. Nested folders are searched. Confirm that the target directive matches the emulator you installed; for example, C64 uses VICE while NES uses Mesen, FCEUX, or Nestopia.
That is expected. Retro Virtual Machine takes no program on its command line, so Run can only start it on the right machine. Drag the built file shown in the output panel onto the emulator window to load it. See Retro Virtual Machine cannot load a program. If MAME is installed for the same target, choosing it in the Target menu makes Run load the program itself.
MAME ships no ROMs and needs a BIOS set for every system. The message names the set, the folder it looked in, and the files that belong inside it. Put the set there and run again; the check is repeated, so a set added after the message is picked up without restarting. If the sets live somewhere else, point MAME ROM folder in Toolchain settings at that folder.
VICE's packages on Linux and BSD are built without the copyrighted ROM images, so a machine whose images were never supplied cannot start. The message names the folder VICE searched and says which machine's subfolder to use; put the images there — /home/you/.local/share/vice/C128 for the C128, for example — and run again. VICE names each image it still wants, and Run prints that, so a partial set can be completed a file at a time — except on the C128, whose start-up check requires every language variant of its KERNAL and character ROM, so it needs the machine's whole ROM folder. A C64 that runs while the C128, VIC-20 or Plus/4 does not is the same cause: the C64 folder is populated and the other machine's is not. The VICE builds for macOS carry the images, which is why it works there. VICE's own log is at ~/.local/state/vice/vice.log.
The output panel carries VICE's own explanation now, and the two it gives are worth telling apart. Couldn't load ... ROM, or Machine initialization failed, is a missing ROM set — see Run says VICE cannot load the ROM images. AUTOSTART: Error - No idea what disk image format to use is a machine with no drive: autostarting a PRG builds a temporary disk image, and the drive type is what tells VICE which format to build. Run supplies -drive8type 1571 for the c128 family, whose emulator defaults to no drive, so this only appears if that choice is overridden from outside the studio — by a drive type in VICE's own settings, or a -drive8type of your own.
Check the first @OPTION TARGET or @OPTION SYSTEM line in the active source. The value is case-insensitive, and the part before a dot determines the family. Save the file if you want the project manifest and future sessions to retain the intended target.
The Tools menu offers each editor for the machines the project builds for, not only for the machine currently being built, so a multi-machine project can open the C64 Sprite Workshop while the Amiga is active. When more than one machine in the project has the same editor — several of them have a sprite editor — the entry opens a submenu naming them, and the machine currently being built is listed first. When only one machine has it, the entry opens it directly and says which machine in brackets if that is not the active one. An entry is greyed out only when no machine in the project uses that editor at all; adding a machine in the Target menu brings it back.
Which machine each editor is for: C64 Sprite, Sound (SID), and SID Music are for the C64, C128, and Plus/4 targets; NES Sprite and Sound (2A03) are for NES; the Sound Editor also serves the Atari 7800 (TIA) and the Amiga (Paula); the Charset/Tile Editor serves the C64 family and the Atari 8-bit and 5200 (the machine chosen picks which side of it opens), and the Tilemap Editor covers C64, NES and the Atari 7800; Atari Sprite is for the Atari 8-bit and 5200; Atari 7800 Sprite is for the Atari 7800; Apple II Graphics is for Apple II; the Music Workshop and the Portable Shapes Editor are machine-neutral and are offered for every target.
Opening an existing asset from the Explorer always works too, because a saved asset records the platform it was authored for — the file's own extension decides which editor opens it, whatever machine is active.
That is the generated-include check working. Crusty Studio only replaces a .cbi that carries the @crusty-asset marker line with a matching kind; anything else is treated as hand-written source and left alone. Export to a different name, or delete the file after checking its contents. The refusal message appears in Output, not in the workshop.
The C64 Sprite Workshop is the only asset editor with an undo history. The Tilemap Editor, the Charset/Tile Editor (both the C64 charset side and the Atari tile side), the Portable Shapes Editor, the sound and music editors do not, and neither do the NES, Atari, Atari 7800, or Apple II graphics editors. Saving before a large change is the practical safety net, and a workshop auto-saves an asset that was opened from the project. Deleting a metatile clears the map cells that used it, resizing a tile map discards what falls outside the new bounds, and switching a charset between standard and multicolour converts every glyph.
Use Tools → Settings → Appearance for the complete interface. For code only, hold Command or Control while scrolling over the editor, or use the code-size keyboard shortcuts. Larger panels scroll internally, and the sidebar and Output panel can be resized.
Confirm that the declaration and the calling source are part of the same project and that the include path is written relative to the including file. Target-specific include patterns are supported; if several target files define the same procedure, Crusty Studio may show a chooser. Navigation removes the declaration itself and duplicate path representations, but it does not treat an unrelated source file containing the same text as a valid reference.
The tool check reports when Gatekeeper has quarantined a helper. Review that download and follow the exact command shown in the diagnostic to clear the quarantine attribute, then run Check installation again.
Browser preview is intentionally safe and limited: it uses simulated tool checks and a simulated C64 build, keeps files in browser memory or downloads workshop assets, and cannot launch native emulators. Use the desktop app for the complete filesystem and toolchain workflow.
For a small retro game, this sequence works well:
src/main.cbs as the entry point and split reusable routines into additional source or include files.The Studio is the organiser and feedback loop around your game. crustyBASIC remains the language and runtime, the assembler turns the compiler's output into machine code, and the emulator provides the target machine's final test.