Skip to content

Commit 39cf2ff

Browse files
committed
feat: Super Game Boy day/night borders, chosen by the seed
Each sea gets a day or night SGB border selected by bit 0 of wSeed16, beamed over with CHR_TRN/PCT_TRN VRAM transfers while the screen is frozen via MASK_EN. Sent once per voyage from EnterSail (and at boot when a save exists, so the title screen gets the saved sea's sky). - tools/png2sgb.py converts 256x224 PNGs to SNES 4bpp tiles + 32x28(+1) map + three 15-color palettes (color quantize + least-damaging tile merges to fit the 256-tile CHR RAM); output committed as src/sgb_*.inc - sgb.asm: JOYP packet sender, SGB VRAM transfer, screen freeze/restore - detection via boot-ROM C register ($14 = SGB/SGB2); header $0146=$03 and $014B=$33 unlock SGB functions; ROM grows to 64 KiB (banks 1-2) - test_sgb.py: header bytes, DMG-inert, forced full-path transfer run - verified visually in Mesen2 (SGB2 firmware): day + night borders at the title screen and while sailing
1 parent a70be6b commit 39cf2ff

12 files changed

Lines changed: 1935 additions & 3 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ tools/rgbds/
88
build/
99
*.gb
1010
*.gb.ram
11+
12+
# Python test artifacts
13+
__pycache__/

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ RGBASM := $(RGBDS)rgbasm
55
RGBLINK := $(RGBDS)rgblink
66
RGBFIX := $(RGBDS)rgbfix
77

8-
SRC := src/main.asm src/joypad.asm src/rng.asm src/tiles.asm src/sail.asm src/world.asm src/port.asm src/combat.asm src/isles.asm src/sound.asm
8+
SRC := src/main.asm src/joypad.asm src/rng.asm src/tiles.asm src/sail.asm src/world.asm src/port.asm src/combat.asm src/isles.asm src/sound.asm src/sgb.asm
99
OBJ := $(SRC:src/%.asm=build/%.o)
1010

1111
pirates_folly.gb: $(OBJ)
@@ -15,6 +15,14 @@ pirates_folly.gb: $(OBJ)
1515
build/%.o: src/%.asm src/defs.inc src/testmap.inc include/hardware.inc | build
1616
$(RGBASM) -o $@ -I include/ -I src/ -Wall $<
1717

18+
build/sgb.o: src/sgb_day.inc src/sgb_night.inc
19+
20+
# Regenerate the SGB border data after editing the art (needs numpy):
21+
src/sgb_day.inc: res/sgb_day.png tools/png2sgb.py
22+
python3 tools/png2sgb.py $< $@ sgb_day 1
23+
src/sgb_night.inc: res/sgb_night.png tools/png2sgb.py
24+
python3 tools/png2sgb.py $< $@ sgb_night 2
25+
1826
build:
1927
mkdir -p build
2028

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ Assemble all nine, survive the final fleet, and the Treasure of the Nine
1212
Isles is yours.
1313

1414
Runs on DMG, CGB, AGB, SGB, emulators, and Analogue Pocket. DMG-first
15-
(4 shades), with color palettes auto-detected on CGB.
15+
(4 shades), with color palettes auto-detected on CGB. On a Super Game Boy
16+
each sea gets a day or night border, chosen by the seed itself.
1617

1718
## Playing
1819

@@ -89,6 +90,7 @@ python tests/lint_worlds.py # seed sweep: spawns, isle land, port census
8990
- `test_title.py` / `test_cgb.py` — title screen, CGB palette init, DMG/CGB parity
9091
- `test_m2.py``test_m5.py` — worldgen/streaming, ports & economy, combat, the Nine Isles
9192
- `test_sound.py` — APU driver and song/SFX triggers
93+
- `test_sgb.py` — SGB header/detection, forced-run of the border transfer
9294
- `test_ports_m6.py` — port content pass
9395
- `tests/lint_worlds.py` — reimplements the worldgen math in Python and sweeps
9496
16 seeds against the running ROM, validating spawns land on water, every
@@ -110,13 +112,17 @@ src/ RGBDS assembly (the whole game is hand-written SM83)
110112
isles.asm the Nine Isles, digs, final battle, victory
111113
port.asm docking, market, tavern, repair/recruit, battery save/load
112114
sound.asm 3-channel shanty driver + SFX (no hUGEDriver)
115+
sgb.asm SGB border transfer (CHR_TRN/PCT_TRN), seed-picked day/night
116+
sgb_day.inc, sgb_night.inc generated border data (tools/png2sgb.py)
113117
tiles.asm hand-drawn 2bpp tiles & 3x5 font (gfx literals), CGB palettes
114118
rng.asm Mul8, Mix16 coordinate hash, xorshift16 PRNG
115119
joypad.asm input with new-press detection and auto-repeat
116120
defs.inc shared constants (states, tiles, tuning knobs)
117121
gen.asm, testmap.asm legacy M0/M1 generator-lab artifacts (not built)
118122
tests/ PyBoy headless test suite + world lint + balance tuner
119-
tools/ gen_testmap.py (legacy), vendored RGBDS (gitignored)
123+
res/ SGB border art (256x224 PNGs)
124+
tools/ png2sgb.py (SGB border converter), gen_testmap.py (legacy),
125+
vendored RGBDS (gitignored)
120126
docs/ PIRATE_GAME_PLAN.md, PIRATE_LORE.md, dev references
121127
MANUAL.md the player's manual
122128
```
@@ -138,6 +144,11 @@ MANUAL.md the player's manual
138144
districts), port names (16×16 prefix/suffix), market price drift, tavern
139145
rumors, encounter rolls, and the Nine Isles' positions are all pure hash
140146
functions of coordinates + seed — deterministic and storage-free.
147+
- **SGB borders.** On SGB/SGB2 (detected via the boot ROM's C register),
148+
the border is beamed over with CHR_TRN/PCT_TRN VRAM transfers while the
149+
screen is frozen with MASK_EN: 256 SNES 4bpp tiles, a 32x28+1 map, and
150+
three 15-color palettes per border, all generated offline from PNG art.
151+
Day or night is bit 0 of `wSeed16` — the sky is part of the world.
141152
- **Battery save.** MBC5 SRAM at `$A000` with magic bytes, a version field,
142153
and a checksum; validated on boot. Isle positions are recomputed from the
143154
seed on load, never saved.

res/sgb_day.png

5.02 KB
Loading

res/sgb_night.png

4.56 KB
Loading

src/main.asm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ SECTION "VBlank vector", ROM0[$0040]
1515
SECTION "Header", ROM0[$0100]
1616
nop
1717
jp EntryPoint
18+
ds $0146 - @, 0 ; rgbfix -v fills the logo and checksums
19+
db $03 ; $0146: SGB flag — unlock SGB functions
20+
ds $014B - @, 0
21+
db $33 ; $014B: old licensee $33 — required for SGB
1822
ds $0150 - @, 0
1923

2024
SECTION "Shared WRAM", WRAM0
@@ -31,11 +35,14 @@ wSeedNib:: ds 8
3135
wSeed16:: dw ; seed folded to 16 bits for the generator
3236
wRngState:: dw
3337
wIsCGB:: db ; $11 = CGB/AGB (boot ROM leaves it in a)
38+
wIsSGB:: db ; $14 = SGB/SGB2 (boot ROM leaves it in c)
3439

3540
SECTION "Main", ROM0[$0150]
3641
EntryPoint:
3742
di
3843
ld [wIsCGB], a ; boot ROM: $11 on CGB/AGB, $01 on DMG
44+
ld a, c
45+
ld [wIsSGB], a ; boot ROM: $14 on SGB/SGB2, $13 on DMG/MGB
3946
ld sp, $E000
4047

4148
.waitVb ; LCD is on after boot ROM; find VBlank
@@ -135,6 +142,19 @@ EntryPoint:
135142

136143
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCK01
137144
ldh [rLCDC], a
145+
; SGB: give the title screen the saved voyage's border, if there is one
146+
; (no save: the seed isn't final until the editor, so EnterSail does it)
147+
ld a, [wHasSave]
148+
and a
149+
jr z, .noBorder
150+
call SGBTransferBorder
151+
and a
152+
jr z, .noBorder
153+
call DrawTitleScreen ; redraw what the transfer trashed (LCD off)
154+
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCK01
155+
ldh [rLCDC], a
156+
call SGBUnfreeze
157+
.noBorder
138158
xor a
139159
ldh [rIF], a
140160
ld a, IE_VBLANK

src/sail.asm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ EnterSail::
7272
call ComputeShipPx
7373
call SailCamera
7474
call SailRedrawBody
75+
call SGBTransferBorder ; the seed is final now: send its border
76+
and a
77+
jr z, .noSGB ; not SGB: nothing was touched
78+
call SailRedrawBody ; rebuild what the transfer trashed
79+
call SGBUnfreeze
80+
.noSGB
7581
ld a, SONG_SAIL
7682
call SetSong
7783
ret

src/sgb.asm

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
; Super Game Boy border: each sea gets a day or night border, chosen by
2+
; bit 0 of the folded seed (a pure function of the seed, like the world).
3+
; Border art lives in ROMX banks 1-2 (generated by tools/png2sgb.py from
4+
; res/sgb_day.png / res/sgb_night.png) and is beamed to the SNES with the
5+
; SGB VRAM-transfer commands (pandocs: SGB VRAM Transfer, Command Border).
6+
;
7+
; Unlock requirements (cartridge header, set in main.asm): $0146 = $03 and
8+
; $014B = $33. Detection: the boot ROM leaves C = $14 only on SGB/SGB2.
9+
10+
INCLUDE "hardware.inc"
11+
INCLUDE "defs.inc"
12+
13+
SECTION "SGB WRAM", WRAM0
14+
wSGBBank: db ; ROM bank of the selected border
15+
wSGBSrc: dw ; its tile data address (map/pal follow at fixed offsets)
16+
17+
SECTION "SGB", ROM0
18+
19+
; in: hl = 16-byte packet. Bit-banged over JOYP bits 4/5: start pulse,
20+
; 128 data bits (LSB first; P14 low = 0, P15 low = 1), stop 0 bit.
21+
; Pulses >= 5 M-cycles, spaces >= 15 M-cycles, per pandocs.
22+
SGBSendPacket:
23+
xor a
24+
ldh [rJOYP], a ; start pulse: P14+P15 low
25+
nop
26+
nop
27+
nop
28+
ld a, $30
29+
ldh [rJOYP], a ; idle
30+
ld b, 16
31+
.byte
32+
ld e, [hl]
33+
inc hl
34+
ld c, 8
35+
.bit
36+
ld a, $20 ; P14 low = 0 bit
37+
rr e
38+
jr nc, .send
39+
ld a, $10 ; P15 low = 1 bit
40+
.send
41+
ldh [rJOYP], a
42+
nop
43+
nop
44+
nop
45+
ld a, $30
46+
ldh [rJOYP], a ; idle high
47+
nop
48+
nop
49+
nop
50+
nop
51+
nop
52+
dec c
53+
jr nz, .bit
54+
dec b
55+
jr nz, .byte
56+
ld a, $20 ; stop bit: 0
57+
ldh [rJOYP], a
58+
nop
59+
nop
60+
nop
61+
ld a, $30
62+
ldh [rJOYP], a
63+
ret
64+
65+
; in: b = frames to wait. LCD must be ON (polls LY).
66+
SGBWaitVBlanks:
67+
.out
68+
ldh a, [rLY]
69+
cp 144
70+
jr nc, .out
71+
.in
72+
ldh a, [rLY]
73+
cp 144
74+
jr c, .in
75+
dec b
76+
jr nz, .out
77+
ret
78+
79+
; The SGB reads VRAM-transfer data out of the video signal: the BG map must
80+
; display tiles $00-$FF in order ($00-$13 on the first row, $14-$27 next).
81+
; LCD off. clobbers a, b, c, d, e, h, l
82+
SGBFillSeqMap:
83+
ld hl, $9800
84+
xor a ; tile number (wraps mod 256)
85+
ld c, 14 ; 14 rows x 20 = 280 >= 256
86+
.row
87+
ld b, 20
88+
.col
89+
ld [hli], a
90+
inc a
91+
dec b
92+
jr nz, .col
93+
ld de, 32 - 20
94+
add hl, de
95+
dec c
96+
jr nz, .row
97+
ret
98+
99+
; in: a = ROM bank, hl = source (ROMX), de = dest (VRAM), bc = count. LCD off.
100+
SGBCopyROMtoVRAM:
101+
ld [$2000], a ; MBC5 ROM bank select
102+
.loop
103+
ld a, [hli]
104+
ld [de], a
105+
inc de
106+
dec bc
107+
ld a, b
108+
or c
109+
jr nz, .loop
110+
ret
111+
112+
; out: hl = wSGBSrc, a = wSGBBank
113+
SGBGetSrc:
114+
ld a, [wSGBSrc]
115+
ld l, a
116+
ld a, [wSGBSrc+1]
117+
ld h, a
118+
ld a, [wSGBBank]
119+
ret
120+
121+
; Beam the seed's border to the SGB. Entry: LCD on (a nice frame showing,
122+
; it gets frozen). out: a = 1 and LCD OFF (VRAM trashed, game tiles
123+
; reloaded) iff running on SGB and the border was sent; a = 0 and LCD
124+
; untouched otherwise. Caller redraws its screen, then calls SGBUnfreeze.
125+
SGBTransferBorder::
126+
ld a, [wIsSGB]
127+
cp $14 ; $14 = SGB/SGB2 (boot ROM C value)
128+
jr z, .sgb
129+
xor a
130+
ret
131+
.sgb
132+
; day or night: bit 0 of the folded seed
133+
ld a, [wSeed16+1]
134+
and 1
135+
jr nz, .night
136+
ld a, BANK(sgb_day_tiles)
137+
ld hl, sgb_day_tiles
138+
jr .sel
139+
.night
140+
ld a, BANK(sgb_night_tiles)
141+
ld hl, sgb_night_tiles
142+
.sel
143+
ld [wSGBBank], a
144+
ld a, l
145+
ld [wSGBSrc], a
146+
ld a, h
147+
ld [wSGBSrc+1], a
148+
; freeze the SGB display so the VRAM trashing stays invisible
149+
ld hl, SGB_PKT_MASK_FRZ
150+
call SGBSendPacket
151+
ld b, 4 ; >= 60 ms between packets (pandocs)
152+
call SGBWaitVBlanks
153+
; transfer setup: canonical BGP, no scroll, map shows tiles $00-$FF
154+
call WaitVBlankPoll
155+
xor a
156+
ldh [rLCDC], a
157+
ldh [rSCX], a
158+
ldh [rSCY], a
159+
ld a, $E4
160+
ldh [rBGP], a
161+
call SGBFillSeqMap
162+
; --- CHR_TRN: border tiles $00-$7F ---
163+
call SGBGetSrc
164+
ld de, $8000
165+
ld bc, 4096
166+
call SGBCopyROMtoVRAM
167+
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCK01
168+
ldh [rLCDC], a
169+
ld hl, SGB_PKT_CHR0
170+
call SGBSendPacket
171+
ld b, 6 ; transfer runs through the 5th frame
172+
call SGBWaitVBlanks
173+
; --- CHR_TRN: border tiles $80-$FF ---
174+
call WaitVBlankPoll
175+
xor a
176+
ldh [rLCDC], a
177+
call SGBGetSrc
178+
ld bc, 4096
179+
add hl, bc
180+
ld de, $8000
181+
ld bc, 4096
182+
call SGBCopyROMtoVRAM
183+
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCK01
184+
ldh [rLCDC], a
185+
ld hl, SGB_PKT_CHR1
186+
call SGBSendPacket
187+
ld b, 6
188+
call SGBWaitVBlanks
189+
; --- PCT_TRN: 32x28+1 map at $8000, palettes 4-6 at $8800 ---
190+
call WaitVBlankPoll
191+
xor a
192+
ldh [rLCDC], a
193+
call SGBGetSrc
194+
ld bc, 8192
195+
add hl, bc
196+
ld de, $8000
197+
ld bc, 1856
198+
call SGBCopyROMtoVRAM
199+
call SGBGetSrc
200+
ld bc, 8192 + 1856
201+
add hl, bc
202+
ld de, $8800
203+
ld bc, 96
204+
call SGBCopyROMtoVRAM
205+
ld a, LCDC_ON | LCDC_BG_ON | LCDC_BLOCK01
206+
ldh [rLCDC], a
207+
ld hl, SGB_PKT_PCT
208+
call SGBSendPacket
209+
ld b, 6
210+
call SGBWaitVBlanks
211+
; done: LCD off, game tiles restored. Caller redraws + unfreezes.
212+
call WaitVBlankPoll
213+
xor a
214+
ldh [rLCDC], a
215+
call LoadTiles
216+
ld a, 1
217+
ret
218+
219+
; Unfreeze the SGB display after the post-transfer redraw. No-op off SGB.
220+
SGBUnfreeze::
221+
ld a, [wIsSGB]
222+
cp $14
223+
ret nz
224+
ld hl, SGB_PKT_MASK_OFF
225+
jp SGBSendPacket
226+
227+
SECTION "SGB packets", ROM0
228+
SGB_PKT_MASK_FRZ: db $B9, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; MASK_EN freeze
229+
SGB_PKT_MASK_OFF: db $B9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; MASK_EN off
230+
SGB_PKT_CHR0: db $99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; CHR_TRN tiles $00-$7F
231+
SGB_PKT_CHR1: db $99, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; CHR_TRN tiles $80-$FF
232+
SGB_PKT_PCT: db $A1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; PCT_TRN
233+
234+
INCLUDE "sgb_day.inc"
235+
INCLUDE "sgb_night.inc"

0 commit comments

Comments
 (0)