Skip to content

Commit bb1c865

Browse files
trekawekclaude
andcommitted
Add a partial Datel Action Replay mapper
Reverse-engineered from the Action Replay Online ROM (issue #66): the ASIC exposes a register file at 0x7FE0-0x7FE7. 0x7FE0 selects the bank of the 0x0000-0x3FFF window and 0x7FE1 the bank of 0x4000-0x7FFF (banks numbered from 1; the boot code writes 1/2 for identity), with the special value 9 mapping the battery-backed cheat SRAM into the window. The software also builds its cheat/game lists in - and executes code from - the 0x7800-0x7FDF region while a ROM bank is mapped, which is modelled as a write-shadow (a byte reads back from RAM once written). 0x7FE2-0x7FE7 belong to the (unemulated) link hardware and just read back. Detection: "ROM only" type with a bank-switched size and a deliberately invalid Nintendo logo (Datel swaps the logo after the boot check on real hardware), which also distinguishes these carts from Wisdom Tree ones. This carries the software exactly as far as SameBoy gets it - the fake "GAME BOY" boot screen it draws itself - instead of the previous garbage tiles; the remaining behaviour depends on undocumented ASIC state that no emulator currently implements. Full battery green. Refs #66 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J1wLWscyUGS7CFwc3CjJR8
1 parent 13702f6 commit bb1c865

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public Cartridge(Rom rom, Battery battery) {
5555
addressSpace = new Mbc7(rom, battery);
5656
} else if (type.isPocketCamera()) {
5757
addressSpace = new PocketCamera(rom, battery);
58+
} else if (rom.getRom().length > 0x8000 && !hasValidLogo(rom)) {
59+
// Datel Action Replay / GameShark carts: "ROM only" type with a deliberately
60+
// bad Nintendo logo (the ASIC swaps it after the boot check on hardware) and
61+
// banking registers at 0x7FE0/0x7FE1 (issue #66)
62+
addressSpace = new Datel(rom, battery);
5863
} else if (rom.getRom().length > 0x8000) {
5964
// "ROM only" carts larger than 32 KB are Wisdom Tree style unlicensed
6065
// mappers with a single switchable 32 KB window (issue #61)
@@ -117,6 +122,16 @@ private static boolean isDuzMulticart(Rom rom) {
117122
return false;
118123
}
119124

125+
private static boolean hasValidLogo(Rom rom) {
126+
int[] data = rom.getRom();
127+
for (int i = 0; i < NINTENDO_LOGO.length; i++) {
128+
if (data[0x104 + i] != NINTENDO_LOGO[i]) {
129+
return false;
130+
}
131+
}
132+
return true;
133+
}
134+
120135
private static boolean isHiddenMmm01(Rom rom) {
121136
int[] data = rom.getRom();
122137
int menuBase = data.length - 0x8000;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
import eu.rekawek.coffeegb.core.memory.cart.battery.Battery;
7+
8+
import java.util.Arrays;
9+
10+
/**
11+
* The Datel Action Replay / GameShark cartridge mapper (reverse-engineered from the
12+
* Action Replay Online / Action Replay Xtreme ROMs, issue #66).
13+
*
14+
* <p>The ASIC maps two independent 16 KB windows through registers at the top of the
15+
* ROM space; banks are numbered from 1:
16+
*
17+
* <ul>
18+
* <li>{@code 0x7FE0} - bank of the 0x0000-0x3FFF window ({@code value-1});
19+
* the boot code writes 1 to keep bank 0 mapped.</li>
20+
* <li>{@code 0x7FE1} - bank of the 0x4000-0x7FFF window; values 1-8 select a ROM
21+
* bank ({@code value-1}), the special value 9 maps the battery-backed SRAM
22+
* (cheat-code storage) into the window, readable and writable.</li>
23+
* <li>{@code 0x7FE2}-{@code 0x7FE7} - control registers of the (unemulated) link
24+
* hardware; the values are stored and read back.</li>
25+
* </ul>
26+
*
27+
* <p>The register file itself wins over the mapped window content for reads of
28+
* 0x7FE0-0x7FE7 only when the SRAM is not mapped there; the software always accesses
29+
* the registers with a ROM bank in the window.
30+
*/
31+
public class Datel implements MemoryController {
32+
33+
private final int[] rom;
34+
35+
private final int romBanks;
36+
37+
// 16 KB of battery-backed SRAM, mapped into the switchable window by bank 9
38+
private final int[] ram = new int[0x4000];
39+
40+
// the ASIC shadows CPU writes to the top of the window (0x7800-0x7FDF) even while a
41+
// ROM bank is mapped: the software builds its cheat/game lists there and executes
42+
// code from the shadow. A byte reads back from the shadow only once written; the
43+
// untouched bytes keep serving the mapped ROM.
44+
private final boolean[] shadowWritten = new boolean[0x800];
45+
46+
private final Battery battery;
47+
48+
private final int[] regs = new int[8];
49+
50+
public Datel(Rom rom, Battery battery) {
51+
this.rom = rom.getRom();
52+
this.romBanks = Math.max(1, this.rom.length / 0x4000);
53+
this.battery = battery;
54+
Arrays.fill(ram, 0xff);
55+
battery.loadRam(ram);
56+
regs[0] = 1; // identity mapping at power-on
57+
regs[1] = 2;
58+
}
59+
60+
@Override
61+
public boolean accepts(int address) {
62+
return (address >= 0x0000 && address < 0x8000) || (address >= 0xa000 && address < 0xc000);
63+
}
64+
65+
private boolean isRamMapped() {
66+
return regs[1] == 9;
67+
}
68+
69+
private int lowBank() {
70+
return (Math.max(1, regs[0]) - 1) % romBanks;
71+
}
72+
73+
private int highBank() {
74+
return (Math.max(1, regs[1]) - 1) % romBanks;
75+
}
76+
77+
78+
@Override
79+
public void setByte(int address, int value) {
80+
if (address >= 0x7fe0 && address <= 0x7fe7) {
81+
regs[address - 0x7fe0] = value & 0xff;
82+
} else if (address >= 0x4000 && address < 0x8000 && isRamMapped()) {
83+
ram[address - 0x4000] = value & 0xff;
84+
} else if (address >= 0x7800 && address < 0x8000) {
85+
ram[address - 0x4000] = value & 0xff;
86+
shadowWritten[address - 0x7800] = true;
87+
}
88+
// other ROM-space writes hit mask ROM and are ignored
89+
}
90+
91+
@Override
92+
public int getByte(int address) {
93+
if (address < 0x4000) {
94+
return rom[lowBank() * 0x4000 + address];
95+
} else if (address < 0x8000) {
96+
if (address >= 0x7fe0 && address <= 0x7fe7) {
97+
return regs[address - 0x7fe0];
98+
}
99+
if (isRamMapped() || (address >= 0x7800 && shadowWritten[address - 0x7800])) {
100+
return ram[address - 0x4000];
101+
}
102+
int offset = highBank() * 0x4000 + (address - 0x4000);
103+
return offset < rom.length ? rom[offset] : 0xff;
104+
}
105+
return 0xff;
106+
}
107+
108+
@Override
109+
public void flushRam() {
110+
battery.saveRam(ram);
111+
battery.flush();
112+
}
113+
114+
@Override
115+
public Memento<MemoryController> saveToMemento() {
116+
return new DatelMemento(ram.clone(), regs.clone(), shadowWritten.clone());
117+
}
118+
119+
@Override
120+
public void restoreFromMemento(Memento<MemoryController> memento) {
121+
if (!(memento instanceof DatelMemento mem)) {
122+
throw new IllegalArgumentException("Invalid memento type");
123+
}
124+
System.arraycopy(mem.ram, 0, ram, 0, ram.length);
125+
System.arraycopy(mem.regs, 0, regs, 0, regs.length);
126+
System.arraycopy(mem.shadowWritten, 0, shadowWritten, 0, shadowWritten.length);
127+
}
128+
129+
private record DatelMemento(int[] ram, int[] regs, boolean[] shadowWritten) implements Memento<MemoryController> {
130+
}
131+
}

0 commit comments

Comments
 (0)