Skip to content

Commit ecdfd26

Browse files
bfirshclaude
andcommitted
Add support for mapper 118 (TxSROM)
Implement MMC3 variant where nametable mirroring is controlled via bit 7 of CHR bank values instead of the $A000 register, enabling single-screen and diagonal mirroring modes. Used by games like Armadillo, Pro Sport Hockey, and Goal! Two. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 6f71212 commit ecdfd26

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/mappers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Mapper66 from "./mapper66.js";
1212
import Mapper71 from "./mapper71.js";
1313
import Mapper79 from "./mapper79.js";
1414
import Mapper94 from "./mapper94.js";
15+
import Mapper118 from "./mapper118.js";
1516
import Mapper140 from "./mapper140.js";
1617
import Mapper180 from "./mapper180.js";
1718
import Mapper240 from "./mapper240.js";
@@ -32,6 +33,7 @@ export default {
3233
71: Mapper71,
3334
79: Mapper79,
3435
94: Mapper94,
36+
118: Mapper118,
3537
140: Mapper140,
3638
180: Mapper180,
3739
240: Mapper240,

src/mappers/mapper118.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Mapper4 from "./mapper4.js";
2+
3+
// TxSROM - MMC3 variant with CHR-controlled nametable mirroring
4+
// Used by games like Armadillo, Pro Sport Hockey, Goal! Two.
5+
// Identical to standard MMC3 except: the $A000 mirroring register is bypassed,
6+
// and bit 7 of CHR bank register values controls CIRAM A10 (nametable page select)
7+
// instead of being used for CHR addressing. This enables single-screen and
8+
// diagonal mirroring modes that standard MMC3 cannot produce.
9+
// See https://www.nesdev.org/wiki/INES_Mapper_118
10+
class Mapper118 extends Mapper4 {
11+
static mapperName = "TxSROM";
12+
13+
constructor(nes) {
14+
super(nes);
15+
// Raw CHR register values (R0-R5) — bit 7 is used for nametable control
16+
this.chrRegs = [0, 0, 0, 0, 0, 0];
17+
}
18+
19+
write(address, value) {
20+
if (address === 0xa000) {
21+
// The standard MMC3 mirroring register is bypassed on TxSROM.
22+
// Nametable mirroring is instead controlled by bit 7 of CHR bank values.
23+
return;
24+
}
25+
super.write(address, value);
26+
if (address === 0x8000) {
27+
// chrAddressSelect may have changed, which affects which CHR registers
28+
// control which nametables
29+
this.updateNametableMirroring();
30+
}
31+
}
32+
33+
executeCommand(cmd, arg) {
34+
if (cmd <= 5) {
35+
// CHR bank command: store the raw value, then mask bit 7 before passing
36+
// to the parent for CHR banking (bit 7 goes to CIRAM A10, not CHR A17)
37+
this.chrRegs[cmd] = arg;
38+
super.executeCommand(cmd, arg & 0x7f);
39+
this.updateNametableMirroring();
40+
} else {
41+
// PRG bank commands pass through unchanged
42+
super.executeCommand(cmd, arg);
43+
}
44+
}
45+
46+
// Update nametable mirroring based on bit 7 of CHR register values.
47+
// The MMC3's CHR banking ignores A13, so pattern table addresses ($0xxx)
48+
// and nametable addresses ($2xxx) use the same bank selection. CHR A17
49+
// (bit 7) is wired to CIRAM A10 on TxSROM boards.
50+
//
51+
// When chrAddressSelect=0: R0/R1 (2KB banks) are at $0000-$0FFF, so they
52+
// control nametables: R0 bit 7 → NT0+NT1, R1 bit 7 → NT2+NT3
53+
// When chrAddressSelect=1: R2-R5 (1KB banks) are at $0000-$0FFF, so they
54+
// control individual nametables: R2→NT0, R3→NT1, R4→NT2, R5→NT3
55+
updateNametableMirroring() {
56+
let ppu = this.nes.ppu;
57+
58+
if (this.chrAddressSelect === 0) {
59+
let nt01 = (this.chrRegs[0] >> 7) & 1;
60+
let nt23 = (this.chrRegs[1] >> 7) & 1;
61+
ppu.ntable1[0] = nt01;
62+
ppu.ntable1[1] = nt01;
63+
ppu.ntable1[2] = nt23;
64+
ppu.ntable1[3] = nt23;
65+
} else {
66+
ppu.ntable1[0] = (this.chrRegs[2] >> 7) & 1;
67+
ppu.ntable1[1] = (this.chrRegs[3] >> 7) & 1;
68+
ppu.ntable1[2] = (this.chrRegs[4] >> 7) & 1;
69+
ppu.ntable1[3] = (this.chrRegs[5] >> 7) & 1;
70+
}
71+
72+
// Update VRAM mirror table to match ntable1 settings
73+
for (let i = 0; i < 4; i++) {
74+
let source = 0x2000 + i * 0x400;
75+
let target = 0x2000 + ppu.ntable1[i] * 0x400;
76+
ppu.defineMirrorRegion(source, target, 0x400);
77+
}
78+
79+
// Invalidate the PPU's mirroring cache so setMirroring() won't skip
80+
// updates if called later
81+
ppu.currentMirroring = -1;
82+
}
83+
84+
loadROM() {
85+
super.loadROM();
86+
this.updateNametableMirroring();
87+
}
88+
89+
toJSON() {
90+
let s = super.toJSON();
91+
s.chrRegs = this.chrRegs.slice();
92+
return s;
93+
}
94+
95+
fromJSON(s) {
96+
super.fromJSON(s);
97+
this.chrRegs = s.chrRegs;
98+
this.updateNametableMirroring();
99+
}
100+
}
101+
102+
export default Mapper118;

0 commit comments

Comments
 (0)