/*
* HalfNES by Andrew Hoffman
* Licensed under the GNU GPL Version 3. See LICENSE file
*/
package com.grapeshot.halfnes.mappers;
import com.grapeshot.halfnes.*;
import com.grapeshot.halfnes.audio.*;
import java.util.Arrays;
/**
*
* @author Andrew
*/
public class NSFMapper extends Mapper {
//a nsf playing mapper
//TODO: add the extra bankswitches required when playing FDS
private int load, init, play, song, numSongs;
public boolean nsfBanking;
public int[] nsfStartBanks = new int[10], nsfBanks = new int[10];
private int sndchip;
boolean vrc6 = false, vrc7 = false, mmc5 = false,
n163 = false, s5b = false, hasInitSound = false, fds = false;
private boolean n163autoincrement = false;
private int n163soundAddr = 0;
private int mmc5multiplier1, mmc5multiplier2;
private int vrc7regaddr = 0;
private int s5bSoundCommand = 0;
private Namco163SoundChip n163Audio;
private VRC6SoundChip vrc6Audio;
private VRC7SoundChip vrc7Audio;
private Sunsoft5BSoundChip s5bAudio;
private MMC5SoundChip mmc5Audio;
private static final String trackstr = "Track --- / --- <-B A->";
private FDSSoundChip fdsAudio;
@Override
public void loadrom() throws BadMapperException {
loader.parseHeader();
prgsize = loader.prgsize;
mappertype = loader.mappertype;
prgoff = loader.prgoff;
for (int i = 0x70; i < 0x78; ++i) {
if (loader.header[i] != 0) {
nsfBanking = true;
nsfStartBanks[i - 0x70] = loader.header[i];
}
}
prgoff = 0;
load = loader.header[0x08] + (loader.header[0x09] << 8);
init = loader.header[0x0a] + (loader.header[0x0b] << 8);
play = loader.header[0x0c] + (loader.header[0x0d] << 8);
numSongs = loader.header[6] - 1;
song = loader.header[7] - 1;
if (loader.header[0x7a] == 1) {
//pal only tune
this.region = TVType.PAL;
System.err.println("pal only tune");
} else {
this.region = TVType.NTSC;
}
chroff = 0;
chrsize = 0;
scrolltype = MirrorType.V_MIRROR;
sndchip = loader.header[0x7B];
if (!nsfBanking && load < 0x8000) {
//no banking
System.err.println("What do I do with this???");
throw new BadMapperException("NSF with no banking loading low");
}
// pad to 4k bank size and copy in starting
//from where the load addr is in a 4k bank
//to the end of the file, padding the end to a 4k bank as well
//so total number of banks can be 2 more than # of 4k
//chunks in the file.
int paddingLen = (nsfBanking) ? load & 0x0fff : load - 0x8000;
prg = new int[1024 * 1024];
System.arraycopy(loader.load(loader.romlen(), prgoff), 0, prg, paddingLen, loader.romlen());
crc = crc32(prg);
haschrram = true;
chrsize = 8192;
chr = new int[8192];
prg_map = new int[(((sndchip & (utils.BIT2)) != 0)) ? 40 : 32];
if (!nsfBanking) {
//identity mapping from 1st loaded bank
for (int i = 0; i < 8; ++i) {
nsfStartBanks[i] = i;
}
}
//additional headache for NSFs with FDS:
if (((sndchip & (utils.BIT2)) != 0)) {
//got to copy some stuff into 6000 - 7fff just because
nsfStartBanks[8] = nsfStartBanks[6];
nsfStartBanks[9] = nsfStartBanks[7];
}
chr_map = new int[8];
for (int i = 0; i < 8; ++i) {
chr_map[i] = (1024 * i) & (chrsize - 1);
}
cpuram = new CPURAM(this);
cpu = new CPU(cpuram);
ppu = new PPU(this);
Arrays.fill(pput0, 0x00);
setmirroring(scrolltype);
//System.out.println(sndchip);
//set up the PPU to display titles
//pick a random color based on the tune's crc (why not?)
ppu.pal[0] = 0x3f;
ppu.pal[1] = 0x20 + (int) (crc % 12);
ppu.pal[2] = 0x20 + (int) (crc % 12);
ppu.pal[3] = 0x20 + (int) (crc % 12);
chr = NSFPlayerFont.font;
}
@Override
public void init() {
//now that we've set up the initial CPU state, do it all over again
//in order to match the NSF spec.
//set banks back to the way they were originally
nsfBanks = nsfStartBanks.clone();
setBanks();
//clear all ram to 0
for (int i = 0; i <= 0x7ff; ++i) {
cpuram.write(i, 0);
}
//initialize sound registers
for (int i = 0x4000; i <= 0x4013; ++i) {
cpuram.write(i, 0);
}
cpuram.write(0x4015, 0x0f);
//disable frame counter on APU
cpuram.write(0x4017, 0x40);
//simulate a jump to the play address
cpu.push(0xff);
cpu.push(0xfa);
cpu.setPC(init);
cpu.interrupt = -99999; //no interrupts for you
cpu.setRegA(song);
if (this.region == TVType.PAL) {
cpu.setRegX(0x01);
} else {
cpu.setRegX(0x00);
}
//copy titles to ppu nametable
for (int i = 0; i < 32 * 24; ++i) {
//random pattern from basic one liner
pput0[i] = (Math.random() > 0.5) ? 0x2f : 0x5c;
}
for (int i = 0; i < 96; ++i) {
pput0[i + (32 * 25)] = loader.header[i + 0xe];
}
for (int i = 0; i < trackstr.length(); ++i) {
pput0[i + (32 * 28)] = trackstr.charAt(i);
}
if (!hasInitSound) {
setSoundChip();
hasInitSound = true;
}
if (!fds) { //DON'T CLEAR THIS WHEN STUFF LOADS HERE
for (int i = 0x6000; i <= 0x7fff; ++i) {
cpuram.write(i, 0);
}
}
}
@Override
public void reset() {
song = loader.header[7] - 1;
init();
cpu.setPC(init);
}
//write into the cartridge's address space
@Override
public void cartWrite(final int addr, final int data) {
if (n163 && addr == 0xF800) {
n163autoincrement = ((data & (utils.BIT7)) != 0);
n163soundAddr = data & 0x7f;
} else if (n163 && addr == 0x4800) {
n163Audio.write(n163soundAddr, data);
if (n163autoincrement) {
n163soundAddr = ++n163soundAddr & 0x7f;
}
} else if (s5b && addr == 0xE000) {
s5bAudio.write(s5bSoundCommand, data);
} else if (s5b && addr == 0xC000) {
s5bSoundCommand = data & 0xF;
} else if (vrc6 && addr >= 0xB000 && addr <= 0xB002) {
vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
} else if (vrc6 && addr >= 0xA000 && addr <= 0xA002) {
vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
} else if (vrc7 && addr == 0x9030) {
vrc7Audio.write(vrc7regaddr, data);
} else if (vrc7 && addr == 0x9010) {
vrc7regaddr = data;
} else if (vrc6 && addr >= 0x9000 && addr <= 0x9002) {
vrc6Audio.write((addr & 0xf000) + (addr & 3), data);
} else if (fds && nsfBanking && addr >= 0x6000) {
if (addr < 0x8000) {
int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023);
prg[fuuu] = data;
} else {
int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
prg[fuuu] = data;
}
} else if (fds && !nsfBanking && addr >= 0x6000) {
if (addr < 0x8000) {
prgram[addr - 0x6000] = data;
} else {
int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
prg[fuuu] = data;
}
} else if (addr >= 0x6000 && addr < 0x8000) {
//default no-mapper operation just writes if in PRG RAM range
prgram[addr & 0x1fff] = data;
} else if ((addr >= 0x5ff8) && (addr < 0x6000)) {
nsfBanks[addr - 0x5ff8] = data;
//System.err.println(addr - 0x5ff8 + " " + data);
setBanks();
} else if (fds && nsfBanking && (addr == 0x5ff6)) {
//System.err.println("fds request bank " + data + " in ram0");
nsfBanks[8] = data;
setBanks();
} else if (fds && nsfBanking && (addr >= 0x5ff7)) {
//System.err.println("fds request bank " + data + " in ram1");
nsfBanks[9] = data;
setBanks();
} else if (mmc5 && (addr >= 0x5C00) && (addr <= 0x5FF5)) {
prgram[addr - 0x5C00] = data; //RAM emulates ExRAM here
} else if (mmc5 && (addr == 0x5206)) {
mmc5multiplier2 = data;
} else if (mmc5 && (addr == 0x5205)) {
mmc5multiplier1 = data;
} else if (mmc5 && (addr >= 0x5000) && (addr <= 0x5015)) {
mmc5Audio.write(addr - 0x5000, data);
} else if (fds && (addr >= 0x4040) && (addr <= 0x4092)) {
fdsAudio.write(addr, data);
} else {
System.err.println("write to " + utils.hex(addr) + " goes nowhere");
}
}
@Override
public int cartRead(final int addr) {
// by default has wram at 0x6000 and cartridge at 0x8000-0xfff
// but some mappers have different so override for those
if (addr >= 0x8000) {
if (addr > 0xfffa) {
//reads of last part of RAM should always
//give the reset vectors here, no matter what
//NSF bank is mapped there.
switch (addr) {
case 0xfffb:
return 0x4c;
case 0xfffc:
return 0xfb;
case 0xfffd:
return 0xff;
default:
return 0x00;
}
}
int fuuu = prg_map[((addr & 0x7fff)) >> 10] + (addr & 1023);
return prg[fuuu];
} else if (addr >= 0x6000 && hasprgram) {
if (fds && nsfBanking) {
int fuuu = prg_map[((addr - 0x6000) >> 10) + 32] + (addr & 1023);
return prg[fuuu];
} else {
return prgram[addr & 0x1fff];
}
} else if ((addr >= 0x5ff8)) {
return nsfBanks[addr - 0x5ff8];
} else if (fds && nsfBanking && (addr == 0x5ff6)) {
return nsfBanks[8];
} else if (fds && nsfBanking && (addr == 0x5ff7)) {
return nsfBanks[9];
} else if (mmc5 && addr >= 0x5C00) {
return prgram[addr - 0x5C00]; //RAM emulates ExRAM here
} else if (mmc5 && addr == 0x5206) {
return ((mmc5multiplier1 * mmc5multiplier2) >> 8) & 0xff;
} else if (mmc5 && addr == 0x5205) {
return (mmc5multiplier1 * mmc5multiplier2) & 0xff;
} else if (mmc5 && addr == 0x5015) {
return mmc5Audio.status();
} else if (n163 && addr == 0x4800) {
System.err.println("readback");
int retval = n163Audio.read(n163soundAddr);
if (n163autoincrement) {
n163soundAddr = ++n163soundAddr & 0x7f;
}
return retval;
} else if (fds && (addr >= 0x4040) && (addr < 0x4093)) {
return fdsAudio.read(addr);
}
System.err.println("reading open bus " + utils.hex(addr));
return addr >> 8; //open bus
}
@Override
public void ppuWrite(int addr, final int data) {
}
int control, prevcontrol;
int unfinishedcounter = 0;
int time = 4;
@Override
public void notifyscanline(final int scanline) {
if (scanline == 240) {
//make sure init isn't still running
if (cpu.PC != 0xFFFB) {
//if not in idle loop
if (unfinishedcounter < time) {
++unfinishedcounter;
System.err.println("Init routine hasn't returned in "
+ unfinishedcounter + " frames");
return;
} else if (unfinishedcounter == time) {
++unfinishedcounter;
System.err.println("giving up");
}
//if we've given it a few frames
//and it still hasn't returned from init, then it probably
//isn't going to (supernsf)
//so we move blithely forward.
} else {
unfinishedcounter = 0;
}
//set PPU registers to enable rendering
ppu.write(6, 0);
ppu.write(6, 0);
ppu.write(5, 0);
ppu.write(0, 0);
ppu.write(1, utils.BIT1 | utils.BIT3 | utils.BIT4);
//write track number to screen
writeTracks();
//todo: visualization effects
//read the controller
prevcontrol = control;
control = 0;
//strobe
cpuram.write(0x4016, 1);
cpuram.write(0x4016, 0);
//read each button out
for (int i = 0; i < 8; ++i) {
control = (control << 1) + (((cpuram.read(0x4016) & 3) != 0) ? 1 : 0);
}
//change song number if we get a button press
if (((control & 0x80) != 0) && ((prevcontrol & 0x80) == 0)) {
++song;
if (song > numSongs) {
song = 0;
}
//System.err.println("next song");
init();
} else if (((control & 0x40) != 0) && ((prevcontrol & 0x40) == 0)) {
--song;
if (song < 0) {
song = numSongs;
}
//System.err.println("previous song");
init();
} else //fake a jsr to the play address from wherever
//unless this is a supernsf
if (unfinishedcounter <= time) {
cpu.push((cpu.PC - 1) >> 8);
cpu.push((cpu.PC - 1) & 0xff);
cpu.setPC(play);
}
}
}
@Override
public String getrominfo() {
return "NSF INFO: \n"
+ "Filename: " + loader.name + "\n"
+ "Size: " + loader.romlen() / 1024 + " K\n"
+ "Expansion Sound: " + expSound() + "\n"
+ "Track: " + (song + 1) + " / " + (numSongs + 1) + "\n"
+ "Load Address: " + utils.hex(load) + "\n"
+ "Init Address: " + utils.hex(init) + "\n"
+ "Play Address: " + utils.hex(play) + "\n"
+ "Banking? " + (nsfBanking ? "Yes" : "No") + "\n"
+ "CRC: " + utils.hex(this.crc);
}
private String expSound() {
String chips = "";
if (vrc6) {
chips += "VRC6 ";
}
if (vrc7) {
chips += "VRC7 ";
}
if (n163) {
chips += "Namco 163 ";
}
if (mmc5) {
chips += "MMC5 ";
}
if (s5b) {
chips += "Sunsoft 5B ";
}
if (fds) {
chips += "FDS ";
}
return ((chips.length() > 0) ? chips : "None");
}
private void setBanks() {
for (int i = 0; i < prg_map.length; ++i) {
prg_map[i] = (4096 * nsfBanks[i / 4]) + (1024 * (i % 4));
if ((prg_map[i]) >= prg.length) {
//System.err.println("broken banks");
prg_map[i] %= prg.length; //probably a bad idea in general though
//but who knows what a NSF wants when it tries
//to switch a bank not in the file?
//there's no "alias it to a power of 2" for nsfs since the file
//size doesn't need to be a power of 2 and you can start loading
//the file halfway into a bank.
}
}
//utils.printarray(prg_map);
//utils.printarray(nsfStartBanks);
}
private void setSoundChip() {
if (((sndchip & (utils.BIT0)) != 0)) {
//VRC6 audio
vrc6 = true;
vrc6Audio = new VRC6SoundChip();
cpuram.apu.addExpnSound(vrc6Audio);
}
if (((sndchip & (utils.BIT1)) != 0)) {
//VRC7 audio
vrc7 = true;
vrc7Audio = new VRC7SoundChip();
cpuram.apu.addExpnSound(vrc7Audio);
}
if (((sndchip & (utils.BIT2)) != 0)) {
//FDS audio, not yet implemented
fds = true;
fdsAudio = new FDSSoundChip();
cpuram.apu.addExpnSound(fdsAudio);
}
if (((sndchip & (utils.BIT3)) != 0)) {
//MMC5 audio
mmc5 = true;
mmc5Audio = new MMC5SoundChip();
cpuram.apu.addExpnSound(mmc5Audio);
}
if (((sndchip & (utils.BIT4)) != 0)) {
//Namco 163 audio
n163 = true;
n163Audio = new Namco163SoundChip();
cpuram.apu.addExpnSound(n163Audio);
}
if (((sndchip & (utils.BIT5)) != 0)) {
//Sunsoft 5B audio
s5b = true;
s5bAudio = new Sunsoft5BSoundChip();
cpuram.apu.addExpnSound(s5bAudio);
}
}
private void writeTracks() {
String cur = String.format("%3d / %-3d", song + 1, numSongs + 1);
for (int i = 0; i < cur.length(); ++i) {
pput0[i + (32 * 28) + 6] = cur.charAt(i);
}
}
}