The game's content is data: the levels, their rooms and themes, the
level-select pages, the actor categories (the vehicle and boss stages),
the bosses' rewards, the music, the cutscenes, the credits, the text in
its six languages and the graphics. A plain make builds whatever data
the tree has, and make compare checks that the original data still
gives the original ROM byte for byte. A mod is other data: it needs no
code change and no baserom.gba.
mods/sample/ is a small mod that uses most of what is
moddable, and the worked example for this page. It adds a fifth world
(a fifth level-select page) after Neo Cortex's:
| what | where it comes from | the doc |
|---|---|---|
a new song, sample_tune, played by a new level theme and by the new cutscene |
an XM the mod writes (16-row patterns; a new instrument with its own synthesized sample, and a pool instrument) | audio.md, "Adding a song" |
a new theme, THEME_SAMPLE |
jungle's picture and colour cycle, the new song | levels.md, "Adding a theme" |
a new room, sample_grove/main (room 41) |
jungle jam's room under its own symbols, one 8x8 block repainted (a new tile) | levels.md, "Adding a room" |
a recoloured actor category, CATEGORY_SAMPLE_ICE |
snow crash's polar stage with its own OBJ palette and BG palette cycle | levels.md, "Category graphics" |
| three numbered levels, "sample grove", "sample slope", "sample sewer" | the new room, the new category's stage, just in slime's room | levels.md, "Adding a world" |
| a boss level, "sample showdown" | Dingodile's room, "boss_kind": "DINGODILE" (the boss HUD) and an "on_clear" that plays the new cutscene and marks it cleared |
levels.md, "Boss levels" |
| a partial level-select page and its number icons 21-23 | world 4 of "worlds", opened when world 3's levels are cleared; new frames of sprite bank 51 |
levels.md, "Partial pages", graphics.md, "New frames" |
| the 64 Kbit save | 29 levels, past the 4 Kbit save's 28 | levels.md, "The save" |
a new cutscene, CUTSCENE_SAMPLE_CLEARED |
a picture the mod draws, a page of text | data.md, "A new cutscene" |
| the levels' names and the cutscene's text in the six languages | entries of games/xs/text/crashxs.pot and the five .po files |
text.md, "A new level's name" |
Every asset is generated by the mod or derived from the tree's own data (the palettes are the ROM's recoloured), so it carries no copied art and no copyrighted music.
make mod MOD=mods/sample
applies the mod to a copy of the tree in build/mods/sample
(tools/apply_mod.py, whose code is crashdata.mods)
and builds it there with a
plain make. The modded ROM is build/mods/sample/crashbandicootxs.gba
(16 MB: the new data goes past the original's 8 MB). Your own tree is
not changed. In the modded tree,
make text-check
python3 tools/gfx_manifest.py check
python3 mods/sample/boottest.py
check the translations and the graphics manifest, and play the ROM in
headless mGBA (mgba, its SDL frontend, on PATH: 0.10.5, which CI
builds from source; see tools/boottest.py,
buildtools.boottest): the new room and its song,
the category's palettes on the screen, the fifth page and its boss entry,
the boss HUD, the cutscene after the boss and the 8 KiB save. CI
(.github/workflows/mods.yml) does all of this on every PR, so a change
that breaks modding fails there even when make compare passes.
In the game, the fifth page opens once the levels of the four ROM worlds are cleared; the boot test pokes that progress into the save instead of playing it.
A mod is a folder with a mod.py and, optionally, a files/ folder:
files/is copied over the tree, at the same paths: new files, or replacements of the tree's. XS's data is undergames/xs/(files/games/xs/graphics/...,files/games/xs/data/levels/...) and the code underengine/(files/engine/src/...), as in the tree (#1167, file_layout.md);- the paths
mod.pygives the helpers (tree.path(...),tree.json(...)) are the tree's too:games/xs/sound/gax_manifest.json,engine/src/game/...; mod.pydefinesapply(tree), which edits the copy's data with the helpers ofcrashdata.mods.Tree(tools/apply_mod.py's):tree.levels()andtree.json(path)edit a JSON file,tree.add_graphics(...)addsgames/xs/graphics/manifest.jsonentries,tree.copy_room(...)copies a room under new symbols,tree.add_level_name(...)andtree.add_cutscene_text(...)add text in the six languages,tree.run(...)runs a tool.mod.pyruns with the copy'stools/onsys.path, so it can import a tool by its old name (import jasc_pal, which iscrashdata.common.jasc_pal, as the sample does) or the packages themselves (from crashdata.common import jasc_pal; file_layout.md, "The Python packages").
tools/apply_mod.py MOD DEST applies it to a new copy at DEST, and
make mod MOD=<folder> does that and builds it. make data-check MOD=<folder> applies it and checks the copy's data without building:
every file against its JSON Schema, then the checks the build makes
(editors.md, "Checking the data"). To work on the data
directly instead, edit the tree itself, following the docs above, and
run make; make compare then fails, as it should. The editor
(make editor, editors.md) edits a mod's tree
(build/mods/<name>) the same way: new, resized and deleted rooms, drawn
tiles, new collision values, a room's kind and blend; applying the mod again makes the
tree anew, so what is kept goes into mod.py.
| area | moddable | the doc |
|---|---|---|
| levels | rooms (pictures, collision, entities), the level table, themes and their colour cycles, level-select pages and what opens them, names | levels.md |
| vehicle and boss stages | actor categories: spawn lists, numbers, cell animation, sky, OBJ palette, palette cycle, sprite sheet, animation table | levels.md, "Actor categories" |
| bosses | which boss a level has ("boss_kind") and what clearing a level does ("on_clear") |
levels.md, "Boss levels" |
| the save | grows with the levels (64 Kbit past 28), versioned by a hash of the level data | levels.md, "The save" |
| text | every string, in six languages; the credits | text.md |
| cutscenes | slides, pictures, timing, music, text | data.md, "Cutscenes" |
| music | new songs from XM files, on the ROM's instruments or new ones (an XM's own samples, or WAVs); new sound effects with their own samples; which song plays where; the music editor plays them and edits the tables, the songs' cells and the samples | audio.md, editors.md, "The music editor" |
| graphics | the graphics of the manifest, new ones, sprite banks and frames, palettes | graphics.md |
These are code, or not built yet:
- Editors. The level, cutscene and music editors (
make editor, editors.md) edit a mod's tree (build/mods/<name>, asmake modmakes it) as they edit the game's: the sample mod'ssample_tuneplays in the music editor, its own instrument too. What they don't do yet is in each editor's "Not yet"; for the music, an XM is still written in a tracker (the editor edits its cells, and plays them as GAX2 will), and a ROM instrument's envelope or loop is the manifest's (the "Data files" tab). - New behaviour. A new actor kind, boss kind, power, category type or entity type is code: the data reaches what the existing kinds draw and where they spawn, not what they do (levels.md, "Category sprite sheets" and "Boss levels").
- New sprite frames outside bank 51's animation 0. Other banks need their C to take new frames (graphics.md, "New frames").
- More than 60 levels, the 64 Kbit save's largest record.
- N-Tranced. The modding pipeline is Crash Bandicoot XS's; N-Tranced's data isn't decompiled yet (n_tranced.md).