Work on the Little League Baseball Championship Series 32-team ROM hack in this repo — build chain, bank layout, bracket state, selector, stock game handoff, and known traps. Use when editing teams, rosters, brackets, banks, the selector, or launching games from the hack.
Install
npx skillscat add davefmurray/little-league/llbcs-32-team Install via the SkillsCat registry.
LLBCS 32-team ROM hack
Locked target: 32 teams × 20 players, 32-team bracket, 256 KiB PRG + 128 KiB
CHR, MMC1, emulator-first. Stock 16-team path must keep working.
Repo root: /Users/dfm/GitHub/nesdav/little-league
Base ROM: Little League Baseball - Championship Series (USA).nes
Report: docs/AUTORUN-REPORT.md · Plan: docs/PLAN-32-TEAM.md
Build chain
BASE="Little League Baseball - Championship Series (USA).nes"
.venv/bin/python tools/expand_rom.py "$BASE" -o build/32x20.nes
.venv/bin/python tools/build_32x20.py build/32x20.nes data/roster.json -o build/32x20-ui.nes
.venv/bin/python tools/build_select_screen.py build/32x20-ui.nes data/teams32.json -o build/select-screen.nes
python3 tools/build_dispatcher.py build/select-screen.nes -o build/dispatcher.nes
.venv/bin/python tools/build_bank11.py build/dispatcher.nes -o build/bank11.nes
.venv/bin/python tools/build_bracket_screen.py build/dispatcher.nes -o build/bracket-screen.nes
.venv/bin/python tools/build_bracket_input.py build/bracket-screen.nes -o build/bracket-input.nesbuild_bracket_screen.py owns bank 11's code/data, so it replaces the bank-11
logic image. Use --loop-to 0x8100 when input code will be added, and--seed-human N only for test builds (production reads $07C3).
Never commit build/ or *.nes. Build outputs are gitignored on purpose.
Bank map (expanded ROM)
| Bank | Use |
|---|---|
| 8 | 32 rosters, 32×$170 bytes, pointer table $7F00 |
| 10 | 32-team selector |
| 11 | bracket scene: screen $8000, input $8100, seed $8200, bracket table $A000, codes $B000, palette $B100, list $B200 |
| 15 | fixed bank (was 7 in stock) |
Only $C000–$FFFF is fixed. Any address $8000–$BFFF needs a bank tag.
State map
| Address | Meaning |
|---|---|
$07C0–$07C2 |
selector page / cursor |
$07C3 |
selected team ($FF = none) |
$07C6–$07C9 |
pad scratch + edge state |
$04EB/$04EC |
alive flags ($80) |
$04EF/$04F0 |
raw team IDs for the two sides |
$047B |
scene index; stock field = 3 |
$047C |
frame/scene tick |
$047D |
vblank handshake |
$04E6 |
scene-transition request |
$C1 |
game/mode flags; stock field $76 |
$0746 |
matches completed |
$074B/$074C |
16-bit result bits |
$7700 |
63 bracket slots (cart RAM) |
$7740 |
32 win counts |
$7760 |
round / match |
Stock field state observed: $04EF=$0D (Italy), $04F0=$02 (Pennsylvania),$047B=$03, $C1=$76, $0746=$0000.
Stock menu path
Team select's prompt is not confirm/cancel:
CONTINUE= keep editing teams → back to mapEND= finish selection
Full path to a field:
team select → END → PASSWORD → A → matchup
→ A → SPEED MODE / WATCH THE GAME → down + A
→ pre-match cutscene → fieldDirectly poking the field state from the 32-team bracket returns to the bracket;
the missing piece is the pre-match/scene init routine, not the handoff values.
Traps that already cost time
$CBE6is not an entry point. It isLDA #$00. Hooking it withJSR
makesSTA $2001use the wrong A. Hook at$CC06(STA $2000) after reset
init, and have the trampoline re-executeSTA $2000.- PRG RAM writes are dropped before CHR0 bit 4 is cleared. Call
LDA #0; JSR $C11Cbefore touching$6000–$7FFF. - NMI corrupts timing-sensitive cart-RAM writes. Disable with
LDA #0; STA $2000immediately before the 32-team seed; re-enable beforeJMP $CC09. - Every display-list team code must be 3 bytes.
"NY"shifted every later
run by one and sent the flush down the RLE path. Pad withcode.ljust(3)[:3]. - Custom byte builders must size zero-page operands as 2 bytes.
CMP $02is 2 bytes, not 3. Mis-sizing silently shifts branch targets. $04EF/$04F0are raw team IDs — confirmed by the bank-1 name renderer
at$AF40, which indexes team-name pointers with them.- Do not write
$C040. It is a ROM byte ($40) used byBITas a bitmask;
the flush checks bit 6 of each run's high byte for RLE. - jsnes 1.2.1 has an MMC1 shift-counter bug. Use fceumm through romdev for
real mapper behavior.
Verification
.venv/bin/python tools/verify_regression.py \
"Little League Baseball - Championship Series (USA).nes" build/32bracket.nesExpected: screenshots MATCH, system_ram differs in 0 bytes.
romdev checks that matter:
- selector renders after reset init
- bracket leaf 0 shows the seeded human (
FLAfor human 15) $7700..$771Fis 32 unique values, human at slot 0- stock field reachable through the menu path above
Current status
- 32 rosters and selector: working.
- 32-team seed + names: green (
$07C3→$7700, human at leaf 0). - Scene dispatcher: fixed hook at
$CC06, committed. - Game launch from the 32-team bracket: open. Best next step is capturing the
stock password→matchup→field init, not poking more state.