Skip to content

Commit cdccd15

Browse files
trekawekclaude
andcommitted
Add the Mani 32K-block multicart mapper (TETRIS SET)
The "Mani 4 in 1 - Tetris + Alleyway + Yakuman + Tennis" cart is a menu plus four unmodified 32 KB games; writing the 32 KB block number to 0x4000-0x5FFF drives the upper ROM address lines, switching the whole 0x0000-0x7FFF window. It carried an MBC3 header and got misdetected as a Duz multicart (whose sub-games are banked); the new detection keys on the sub-games' plain-ROM type bytes. All four games launch from the menu now. Fixes #74 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent 495fa69 commit cdccd15

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

core/src/main/java/eu/rekawek/coffeegb/core/memory/cart/Cartridge.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public Cartridge(Rom rom, Battery battery) {
3333
addressSpace = new Mmm01(rom, battery, true);
3434
} else if (isHiddenMmm01(rom)) {
3535
addressSpace = new Mmm01(rom, battery, false);
36+
} else if (isMani32kMulticart(rom)) {
37+
addressSpace = new Mani32kMulticart(rom);
3638
} else if (isDuzMulticart(rom)) {
3739
addressSpace = new DuzMulticart(rom, battery);
3840
} else if (sachenBase(rom) >= 0) {
@@ -105,6 +107,39 @@ public void flushBattery() {
105107
* individual games each carry their own Nintendo logo at a 16 KB boundary past bank 0.
106108
* A normal single-game cart has exactly one logo.
107109
*/
110+
/**
111+
* The Mani "TETRIS SET" style multicart (issue #74): a menu in the first 32 KB block
112+
* followed by plain 32 KB games, each with a valid logo and a plain-ROM (0x00) type
113+
* byte in its own header. The Duz multicarts also duplicate logos at 32 KB blocks,
114+
* but their sub-games are banked (non-zero type bytes).
115+
*/
116+
private static boolean isMani32kMulticart(Rom rom) {
117+
int[] data = rom.getRom();
118+
if (rom.getType() == CartridgeType.ROM || data.length < 0x10000) {
119+
return false;
120+
}
121+
int subGames = 0;
122+
for (int block = 1; block * 0x8000 + 0x150 <= data.length; block++) {
123+
int base = block * 0x8000;
124+
boolean logo = true;
125+
for (int i = 0; i < NINTENDO_LOGO.length; i++) {
126+
if (data[base + 0x104 + i] != NINTENDO_LOGO[i]) {
127+
logo = false;
128+
break;
129+
}
130+
}
131+
if (!logo) {
132+
// trailing blocks may be pad filler; games are contiguous from block 1
133+
break;
134+
}
135+
if (data[base + 0x147] != 0x00) {
136+
return false;
137+
}
138+
subGames++;
139+
}
140+
return subGames >= 2;
141+
}
142+
108143
private static boolean isDuzMulticart(Rom rom) {
109144
int[] data = rom.getRom();
110145
if (!rom.getType().isMbc3() || data.length < 0x10000) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package eu.rekawek.coffeegb.core.memory.cart.type;
2+
3+
import eu.rekawek.coffeegb.core.memento.Memento;
4+
import eu.rekawek.coffeegb.core.memory.cart.MemoryController;
5+
import eu.rekawek.coffeegb.core.memory.cart.Rom;
6+
7+
/**
8+
* The Mani "TETRIS SET" style 4-in-1 multicart (issue #74): a menu program in the first
9+
* 32 KB followed by unmodified 32 KB games, each with its own header. The menu selects a
10+
* game by writing its 32 KB block number to 0x4000-0x5FFF; the register drives the upper
11+
* ROM address lines directly, so the whole 0x0000-0x7FFF window switches to the chosen
12+
* block and the plain-ROM game runs unmapped. Power-on maps block 0 (the menu).
13+
*/
14+
public class Mani32kMulticart implements MemoryController {
15+
16+
private final int[] rom;
17+
18+
private final int blocks;
19+
20+
private int block;
21+
22+
public Mani32kMulticart(Rom rom) {
23+
this.rom = rom.getRom();
24+
this.blocks = Math.max(1, this.rom.length / 0x8000);
25+
}
26+
27+
@Override
28+
public boolean accepts(int address) {
29+
return (address >= 0x0000 && address < 0x8000) || (address >= 0xa000 && address < 0xc000);
30+
}
31+
32+
@Override
33+
public void setByte(int address, int value) {
34+
if (address >= 0x4000 && address < 0x6000) {
35+
block = value % blocks;
36+
}
37+
}
38+
39+
@Override
40+
public int getByte(int address) {
41+
if (address < 0x8000) {
42+
return rom[block * 0x8000 + address];
43+
}
44+
return 0xff;
45+
}
46+
47+
@Override
48+
public Memento<MemoryController> saveToMemento() {
49+
return new Mani32kMemento(block);
50+
}
51+
52+
@Override
53+
public void restoreFromMemento(Memento<MemoryController> memento) {
54+
if (!(memento instanceof Mani32kMemento mem)) {
55+
throw new IllegalArgumentException("Invalid memento type");
56+
}
57+
this.block = mem.block;
58+
}
59+
60+
private record Mani32kMemento(int block) implements Memento<MemoryController> {
61+
}
62+
}

0 commit comments

Comments
 (0)