Files
gen1recomp/mods/examples/example_jukebox/README.md
T
2026-07-19 16:18:18 -04:00

3.5 KiB

Jukebox Example

Adds one song authored note-by-note in Lua, swaps it in as the Pallet Town theme, replaces Mew's cry, and ships a jukebox screen that lists every song in the merged registry.

Persona: the Musician. Nothing here is a .ogg. The song is a Game Boy channel program assembled at load time by ChipAsm.

Try it

python3 tools/modkit.py validate mods/examples/example_jukebox --base imported
luajit mods/examples/example_jukebox/tests/example_jukebox_test.lua

# render the song to a wav to hear it without launching the game
python3 tools/modkit.py bounce Music_ExamplePalletRain --seconds 20 --out bounce

Enable it (example_jukebox = true under mods in options.lua, or the F10 manager), then open OPTIONS → JUKEBOX.

What it demonstrates

Seam Where
content.music:register (ChipAsm DSL) song.lua + main.lua
content.cries:override (chip program) main.lua
hooks:wrap("music.select") main.lua — one choke point covers map, battle and jingle music
content.screens:register main.lua — a screen factory the engine instantiates by id
hooks:wrap("ui.options.rows") main.lua — how the player reaches the screen
content.music:each main.lua — the jukebox list is the merged view, not a hard-coded array
mod:read main.lua — loading a sibling file through the loader's filesystem

Authoring a song

song.lua returns what ChipAsm.song{...} builds: a self-contained program blob plus its channel layout. Events are Lua tables, one per command:

{ duty = 2 },
{ notetype = { speed = 12, volume = 11, fade = 2 } },
{ octave = 4 },
{ label = "lead" },
{ note = "E", len = 6 }, { note = "D", len = 2 },
{ loop = { count = 0, to = "lead" } },

The assembler is the validator. An unknown note name or an out-of-range length raises there, naming the channel and event index, and main.lua turns that into one mod-attributed load error. The music system never latches on a bad program, because a bad program never reaches it.

Two shapes share the music registry and are dispatched per definition, not by a global flag: { chip = ... } for an authored program and { file = "..." } for an audio file. This example uses the first; a file track is one line:

mod.content.music:register("Music_MyTheme", { file = mod.path .. "/theme.ogg" })

Note the cry uses ChipAsm.sfx{...}.chip — the assembler returns { chip = program }, and a cry record wants the program under its own chip key.

Deferring is the parity guarantee

mod.hooks:wrap("music.select", function(next, chosen, ctx)
  if ctx and ctx.reason == "map" and ctx.mapId == "PALLET_TOWN" then
    return next(SONG_ID, ctx)
  end
  return next(chosen, ctx)
end)

Every path that is not Pallet Town calls next(chosen, ctx) with the argument it was given, so playback everywhere else is exactly what it was before the mod loaded.

Empty state

The jukebox is a ListMenu, which draws Nothing here. when its item list is empty rather than an empty frame. That cannot happen with the engine's 45 songs present, but it is the behavior a mod screen owes the player.

Permissions

This mod declares engine_internals, because playing a song from a screen currently needs require("src.core.Music") — the mod surface has no audio playback facade yet. Declaring it is the honest path: modkit validate accepts a declared require and flags an undeclared one.

Credits

  • Pallet Rain arrangement: this project.
  • pret/pokered: the channel command set ChipAsm assembles to.