bsnes/icarus/heuristics/super-famicom.cpp

537 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace Heuristics {
struct SuperFamicom {
SuperFamicom(vector<uint8_t>& data, string location);
explicit operator bool() const;
auto manifest() const -> string;
auto region() const -> string;
auto revision() const -> string;
auto board() const -> string;
auto label() const -> string;
auto serial() const -> string;
auto romSize() const -> uint;
auto ramSize() const -> uint;
auto expansionRamSize() const -> uint;
auto battery() const -> bool;
private:
auto size() const -> uint { return data.size(); }
auto memory(string type, uint size, string name) const -> string;
auto scoreHeader(uint address) -> uint;
auto firmwareARM() const -> string;
auto firmwareHITACHI() const -> string;
auto firmwareNEC() const -> string;
auto firmwareNECEX() const -> string;
auto firmwareSGB() const -> string;
vector<uint8_t>& data;
string location;
uint headerAddress = 0;
};
SuperFamicom::SuperFamicom(vector<uint8_t>& data, string location) : data(data), location(location) {
if((size() & 0x7fff) == 512) {
//remove header if present
memory::move(&data[0], &data[512], size() - 512);
data.resize(size() - 512);
}
if(size() < 0x8000) return; //ignore images too small to be valid
uint scoreLo = scoreHeader( 0x7fb0);
uint scoreHi = scoreHeader( 0xffb0);
uint scoreEx = scoreHeader(0x40ffb0);
if(scoreEx) scoreEx += 4;
if(scoreLo >= scoreHi && scoreLo >= scoreEx) headerAddress = 0x7fb0;
else if(scoreHi >= scoreEx) headerAddress = 0xffb0;
else headerAddress = 0x40ffb0;
}
SuperFamicom::operator bool() const {
return headerAddress;
}
auto SuperFamicom::manifest() const -> string {
if(!operator bool()) return {};
string output;
output.append("game\n");
output.append(" sha256: ", Hash::SHA256(data).digest(), "\n");
output.append(" region: ", region(), "\n");
output.append(" revision: ", revision(), "\n");
output.append(" board: ", board(), "\n");
output.append(" name: ", Location::prefix(location), "\n");
output.append(" label: ", label(), "\n");
auto board = this->board().split("-");
if(board.left() == "ARM") {
output.append(memory("ROM", size() - 0x28000, "program.rom"));
output.append(memory("ROM", 0x20000, {firmwareARM(), ".program.rom"}));
output.append(memory("ROM", 0x8000, {firmwareARM(), ".data.rom"}));
} else if(board.left() == "HITACHI") {
output.append(memory("ROM", size() - 0xc00, "program.rom"));
output.append(memory("ROM", 0xc00, {firmwareHITACHI(), ".data.rom"}));
} else if(board.left() == "NEC") {
output.append(memory("ROM", size() - 0x2000, "program.rom"));
output.append(memory("ROM", 0x1800, {firmwareNEC(), ".program.rom"}));
output.append(memory("ROM", 0x800, {firmwareNEC(), ".data.rom"}));
} else if(board.left() == "NECEX") {
output.append(memory("ROM", size() - 0xd000, "program.rom"));
output.append(memory("ROM", 0xc000, {firmwareNECEX(), ".program.rom"}));
output.append(memory("ROM", 0x1000, {firmwareNECEX(), ".data.rom"}));
} else if(board.left() == "SGB") {
output.append(memory("ROM", size() - 0x100, "program.rom"));
output.append(memory("ROM", 0x100, {firmwareSGB(), ".boot.rom"}));
} else if(board.left() == "SPC7110") {
output.append(memory("ROM", 0x100000, "program.rom"));
output.append(memory("ROM", size() - 0x100000, "data.rom"));
} else {
output.append(memory("ROM", size(), "program.rom"));
}
if(auto size = ramSize()) {
auto type = battery() ? "NVRAM" : "RAM";
output.append(memory(type, size, "save.ram"));
}
if(auto size = expansionRamSize()) {
auto type = battery() ? "NVRAM" : "RAM";
output.append(memory(type, size, "expansion.ram"));
}
if(board.left() == "ARM") {
output.append(memory("NVRAM", 0x4000, {firmwareARM(), ".data.ram"}));
} else if(board.left() == "BS" && board(1) == "MCC") {
output.append(memory("PSRAM", 0x80000, "download.ram"));
} else if(board.left() == "HITACHI") {
output.append(memory("RAM", 0xc00, {firmwareHITACHI(), ".data.ram"}));
} else if(board.left() == "NEC") {
output.append(memory("RAM", 0x200, {firmwareNEC(), ".data.ram"}));
} else if(board.left() == "NECEX") {
output.append(memory("NVRAM", 0x1000, {firmwareNEC(), ".data.ram"}));
} else if(board.left() == "RTC") {
output.append(memory("NVRAM", 0x10, "sharp.rtc.ram"));
} else if(board.left() == "SA1") {
output.append(memory("RAM", 0x800, "internal.ram"));
} else if(board.left() == "SPC7110" && board(1) == "RTC") {
output.append(memory("NVRAM", 0x10, "epson.rtc.ram"));
}
return output;
}
auto SuperFamicom::memory(string type, uint size, string name) const -> string {
string output;
output.append(" memory\n");
output.append(" type: ", type, "\n");
output.append(" size: 0x", hex(size), "\n");
output.append(" name: ", name, "\n");
return output;
}
auto SuperFamicom::region() const -> string {
string region;
char A = data[headerAddress + 0x02]; //game type
char B = data[headerAddress + 0x03]; //game code
char C = data[headerAddress + 0x04]; //game code
char D = data[headerAddress + 0x05]; //region code (new; sometimes ambiguous)
auto E = data[headerAddress + 0x29]; //region code (old)
auto valid = [](char n) { return (n >= '0' && n <= '9') || (n >= 'A' && n <= 'Z'); };
if(data[headerAddress + 0x2a] == 0x33 && valid(A) && valid(B) & valid(C) & valid(D)) {
string code{A, B, C, D};
if(D == 'B') region = {"SNS-", code, "-BRA"};
if(D == 'C') region = {"SNSN-", code, "-ROC"};
if(D == 'D') region = {"SNSP-", code, "-NOE"};
if(D == 'E') region = {"SNS-", code, "-USA"};
if(D == 'F') region = {"SNSP-", code, "-FRA"};
if(D == 'H') region = {"SNSP-", code, "-HOL"};
if(D == 'I') region = {"SNSP-", code, "-ITA"};
if(D == 'J') region = {"SHVC-", code, "-JPN"};
if(D == 'K') region = {"SNSN-", code, "-KOR"};
if(D == 'N') region = {"SNS-", code, "-CAN"};
if(D == 'P') region = {"SNSP-", code, "-EUR"};
if(D == 'S') region = {"SNSP-", code, "-ESP"};
if(D == 'U') region = {"SNSP-", code, "-AUS"};
if(D == 'W') region = {"SNSP-", code, "-SCN"};
}
if(!region) {
if(E == 0x00) region = {"SHVC-JPN"};
if(E == 0x01) region = { "SNS-USA"};
if(E == 0x02) region = {"SNSP-EUR"};
if(E == 0x03) region = {"SNSP-SCN"};
if(E == 0x06) region = {"SNSP-FRA"};
if(E == 0x07) region = {"SNSP-HOL"};
if(E == 0x08) region = {"SNSP-ESP"};
if(E == 0x09) region = {"SNSP-NOE"};
if(E == 0x0a) region = {"SNSP-ITA"};
if(E == 0x0b) region = {"SNSP-ROC"};
if(E == 0x0d) region = {"SNSP-KOR"};
if(E == 0x0f) region = { "SNS-CAN"};
if(E == 0x10) region = { "SNS-BRA"};
if(E == 0x11) region = {"SNSP-AUS"};
}
return region ? region : "NTSC";
}
auto SuperFamicom::revision() const -> string {
string revision;
char A = data[headerAddress + 0x02]; //game type
char B = data[headerAddress + 0x03]; //game code
char C = data[headerAddress + 0x04]; //game code
char D = data[headerAddress + 0x05]; //region code (new; sometimes ambiguous)
auto E = data[headerAddress + 0x29]; //region code (old)
uint F = data[headerAddress + 0x2b]; //revision code
auto valid = [](char n) { return (n >= '0' && n <= '9') || (n >= 'A' && n <= 'Z'); };
if(data[headerAddress + 0x2a] == 0x33 && valid(A) && valid(B) & valid(C) & valid(D)) {
string code{A, B, C, D};
if(D == 'B') revision = {"SNS-", code, "-", F};
if(D == 'C') revision = {"SNSN-", code, "-", F};
if(D == 'D') revision = {"SNSP-", code, "-", F};
if(D == 'E') revision = {"SNS-", code, "-", F};
if(D == 'F') revision = {"SNSP-", code, "-", F};
if(D == 'H') revision = {"SNSP-", code, "-", F};
if(D == 'I') revision = {"SNSP-", code, "-", F};
if(D == 'J') revision = {"SHVC-", code, "-", F};
if(D == 'K') revision = {"SNSN-", code, "-", F};
if(D == 'N') revision = {"SNS-", code, "-", F};
if(D == 'P') revision = {"SNSP-", code, "-", F};
if(D == 'S') revision = {"SNSP-", code, "-", F};
if(D == 'U') revision = {"SNSP-", code, "-", F};
if(D == 'W') revision = {"SNSP-", code, "-", F};
}
if(!revision) {
if(E == 0x00) revision = {"SHVC-", F};
if(E == 0x01) revision = { "SNS-", F};
if(E == 0x02) revision = {"SNSP-", F};
if(E == 0x03) revision = {"SSWE-", F};
if(E == 0x06) revision = {"SFRA-", F};
if(E == 0x07) revision = {"SHOL-", F};
if(E == 0x08) revision = {"SESP-", F};
if(E == 0x09) revision = {"SFRG-", F};
if(E == 0x0a) revision = {"SITA-", F};
if(E == 0x0b) revision = {"SSCN-", F};
if(E == 0x0d) revision = {"SKOR-", F};
if(E == 0x0f) revision = { "SNS-", F};
if(E == 0x10) revision = {"SBRA-", F};
if(E == 0x11) revision = {"SNSP-", F};
}
return revision ? revision : string{"1.", F};
}
auto SuperFamicom::board() const -> string {
auto mapMode = data[headerAddress + 0x25];
auto cartridgeType = data[headerAddress + 0x26];
auto cartridgeTypeLo = cartridgeType & 15;
auto cartridgeTypeHi = cartridgeType >> 4;
auto cartridgeSubType = data[headerAddress + 0x0f];
string board;
string mode;
if(mapMode == 0x20 || mapMode == 0x30) mode = "LOROM-";
if(mapMode == 0x21 || mapMode == 0x31) mode = "HIROM-";
if(mapMode == 0x22 || mapMode == 0x32) mode = "SDD1-";
if(mapMode == 0x23 || mapMode == 0x33) mode = "SA1-";
if(mapMode == 0x25 || mapMode == 0x35) mode = "HIROMEX-";
if(mapMode == 0x2a || mapMode == 0x3a) mode = "SPC7110-";
//many games will store an extra title character, overwriting the map mode
if(!mode) {
if(headerAddress == 0x7fb0) mode = "LOROM-";
if(headerAddress == 0xffb0) mode = "HIROM-";
if(headerAddress == 0x40ffb0) mode = "HIROMEX-";
}
if(serial() == "A9PJ") {
//Sufami Turbo (JPN)
board.append("ST-", mode);
} else if(serial() == "ZSBJ") {
//BS-X: Sore wa Namae o Nusumareta Machi no Monogatari (JPN)
board.append("BS-MCC-");
} else if(serial() == "042J") {
//Super Game Boy 2
board.append("SGB-", mode);
} else if(serial().match("Z\?\?J")) {
board.append("BS-", mode);
} else if(cartridgeTypeLo >= 0x3) {
if(cartridgeTypeHi == 0x0) board.append("NEC-", mode);
if(cartridgeTypeHi == 0x1) board.append("SUPERFX-");
if(cartridgeTypeHi == 0x2) board.append("OBC1-", mode);
if(cartridgeTypeHi == 0x3) board.append("SA1-");
if(cartridgeTypeHi == 0x4) board.append("SDD1-");
if(cartridgeTypeHi == 0x5) board.append("RTC-", mode);
if(cartridgeTypeHi == 0xe && cartridgeTypeLo == 0x3) board.append("SGB-", mode);
if(cartridgeTypeHi == 0xf && cartridgeSubType == 0x00 && cartridgeTypeLo == 0x5) board.append("SPC7110-");
if(cartridgeTypeHi == 0xf && cartridgeSubType == 0x00 && cartridgeTypeLo == 0x9) board.append("SPC7110-RTC-");
if(cartridgeTypeHi == 0xf && cartridgeSubType == 0x01) board.append("NECEX-", mode);
if(cartridgeTypeHi == 0xf && cartridgeSubType == 0x02) board.append("ARM-", mode);
if(cartridgeTypeHi == 0xf && cartridgeSubType == 0x10) board.append("HITACHI-", mode);
}
if(!board) board.append(mode);
if(board.beginsWith("LOROM-") && romSize() > 0x200000 && ramSize()) board.replace("LOROM-", "LOROMEX-");
if(board.beginsWith("NEC-LOROM-") && romSize() > 0x100000) board.replace("NEC-LOROM-", "NEC-LOROMEX-");
if(cartridgeTypeLo == 0x1 || cartridgeTypeLo == 0x4) board.append("RAM-"); //without battery
if(cartridgeTypeLo == 0x2 || cartridgeTypeLo == 0x5) board.append("RAM-"); //with battery
if(cartridgeTypeLo == 0x6) board.append("BATTERY-"); //without RAM
board.trimRight("-", 1L);
//NEC uPD96050 frequency
if(board.beginsWith("NECEX-") && firmwareNECEX() == "st010") board.append("#11"); //11MHz (22MHz / 2)
if(board.beginsWith("NECEX-") && firmwareNECEX() == "st011") board.append("#15"); //15MHz
return board;
}
auto SuperFamicom::label() const -> string {
string label;
for(uint n = 0; n < 0x15; n++) {
auto x = data[headerAddress + 0x10 + n];
auto y = n == 0x14 ? 0 : data[headerAddress + 0x11 + n];
//null terminator (padding)
if(x == 0x00 || x == 0xff);
//ASCII
else if(x >= 0x20 && x <= 0x7e) label.append((char)x);
//Shift-JIS (half-width katakana)
else if(x == 0xa1) label.append("");
else if(x == 0xa2) label.append("");
else if(x == 0xa3) label.append("");
else if(x == 0xa4) label.append("");
else if(x == 0xa5) label.append("");
else if(x == 0xa6) label.append("");
else if(x == 0xa7) label.append("");
else if(x == 0xa8) label.append("");
else if(x == 0xa9) label.append("");
else if(x == 0xaa) label.append("");
else if(x == 0xab) label.append("");
else if(x == 0xac) label.append("");
else if(x == 0xad) label.append("");
else if(x == 0xae) label.append("");
else if(x == 0xaf) label.append("");
else if(x == 0xb0) label.append("");
else if(x == 0xb1) label.append( "");
else if(x == 0xb2) label.append( "");
else if(x == 0xb3) label.append(y == 0xde ? "" : "");
else if(x == 0xb4) label.append( "");
else if(x == 0xb5) label.append( "");
else if(x == 0xb6) label.append(y == 0xde ? "" : "");
else if(x == 0xb7) label.append(y == 0xde ? "" : "");
else if(x == 0xb8) label.append(y == 0xde ? "" : "");
else if(x == 0xb9) label.append(y == 0xde ? "" : "");
else if(x == 0xba) label.append(y == 0xde ? "" : "");
else if(x == 0xbb) label.append(y == 0xde ? "" : "");
else if(x == 0xbc) label.append(y == 0xde ? "" : "");
else if(x == 0xbd) label.append(y == 0xde ? "" : "");
else if(x == 0xbe) label.append(y == 0xde ? "" : "");
else if(x == 0xbf) label.append(y == 0xde ? "" : "");
else if(x == 0xc0) label.append(y == 0xde ? "" : "");
else if(x == 0xc1) label.append(y == 0xde ? "" : "");
else if(x == 0xc2) label.append(y == 0xde ? "" : "");
else if(x == 0xc3) label.append(y == 0xde ? "" : "");
else if(x == 0xc4) label.append(y == 0xde ? "" : "");
else if(x == 0xc5) label.append("");
else if(x == 0xc6) label.append("");
else if(x == 0xc7) label.append("");
else if(x == 0xc8) label.append("");
else if(x == 0xc9) label.append("");
else if(x == 0xca) label.append(y == 0xdf ? "" : y == 0xde ? "" : "");
else if(x == 0xcb) label.append(y == 0xdf ? "" : y == 0xde ? "" : "");
else if(x == 0xcc) label.append(y == 0xdf ? "" : y == 0xde ? "" : "");
else if(x == 0xcd) label.append(y == 0xdf ? "" : y == 0xde ? "" : "");
else if(x == 0xce) label.append(y == 0xdf ? "" : y == 0xde ? "" : "");
else if(x == 0xcf) label.append("");
else if(x == 0xd0) label.append("");
else if(x == 0xd1) label.append("");
else if(x == 0xd2) label.append("");
else if(x == 0xd3) label.append("");
else if(x == 0xd4) label.append("");
else if(x == 0xd5) label.append("");
else if(x == 0xd6) label.append("");
else if(x == 0xd7) label.append("");
else if(x == 0xd8) label.append("");
else if(x == 0xd9) label.append("");
else if(x == 0xda) label.append("");
else if(x == 0xdb) label.append("");
else if(x == 0xdc) label.append("");
else if(x == 0xdd) label.append("");
else if(x == 0xde) label.append("\xef\xbe\x9e"); //dakuten
else if(x == 0xdf) label.append("\xef\xbe\x9f"); //handakuten
//unknown
else label.append("?");
//(han)dakuten skip
if(y == 0xde && x == 0xb3) n++;
if(y == 0xde && x >= 0xb6 && x <= 0xc4) n++;
if(y == 0xde && x >= 0xca && x <= 0xce) n++;
if(y == 0xdf && x >= 0xca && y <= 0xce) n++;
}
return label.strip();
}
auto SuperFamicom::serial() const -> string {
char A = data[headerAddress + 0x02]; //game type
char B = data[headerAddress + 0x03]; //game code
char C = data[headerAddress + 0x04]; //game code
char D = data[headerAddress + 0x05]; //region code (new; sometimes ambiguous)
auto valid = [](char n) { return (n >= '0' && n <= '9') || (n >= 'A' && n <= 'Z'); };
if(data[headerAddress + 0x2a] == 0x33 && valid(A) && valid(B) & valid(C) & valid(D)) {
return {A, B, C, D};
}
return "";
}
auto SuperFamicom::romSize() const -> uint {
//subtract appended firmware size, if firmware is present
if((size() & 0x7fff) == 0x100) return size() - 0x100;
if((size() & 0x7fff) == 0xc00) return size() - 0xc00;
if((size() & 0x7fff) == 0x2000) return size() - 0x2000;
if((size() & 0xffff) == 0xd000) return size() - 0xd000;
if((size() & 0x3ffff) == 0x28000) return size() - 0x28000;
return size();
//auto romSize = data[headerAddress + 0x27] & 15;
//return 1024 << romSize;
}
auto SuperFamicom::ramSize() const -> uint {
auto ramSize = data[headerAddress + 0x28] & 7;
if(ramSize) return 1024 << ramSize;
return 0;
}
auto SuperFamicom::expansionRamSize() const -> uint {
if(data[headerAddress + 0x2a] != 0x33) return 0;
auto ramSize = data[headerAddress + 0x0d] & 7;
if(ramSize) return 1024 << ramSize;
return 0;
}
auto SuperFamicom::battery() const -> bool {
auto cartridgeTypeLo = data[headerAddress + 0x26] & 15;
return cartridgeTypeLo == 0x2 || cartridgeTypeLo == 0x5 || cartridgeTypeLo == 0x6;
}
auto SuperFamicom::scoreHeader(uint address) -> uint {
int score = 0;
if(size() < address + 0x50) return score;
uint8_t mapMode = data[address + 0x25] & ~0x10; //ignore FastROM bit
uint16_t complement = data[address + 0x2c] << 0 | data[address + 0x2d] << 8;
uint16_t checksum = data[address + 0x2e] << 0 | data[address + 0x2f] << 8;
uint16_t resetVector = data[address + 0x4c] << 0 | data[address + 0x4d] << 8;
if(resetVector < 0x8000) return score; //$00:0000-7fff is never ROM data
uint8_t opcode = data[(address & ~0x7fff) | (resetVector & 0x7fff)]; //first instruction executed
//most likely opcodes
if(opcode == 0x78 //sei
|| opcode == 0x18 //clc (clc; xce)
|| opcode == 0x38 //sec (sec; xce)
|| opcode == 0x9c //stz $nnnn (stz $4200)
|| opcode == 0x4c //jmp $nnnn
|| opcode == 0x5c //jml $nnnnnn
) score += 8;
//plausible opcodes
if(opcode == 0xc2 //rep #$nn
|| opcode == 0xe2 //sep #$nn
|| opcode == 0xad //lda $nnnn
|| opcode == 0xae //ldx $nnnn
|| opcode == 0xac //ldy $nnnn
|| opcode == 0xaf //lda $nnnnnn
|| opcode == 0xa9 //lda #$nn
|| opcode == 0xa2 //ldx #$nn
|| opcode == 0xa0 //ldy #$nn
|| opcode == 0x20 //jsr $nnnn
|| opcode == 0x22 //jsl $nnnnnn
) score += 4;
//implausible opcodes
if(opcode == 0x40 //rti
|| opcode == 0x60 //rts
|| opcode == 0x6b //rtl
|| opcode == 0xcd //cmp $nnnn
|| opcode == 0xec //cpx $nnnn
|| opcode == 0xcc //cpy $nnnn
) score -= 4;
//least likely opcodes
if(opcode == 0x00 //brk #$nn
|| opcode == 0x02 //cop #$nn
|| opcode == 0xdb //stp
|| opcode == 0x42 //wdm
|| opcode == 0xff //sbc $nnnnnn,x
) score -= 8;
if(checksum + complement == 0xffff) score += 4;
if(address == 0x7fb0 && mapMode == 0x20) score += 2;
if(address == 0xffb0 && mapMode == 0x21) score += 2;
return max(0, score);
}
auto SuperFamicom::firmwareARM() const -> string {
return "st018";
}
auto SuperFamicom::firmwareHITACHI() const -> string {
return "cx4";
}
auto SuperFamicom::firmwareNEC() const -> string {
if(label() == "PILOTWINGS") return "dsp1";
if(label() == "DUNGEON MASTER") return "dsp2";
if(label() == "SDガンダムGX") return "dsp3";
if(label() == "PLANETS CHAMP TG3000") return "dsp4";
if(label() == "TOP GEAR 3000") return "dsp4";
return "dsp1b";
}
auto SuperFamicom::firmwareNECEX() const -> string {
if(label() == "EXHAUST HEAT2") return "st010";
if(label() == "F1 ROC II") return "st010";
if(label() == "2DAN MORITA SHOUGI") return "st011";
return "st010";
}
auto SuperFamicom::firmwareSGB() const -> string {
if(label() == "Super GAMEBOY") return "sgb1";
if(label() == "Super GAMEBOY2") return "sgb2";
return "sgb1";
}
}