diff --git a/.gitignore b/.gitignore index 1a533585..ffa62a02 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ ananke/libananke.so +icarus/icarus diff --git a/icarus/GNUmakefile b/icarus/GNUmakefile new file mode 100644 index 00000000..bfb434f3 --- /dev/null +++ b/icarus/GNUmakefile @@ -0,0 +1,29 @@ +include ../nall/GNUmakefile +include ../hiro/GNUmakefile + +flags += -O3 -I.. +link += +objects := obj/hiro.o obj/icarus.o +objects += $(if $(call streq,$(platform),windows),obj/resource.o) + +all: $(objects) + $(compiler) -o icarus $(objects) $(link) $(hirolink) + +obj/hiro.o: ../hiro/hiro.cpp + $(compiler) $(hiroflags) -o obj/hiro.o -c ../hiro/hiro.cpp + +obj/icarus.o: icarus.cpp $(call rwildcard,core/) $(call rwildcard,heuristics/) $(call rwildcard,ui/) + $(compiler) $(cppflags) $(flags) -o obj/icarus.o -c icarus.cpp + +obj/resource.o: + windres ../hiro/windows/hiro.rc obj/resource.o + +clean: + if [ -f ./icarus ]; then rm ./icarus; fi + $(call delete,obj/*.o) + +install: + if [ -f ./icarus ]; then cp ./icarus $(prefix)/bin/icarus; fi + +uninstall: + if [ -f $(prefix)/bin/icarus ]; then rm $(prefix)/bin/icarus; fi diff --git a/icarus/core/bsx-satellaview.cpp b/icarus/core/bsx-satellaview.cpp new file mode 100644 index 00000000..b1a3346d --- /dev/null +++ b/icarus/core/bsx-satellaview.cpp @@ -0,0 +1,40 @@ +auto Icarus::bsxSatellaviewManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return bsxSatellaviewManifest(buffer, location); +} + +auto Icarus::bsxSatellaviewManifest(vector& buffer, const string& location) -> string { + BsxSatellaviewCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::bsxSatellaviewImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "BS-X Satellaview/", name, ".bs/"}; +//if(directory::exists(target)) return failure("game already exists"); + + BsxSatellaviewCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "program.rom"}, buffer); + return success(); +} diff --git a/icarus/core/core.cpp b/icarus/core/core.cpp new file mode 100644 index 00000000..93910769 --- /dev/null +++ b/icarus/core/core.cpp @@ -0,0 +1,70 @@ +auto Icarus::error() const -> string { + return errorMessage; +} + +auto Icarus::success() -> bool { + errorMessage = ""; + return true; +} + +auto Icarus::failure(const string& message) -> bool { + errorMessage = message; + return false; +} + +auto Icarus::manifest(string location) -> string { + location.transform("\\", "/").rtrim("/").append("/"); + if(!directory::exists(location)) return ""; + + auto type = location.suffixname().downcase(); + if(type == ".fc") return famicomManifest(location); + if(type == ".sfc") return superFamicomManifest(location); + if(type == ".gb") return gameBoyManifest(location); + if(type == ".gbc") return gameBoyColorManifest(location); + if(type == ".gba") return gameBoyAdvanceManifest(location); + if(type == ".bs") return bsxSatellaviewManifest(location); + if(type == ".st") return sufamiTurboManifest(location); + + return ""; +} + +auto Icarus::import(string location) -> bool { + location.transform("\\", "/").rtrim("/"); + if(!file::exists(location)) return failure("file does not exist"); + if(!file::readable(location)) return failure("file is unreadable"); + + auto name = location.prefixname(); + auto type = location.suffixname().downcase(); + if(!name || !type) return failure("invalid file name"); + + auto buffer = file::read(location); + if(!buffer) return failure("file is empty"); + + if(type == ".zip") { + Decode::ZIP zip; + if(!zip.open(location)) return failure("ZIP archive is invalid"); + if(!zip.file) return failure("ZIP archive is empty"); + + name = zip.file[0].name.prefixname(); + type = zip.file[0].name.suffixname().downcase(); + buffer = zip.extract(zip.file[0]); + } + + if(type == ".fc" || type == ".nes") return famicomImport(buffer, location); + if(type == ".sfc" || type == ".smc") return superFamicomImport(buffer, location); + if(type == ".gb") return gameBoyImport(buffer, location); + if(type == ".gbc") return gameBoyColorImport(buffer, location); + if(type == ".gba") return gameBoyAdvanceImport(buffer, location); + if(type == ".bs") return bsxSatellaviewImport(buffer, location); + if(type == ".st") return sufamiTurboImport(buffer, location); + + return failure("unrecognized file extension"); +} + +auto Icarus::concatenate(vector& output, const string& location) -> void { + if(auto input = file::read(location)) { + auto size = output.size(); + output.resize(size + input.size()); + memory::copy(output.data() + size, input.data(), input.size()); + } +} diff --git a/icarus/core/core.hpp b/icarus/core/core.hpp new file mode 100644 index 00000000..66832ce9 --- /dev/null +++ b/icarus/core/core.hpp @@ -0,0 +1,50 @@ +struct Icarus { + //core.cpp + auto error() const -> string; + auto success() -> bool; + auto failure(const string& message) -> bool; + + auto manifest(string location) -> string; + auto import(string location) -> bool; + + auto concatenate(vector& output, const string& location) -> void; + + //famicom.cpp + auto famicomManifest(const string& location) -> string; + auto famicomManifest(vector& buffer, const string& location) -> string; + auto famicomImport(vector& buffer, const string& location) -> bool; + + //super-famicom.cpp + auto superFamicomManifest(const string& location) -> string; + auto superFamicomManifest(vector& buffer, const string& location) -> string; + auto superFamicomImport(vector& buffer, const string& location) -> bool; + auto superFamicomImportScanManifest(vector& roms, Markup::Node node) -> void; + + //game-boy.cpp + auto gameBoyManifest(const string& location) -> string; + auto gameBoyManifest(vector& buffer, const string& location) -> string; + auto gameBoyImport(vector& buffer, const string& location) -> bool; + + //game-boy-color.cpp + auto gameBoyColorManifest(const string& location) -> string; + auto gameBoyColorManifest(vector& buffer, const string& location) -> string; + auto gameBoyColorImport(vector& buffer, const string& location) -> bool; + + //game-boy-advance.cpp + auto gameBoyAdvanceManifest(const string& location) -> string; + auto gameBoyAdvanceManifest(vector& buffer, const string& location) -> string; + auto gameBoyAdvanceImport(vector& buffer, const string& location) -> bool; + + //bsx-satellaview.cpp + auto bsxSatellaviewManifest(const string& location) -> string; + auto bsxSatellaviewManifest(vector& buffer, const string& location) -> string; + auto bsxSatellaviewImport(vector& buffer, const string& location) -> bool; + + //sufami-turbo.cpp + auto sufamiTurboManifest(const string& location) -> string; + auto sufamiTurboManifest(vector& buffer, const string& location) -> string; + auto sufamiTurboImport(vector& buffer, const string& location) -> bool; + +private: + string errorMessage; +}; diff --git a/icarus/core/famicom.cpp b/icarus/core/famicom.cpp new file mode 100644 index 00000000..8b820fac --- /dev/null +++ b/icarus/core/famicom.cpp @@ -0,0 +1,45 @@ +auto Icarus::famicomManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "ines.rom"}); + concatenate(buffer, {location, "program.rom"}); + concatenate(buffer, {location, "character.rom"}); + return famicomManifest(buffer, location); +} + +auto Icarus::famicomManifest(vector& buffer, const string& location) -> string { + FamicomCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::famicomImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Famicom/", name, ".fc/"}; +//if(directory::exists(target)) return failure("game already exists"); + + FamicomCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "ines.rom"}, buffer.data(), 16); + file::write({target, "program.rom"}, buffer.data() + 16, cartridge.prgrom); + if(!cartridge.chrrom) return success(); + file::write({target, "character.rom"}, buffer.data() + 16 + cartridge.prgrom, cartridge.chrrom); + return success(); +} diff --git a/icarus/core/game-boy-advance.cpp b/icarus/core/game-boy-advance.cpp new file mode 100644 index 00000000..2d018e0e --- /dev/null +++ b/icarus/core/game-boy-advance.cpp @@ -0,0 +1,40 @@ +auto Icarus::gameBoyAdvanceManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return gameBoyAdvanceManifest(buffer, location); +} + +auto Icarus::gameBoyAdvanceManifest(vector& buffer, const string& location) -> string { + GameBoyAdvanceCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::gameBoyAdvanceImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Game Boy Advance/", name, ".gba/"}; +//if(directory::exists(target)) return failure("game already exists"); + + GameBoyAdvanceCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "program.rom"}, buffer); + return success(); +} diff --git a/icarus/core/game-boy-color.cpp b/icarus/core/game-boy-color.cpp new file mode 100644 index 00000000..d64a8da2 --- /dev/null +++ b/icarus/core/game-boy-color.cpp @@ -0,0 +1,40 @@ +auto Icarus::gameBoyColorManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return gameBoyColorManifest(buffer, location); +} + +auto Icarus::gameBoyColorManifest(vector& buffer, const string& location) -> string { + GameBoyCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::gameBoyColorImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Game Boy Color/", name, ".gbc/"}; +//if(directory::exists(target)) return failure("game already exists"); + + GameBoyCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "program.rom"}, buffer); + return success(); +} diff --git a/icarus/core/game-boy.cpp b/icarus/core/game-boy.cpp new file mode 100644 index 00000000..74240498 --- /dev/null +++ b/icarus/core/game-boy.cpp @@ -0,0 +1,40 @@ +auto Icarus::gameBoyManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return gameBoyManifest(buffer, location); +} + +auto Icarus::gameBoyManifest(vector& buffer, const string& location) -> string { + GameBoyCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::gameBoyImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Game Boy/", name, ".gb/"}; +//if(directory::exists(target)) return failure("game already exists"); + + GameBoyCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "program.rom"}, buffer); + return success(); +} diff --git a/icarus/core/sufami-turbo.cpp b/icarus/core/sufami-turbo.cpp new file mode 100644 index 00000000..0c88b39e --- /dev/null +++ b/icarus/core/sufami-turbo.cpp @@ -0,0 +1,40 @@ +auto Icarus::sufamiTurboManifest(const string& location) -> string { + vector buffer; + concatenate(buffer, {location, "program.rom"}); + return sufamiTurboManifest(buffer, location); +} + +auto Icarus::sufamiTurboManifest(vector& buffer, const string& location) -> string { + SufamiTurboCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::sufamiTurboImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Sufami Turbo/", name, ".st/"}; +//if(directory::exists(target)) return failure("game already exists"); + + SufamiTurboCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + file::write({target, "program.rom"}, buffer); + return success(); +} diff --git a/icarus/core/super-famicom.cpp b/icarus/core/super-famicom.cpp new file mode 100644 index 00000000..a1986d8b --- /dev/null +++ b/icarus/core/super-famicom.cpp @@ -0,0 +1,72 @@ +auto Icarus::superFamicomManifest(const string& location) -> string { + vector buffer; + auto files = directory::files(location, "*.rom"); + concatenate(buffer, {location, "program.rom"}); + concatenate(buffer, {location, "data.rom" }); + for(auto& file : files.match("*.boot.rom" )) concatenate(buffer, {location, file}); + for(auto& file : files.match("*.program.rom")) concatenate(buffer, {location, file}); + for(auto& file : files.match("*.data.rom" )) concatenate(buffer, {location, file}); + return superFamicomManifest(buffer, location); +} + +auto Icarus::superFamicomManifest(vector& buffer, const string& location) -> string { + SuperFamicomCartridge cartridge{buffer.data(), buffer.size()}; + if(auto markup = cartridge.markup) { + markup.append("\n"); + markup.append("information\n"); + markup.append(" sha256: ", Hash::SHA256(buffer.data(), buffer.size()).digest(), "\n"); + markup.append(" title: ", location.prefixname(), "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + return markup; + } + return ""; +} + +auto Icarus::superFamicomImport(vector& buffer, const string& location) -> bool { + auto name = location.prefixname(); + auto source = location.pathname(); + string target{settings.libraryPath, "Super Famicom/", name, ".sfc/"}; +//if(directory::exists(target)) return failure("game already exists"); + + SuperFamicomCartridge cartridge{buffer.data(), buffer.size()}; + auto markup = cartridge.markup; + if(!markup) return failure("does not appear to be a valid image"); + + vector roms; + auto document = BML::unserialize(markup); + superFamicomImportScanManifest(roms, document); + + for(auto rom : roms) { + auto name = rom["name"].text(); + auto size = rom["size"].decimal(); + if(name == "program.rom" || name == "data.rom" || cartridge.firmware_appended) continue; + if(file::size({source, name}) != size) return failure({"firmware (", name, ") missing or invalid"}); + } + + markup.append("\n"); + markup.append("information\n"); + markup.append(" title: ", name, "\n"); + markup.append(" note: ", "heuristically generated by icarus\n"); + + if(!directory::create(target)) return failure("library path unwritable"); + + if(settings.createManifests) file::write({target, "manifest.bml"}, markup); + unsigned offset = (buffer.size() & 0x7fff) == 512 ? 512 : 0; //skip header if present + for(auto rom : roms) { + auto name = rom["name"].text(); + auto size = rom["size"].decimal(); + if(name == "program.rom" || name == "data.rom" || cartridge.firmware_appended) { + file::write({target, name}, buffer.data() + offset, size); + offset += size; + } else { + auto firmware = file::read({source, name}); + file::write({target, name}, firmware); + } + } + return success(); +} + +auto Icarus::superFamicomImportScanManifest(vector& roms, Markup::Node node) -> void { + if(node.name() == "rom") roms.append(node); + for(auto leaf : node) superFamicomImportScanManifest(roms, leaf); +} diff --git a/icarus/heuristics/bsx-satellaview.hpp b/icarus/heuristics/bsx-satellaview.hpp new file mode 100644 index 00000000..7d36f0cf --- /dev/null +++ b/icarus/heuristics/bsx-satellaview.hpp @@ -0,0 +1,10 @@ +struct BsxSatellaviewCartridge { + BsxSatellaviewCartridge(const uint8_t* data, unsigned size); + + string markup; +}; + +BsxSatellaviewCartridge::BsxSatellaviewCartridge(const uint8_t* data, unsigned size) { + markup.append("cartridge\n"); + markup.append(" rom name=program.rom size=0x", hex(size), " type=FlashROM\n"); +} diff --git a/icarus/heuristics/famicom.hpp b/icarus/heuristics/famicom.hpp new file mode 100644 index 00000000..5b71b5df --- /dev/null +++ b/icarus/heuristics/famicom.hpp @@ -0,0 +1,161 @@ +struct FamicomCartridge { + FamicomCartridge(const uint8_t* data, unsigned size); + + string markup; + +//private: + unsigned mapper; + unsigned mirror; + unsigned prgrom; + unsigned prgram; + unsigned chrrom; + unsigned chrram; +}; + +FamicomCartridge::FamicomCartridge(const uint8_t* data, unsigned size) { + if(size < 16) return; + if(data[0] != 'N') return; + if(data[1] != 'E') return; + if(data[2] != 'S') return; + if(data[3] != 26) return; + + mapper = ((data[7] >> 4) << 4) | (data[6] >> 4); + mirror = ((data[6] & 0x08) >> 2) | (data[6] & 0x01); + prgrom = data[4] * 0x4000; + chrrom = data[5] * 0x2000; + prgram = 0u; + chrram = chrrom == 0u ? 8192u : 0u; + + markup.append("cartridge\n"); + + switch(mapper) { + default: + markup.append(" board type=NES-NROM-256\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + break; + + case 1: + markup.append(" board type=NES-SXROM\n"); + markup.append(" chip type=MMC1B2\n"); + prgram = 8192; + break; + + case 2: + markup.append(" board type=NES-UOROM\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + break; + + case 3: + markup.append(" board type=NES-CNROM\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + break; + + case 4: + //MMC3 + markup.append(" board type=NES-TLROM\n"); + markup.append(" chip type=MMC3B\n"); + prgram = 8192; + //MMC6 + //markup.append(" board type=NES-HKROM\n"); + //markup.append(" chip type=MMC6n"); + //prgram = 1024; + break; + + case 5: + markup.append(" board type=NES-ELROM\n"); + markup.append(" chip type=MMC5\n"); + prgram = 65536; + break; + + case 7: + markup.append(" board type=NES-AOROM\n"); + break; + + case 9: + markup.append(" board type=NES-PNROM\n"); + markup.append(" chip type=MMC2\n"); + prgram = 8192; + break; + + case 10: + markup.append(" board type=NES-FKROM\n"); + markup.append(" chip type=MMC4\n"); + prgram = 8192; + break; + + case 16: + markup.append(" board type=BANDAI-FCG\n"); + markup.append(" chip type=LZ93D50\n"); + break; + + case 21: + case 23: + case 25: + //VRC4 + markup.append(" board type=KONAMI-VRC-4\n"); + markup.append(" chip type=VRC4\n"); + markup.append(" pinout a0=1 a1=0\n"); + prgram = 8192; + break; + + case 22: + //VRC2 + markup.append(" board type=KONAMI-VRC-2\n"); + markup.append(" chip type=VRC2\n"); + markup.append(" pinout a0=0 a1=1\n"); + break; + + case 24: + markup.append(" board type=KONAMI-VRC-6\n"); + markup.append(" chip type=VRC6\n"); + break; + + case 26: + markup.append(" board type=KONAMI-VRC-6\n"); + markup.append(" chip type=VRC6\n"); + prgram = 8192; + break; + + case 34: + markup.append(" board type=NES-BNROM\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + break; + + case 66: + markup.append(" board type=NES-GNROM\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + break; + + case 69: + markup.append(" board type=SUNSOFT-5B\n"); + markup.append(" chip type=5B\n"); + prgram = 8192; + break; + + case 73: + markup.append(" board type=KONAMI-VRC-3\n"); + markup.append(" chip type=VRC3\n"); + markup.append(" mirror mode=", mirror == 0 ? "horizontal" : "vertical", "\n"); + prgram = 8192; + break; + + case 75: + markup.append(" board type=KONAMI-VRC-1\n"); + markup.append(" chip type=VRC1\n"); + break; + + case 85: + markup.append(" board type=KONAMI-VRC-7\n"); + markup.append(" chip type=VRC7\n"); + prgram = 8192; + break; + } + + markup.append(" prg\n"); + if(prgrom) markup.append(" rom name=program.rom size=0x", hex(prgrom), "\n"); + if(prgram) markup.append(" ram name=save.ram size=0x", hex(prgram), "\n"); + + markup.append(" chr\n"); + if(chrrom) markup.append(" rom name=character.rom size=0x", hex(chrrom), "\n"); + if(chrram) markup.append(" ram size=0x", hex(chrram), "\n"); +} diff --git a/icarus/heuristics/game-boy-advance.hpp b/icarus/heuristics/game-boy-advance.hpp new file mode 100644 index 00000000..9f1f3175 --- /dev/null +++ b/icarus/heuristics/game-boy-advance.hpp @@ -0,0 +1,50 @@ +struct GameBoyAdvanceCartridge { + GameBoyAdvanceCartridge(const uint8_t *data, unsigned size); + + string markup; + string identifiers; +}; + +GameBoyAdvanceCartridge::GameBoyAdvanceCartridge(const uint8_t *data, unsigned size) { + struct Identifier { + string name; + unsigned size; + }; + vector idlist; + idlist.append({"SRAM_V", 6}); + idlist.append({"SRAM_F_V", 8}); + idlist.append({"EEPROM_V", 8}); + idlist.append({"FLASH_V", 7}); + idlist.append({"FLASH512_V", 10}); + idlist.append({"FLASH1M_V", 9}); + + lstring list; + for(auto& id : idlist) { + for(signed n = 0; n < size - 16; n++) { + if(!memcmp(data + n, (const char*)id.name, id.size)) { + const char* p = (const char*)data + n + id.size; + if(p[0] >= '0' && p[0] <= '9' + && p[1] >= '0' && p[1] <= '9' + && p[2] >= '0' && p[2] <= '9' + ) { + char text[16]; + memcpy(text, data + n, id.size + 3); + text[id.size + 3] = 0; + list.appendOnce(text); + } + } + } + } + identifiers = list.merge(","); + + markup.append("cartridge\n"); + markup.append(" rom name=program.rom size=0x", hex(size), "\n"); + if(0); + else if(identifiers.beginsWith("SRAM_V" )) markup.append(" ram name=save.ram type=SRAM size=0x8000\n"); + else if(identifiers.beginsWith("SRAM_F_V" )) markup.append(" ram name=save.ram type=FRAM size=0x8000\n"); + else if(identifiers.beginsWith("EEPROM_V" )) markup.append(" ram name=save.ram type=EEPROM size=0x0\n"); + else if(identifiers.beginsWith("FLASH_V" )) markup.append(" ram name=save.ram type=FlashROM size=0x10000\n"); + else if(identifiers.beginsWith("FLASH512_V")) markup.append(" ram name=save.ram type=FlashROM size=0x10000\n"); + else if(identifiers.beginsWith("FLASH1M_V" )) markup.append(" ram name=save.ram type=FlashROM size=0x20000\n"); +//if(identifiers.empty() == false) markup.append(" #detected: ", identifiers, "\n"); +} diff --git a/icarus/heuristics/game-boy.hpp b/icarus/heuristics/game-boy.hpp new file mode 100644 index 00000000..ebe4da58 --- /dev/null +++ b/icarus/heuristics/game-boy.hpp @@ -0,0 +1,107 @@ +struct GameBoyCartridge { + GameBoyCartridge(uint8_t* data, unsigned size); + + string markup; + +//private: + struct Information { + string mapper; + bool ram; + bool battery; + bool rtc; + bool rumble; + + unsigned romsize; + unsigned ramsize; + + bool cgb; + bool cgbonly; + } info; +}; + +GameBoyCartridge::GameBoyCartridge(uint8_t *romdata, unsigned romsize) { + if(romsize < 0x4000) return; + + info.mapper = "unknown"; + info.ram = false; + info.battery = false; + info.rtc = false; + info.rumble = false; + + info.romsize = 0; + info.ramsize = 0; + + unsigned base = romsize - 0x8000; + if(romdata[base + 0x0104] == 0xce && romdata[base + 0x0105] == 0xed + && romdata[base + 0x0106] == 0x66 && romdata[base + 0x0107] == 0x66 + && romdata[base + 0x0108] == 0xcc && romdata[base + 0x0109] == 0x0d + && romdata[base + 0x0147] >= 0x0b && romdata[base + 0x0147] <= 0x0d + ) { + //MMM01 stores header at bottom of image + //flip this around for consistency with all other mappers + uint8_t header[0x8000]; + memcpy(header, romdata + base, 0x8000); + memmove(romdata + 0x8000, romdata, romsize - 0x8000); + memcpy(romdata, header, 0x8000); + } + + info.cgb = (romdata[0x0143] & 0x80) == 0x80; + info.cgbonly = (romdata[0x0143] & 0xc0) == 0xc0; + + switch(romdata[0x0147]) { + case 0x00: info.mapper = "none"; break; + case 0x01: info.mapper = "MBC1"; break; + case 0x02: info.mapper = "MBC1"; info.ram = true; break; + case 0x03: info.mapper = "MBC1"; info.ram = true; info.battery = true; break; + case 0x05: info.mapper = "MBC2"; info.ram = true; break; + case 0x06: info.mapper = "MBC2"; info.ram = true; info.battery = true; break; + case 0x08: info.mapper = "none"; info.ram = true; break; + case 0x09: info.mapper = "MBC0"; info.ram = true; info.battery = true; break; + case 0x0b: info.mapper = "MMM01"; break; + case 0x0c: info.mapper = "MMM01"; info.ram = true; break; + case 0x0d: info.mapper = "MMM01"; info.ram = true; info.battery = true; break; + case 0x0f: info.mapper = "MBC3"; info.rtc = true; info.battery = true; break; + case 0x10: info.mapper = "MBC3"; info.rtc = true; info.ram = true; info.battery = true; break; + case 0x11: info.mapper = "MBC3"; break; + case 0x12: info.mapper = "MBC3"; info.ram = true; break; + case 0x13: info.mapper = "MBC3"; info.ram = true; info.battery = true; break; + case 0x19: info.mapper = "MBC5"; break; + case 0x1a: info.mapper = "MBC5"; info.ram = true; break; + case 0x1b: info.mapper = "MBC5"; info.ram = true; info.battery = true; break; + case 0x1c: info.mapper = "MBC5"; info.rumble = true; break; + case 0x1d: info.mapper = "MBC5"; info.rumble = true; info.ram = true; break; + case 0x1e: info.mapper = "MBC5"; info.rumble = true; info.ram = true; info.battery = true; break; + case 0xfc: break; //Pocket Camera + case 0xfd: break; //Bandai TAMA5 + case 0xfe: info.mapper = "HuC3"; break; + case 0xff: info.mapper = "HuC1"; info.ram = true; info.battery = true; break; + } + + switch(romdata[0x0148]) { default: + case 0x00: info.romsize = 2 * 16 * 1024; break; + case 0x01: info.romsize = 4 * 16 * 1024; break; + case 0x02: info.romsize = 8 * 16 * 1024; break; + case 0x03: info.romsize = 16 * 16 * 1024; break; + case 0x04: info.romsize = 32 * 16 * 1024; break; + case 0x05: info.romsize = 64 * 16 * 1024; break; + case 0x06: info.romsize = 128 * 16 * 1024; break; + case 0x07: info.romsize = 256 * 16 * 1024; break; + case 0x52: info.romsize = 72 * 16 * 1024; break; + case 0x53: info.romsize = 80 * 16 * 1024; break; + case 0x54: info.romsize = 96 * 16 * 1024; break; + } + + switch(romdata[0x0149]) { default: + case 0x00: info.ramsize = 0 * 1024; break; + case 0x01: info.ramsize = 2 * 1024; break; + case 0x02: info.ramsize = 8 * 1024; break; + case 0x03: info.ramsize = 32 * 1024; break; + } + + if(info.mapper == "MBC2") info.ramsize = 512; //512 x 4-bit + + markup.append("cartridge\n"); + markup.append(" board type=", info.mapper, "\n"); + markup.append(" rom name=program.rom size=0x", hex(romsize), "\n"); + if(info.ramsize > 0) markup.append(" ram name=save.ram size=0x", hex(info.ramsize), "\n"); +} diff --git a/icarus/heuristics/sufami-turbo.hpp b/icarus/heuristics/sufami-turbo.hpp new file mode 100644 index 00000000..71c293ea --- /dev/null +++ b/icarus/heuristics/sufami-turbo.hpp @@ -0,0 +1,18 @@ +struct SufamiTurboCartridge { + SufamiTurboCartridge(const uint8_t* data, unsigned size); + + string markup; +}; + +SufamiTurboCartridge::SufamiTurboCartridge(const uint8_t* data, unsigned size) { + if(size < 0x20000) return; //too small to be a valid game? + if(memcmp(data, "BANDAI SFC-ADX", 14)) return; //missing required header? + unsigned romsize = data[0x36] * 0x20000; //128KB + unsigned ramsize = data[0x37] * 0x800; //2KB + bool linkable = data[0x35] != 0x00; //TODO: unconfirmed + + markup.append("cartridge", linkable ? " linkable" : "", "\n"); + markup.append(" rom name=program.rom size=0x", hex(romsize), "\n"); + if(ramsize) + markup.append(" ram name=save.ram size=0x", hex(ramsize), "\n"); +} diff --git a/icarus/heuristics/super-famicom.hpp b/icarus/heuristics/super-famicom.hpp new file mode 100644 index 00000000..ef1a0ce4 --- /dev/null +++ b/icarus/heuristics/super-famicom.hpp @@ -0,0 +1,843 @@ +struct SuperFamicomCartridge { + SuperFamicomCartridge(const uint8_t *data, unsigned size); + + string markup; + +//private: + auto readHeader(const uint8_t *data, unsigned size) -> void; + auto findHeader(const uint8_t *data, unsigned size) -> unsigned; + auto scoreHeader(const uint8_t *data, unsigned size, unsigned addr) -> unsigned; + + enum HeaderField : unsigned { + CartName = 0x00, + Mapper = 0x15, + RomType = 0x16, + RomSize = 0x17, + RamSize = 0x18, + CartRegion = 0x19, + Company = 0x1a, + Version = 0x1b, + Complement = 0x1c, //inverse checksum + Checksum = 0x1e, + ResetVector = 0x3c, + }; + + enum Mode : unsigned { + ModeNormal, + ModeBsxSlotted, + ModeBsx, + ModeSufamiTurbo, + ModeSuperGameBoy, + }; + + enum Type : unsigned { + TypeNormal, + TypeBsxSlotted, + TypeBsxBios, + TypeBsx, + TypeSufamiTurboBios, + TypeSufamiTurbo, + TypeSuperGameBoy1Bios, + TypeSuperGameBoy2Bios, + TypeGameBoy, + TypeUnknown, + }; + + enum Region : unsigned { + NTSC, + PAL, + }; + + enum MemoryMapper : unsigned { + LoROM, + HiROM, + ExLoROM, + ExHiROM, + SuperFXROM, + SA1ROM, + SPC7110ROM, + BSCLoROM, + BSCHiROM, + BSXROM, + STROM, + }; + + enum DSP1MemoryMapper : unsigned { + DSP1Unmapped, + DSP1LoROM1MB, + DSP1LoROM2MB, + DSP1HiROM, + }; + + bool loaded; //is a base cartridge inserted? + unsigned crc32; //crc32 of all cartridges (base+slot(s)) + unsigned rom_size; + unsigned ram_size; + bool firmware_required; //true if firmware is required for emulation + bool firmware_appended; //true if firmware is present at end of data + + Mode mode; + Type type; + Region region; + MemoryMapper mapper; + DSP1MemoryMapper dsp1_mapper; + + bool has_bsx_slot; + bool has_superfx; + bool has_sa1; + bool has_sharprtc; + bool has_epsonrtc; + bool has_sdd1; + bool has_spc7110; + bool has_cx4; + bool has_dsp1; + bool has_dsp2; + bool has_dsp3; + bool has_dsp4; + bool has_obc1; + bool has_st010; + bool has_st011; + bool has_st018; +}; + +SuperFamicomCartridge::SuperFamicomCartridge(const uint8_t *data, unsigned size) { + firmware_required = false; + firmware_appended = false; + + //skip copier header + if((size & 0x7fff) == 512) data += 512, size -= 512; + + if(size < 0x8000) return; + + readHeader(data, size); + + if(type == TypeGameBoy) return; + if(type == TypeBsx) return; + if(type == TypeSufamiTurbo) return; + + const char* range = (rom_size > 0x200000) || (ram_size > 32 * 1024) ? "0000-7fff" : "0000-ffff"; + markup.append("cartridge region=", region == NTSC ? "NTSC" : "PAL", "\n"); + + //detect appended firmware + + if(has_dsp1) { + firmware_required = true; + if((size & 0x7fff) == 0x2000) { + firmware_appended = true; + rom_size -= 0x2000; + } + } + + if(has_dsp2) { + firmware_required = true; + if((size & 0x7fff) == 0x2000) { + firmware_appended = true; + rom_size -= 0x2000; + } + } + + if(has_dsp3) { + firmware_required = true; + if((size & 0x7fff) == 0x2000) { + firmware_appended = true; + rom_size -= 0x2000; + } + } + + if(has_dsp4) { + firmware_required = true; + if((size & 0x7fff) == 0x2000) { + firmware_appended = true; + rom_size -= 0x2000; + } + } + + if(has_st010) { + firmware_required = true; + if((size & 0xffff) == 0xd000) { + firmware_appended = true; + rom_size -= 0xd000; + } + } + + if(has_st011) { + firmware_required = true; + if((size & 0xffff) == 0xd000) { + firmware_appended = true; + rom_size -= 0xd000; + } + } + + if(has_st018) { + firmware_required = true; + if((size & 0x3ffff) == 0x28000) { + firmware_appended = true; + rom_size -= 0x28000; + } + } + + if(has_cx4) { + firmware_required = true; + if((rom_size & 0x7fff) == 0xc00) { + firmware_appended = true; + rom_size -= 0xc00; + } + } + + if(type == TypeSuperGameBoy1Bios || type == TypeSuperGameBoy2Bios) { + firmware_required = true; + if((rom_size & 0x7fff) == 0x100) { + firmware_appended = true; + rom_size -= 0x100; + } + } + + //end firmware detection + + if(type == TypeSuperGameBoy1Bios || type == TypeSuperGameBoy2Bios) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + " map id=rom address=00-7f,80-ff:8000-ffff mask=0x8000\n" + " icd2 revision=1\n" + " rom name=sgb.boot.rom size=0x100\n" + " map id=io address=00-3f,80-bf:6000-7fff\n" + ); + } + + else if(has_cx4) { + markup.append( + " hitachidsp model=HG51B169 frequency=20000000\n" + " rom id=program name=program.rom size=0x", hex(rom_size), "\n" + " rom id=data name=cx4.data.rom size=0xc00\n" + " ram id=data size=0xc00\n" + " map id=io address=00-3f,80-bf:6000-7fff\n" + " map id=rom address=00-7f,80-ff:8000-ffff mask=0x8000\n" + " map id=ram address=70-77:0000-7fff\n" + ); + } + + else if(has_spc7110) { + markup.append( + " spc7110\n" + " rom id=program name=program.rom size=0x100000\n" + " rom id=data name=data.rom size=0x", hex(rom_size - 0x100000), "\n" + " ram name=save.ram size=0x", hex(ram_size), "\n" + " map id=io address=00-3f,80-bf:4800-483f\n" + " map id=io address=50:0000-ffff\n" + " map id=rom address=00-3f,80-bf:8000-ffff\n" + " map id=rom address=c0-ff:0000-ffff\n" + " map id=ram address=00-3f,80-bf:6000-7fff mask=0xe000\n" + ); + } + + else if(has_sdd1) { + markup.append( + " sdd1\n" + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=io address=00-3f,80-bf:4800-4807\n" + " map id=rom address=00-3f,80-bf:8000-ffff mask=0x8000\n" + " map id=rom address=c0-ff:0000-ffff\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=20-3f,a0-bf:6000-7fff mask=0xe000\n" + " map id=ram address=70-7f:0000-7fff\n" + ); + } + + else if(mapper == LoROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=rom address=00-7f,80-ff:8000-ffff mask=0x8000\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=70-7f,f0-ff:", range, "\n" + ); + } + + else if(mapper == HiROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=rom address=00-3f,80-bf:8000-ffff\n" + " map id=rom address=40-7f,c0-ff:0000-ffff\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=10-3f,90-bf:6000-7fff mask=0xe000\n" + ); + } + + else if(mapper == ExLoROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=rom address=00-3f,80-bf:8000-ffff mask=0x8000\n" + " map id=rom address=40-7f:0000-ffff\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=20-3f,a0-bf:6000-7fff\n" + " map id=ram address=70-7f:0000-7fff\n" + ); + } + + else if(mapper == ExHiROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=rom address=00-3f:8000-ffff base=0x400000\n" + " map id=rom address=40-7f:0000-ffff base=0x400000\n" + " map id=rom address=80-bf:8000-ffff mask=0xc00000\n" + " map id=rom address=c0-ff:0000-ffff mask=0xc00000\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=20-3f,a0-bf:6000-7fff mask=0xe000\n" + " map id=ram address=70-7f:", range, "\n" + ); + } + + else if(mapper == SuperFXROM) { + markup.append( + " superfx revision=3\n" + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " map id=io address=00-3f,80-bf:3000-32ff\n" + " map id=rom address=00-3f,80-bf:8000-ffff mask=0x8000\n" + " map id=rom address=40-5f,c0-df:0000-ffff\n" + ); + if(ram_size > 0) markup.append( + " map id=ram address=00-3f,80-bf:6000-7fff size=0x2000\n" + " map id=ram address=70-71,f0-f1:0000-ffff\n" + ); + } + + else if(mapper == SA1ROM) { + markup.append( + " sa1\n" + " rom name=program.rom size=0x", hex(rom_size), "\n" + ); + if(ram_size > 0) markup.append( + " ram id=bitmap name=save.ram size=0x", hex(ram_size), "\n" + ); + markup.append( + " ram id=internal size=0x800\n" + " map id=io address=00-3f,80-bf:2200-23ff\n" + " map id=rom address=00-3f,80-bf:8000-ffff\n" + " map id=rom address=c0-ff:0000-ffff\n" + ); + if(ram_size > 0) markup.append( + " map id=bwram address=00-3f,80-bf:6000-7fff\n" + " map id=bwram address=40-4f:0000-ffff\n" + ); + markup.append( + " map id=iram address=00-3f,80-bf:3000-37ff\n" + ); + } + + else if(mapper == BSCLoROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + " ram name=save.ram size=0x", hex(ram_size), "\n" + " map id=rom address=00-1f:8000-ffff base=0x000000 mask=0x8000\n" + " map id=rom address=20-3f:8000-ffff base=0x100000 mask=0x8000\n" + " map id=rom address=80-9f:8000-ffff base=0x200000 mask=0x8000\n" + " map id=rom address=a0-bf:8000-ffff base=0x100000 mask=0x8000\n" + " map id=ram address=70-7f,f0-ff:0000-7fff\n" + " satellaview\n" + " map id=rom address=c0-ef:0000-ffff\n" + ); + } + + else if(mapper == BSCHiROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + " ram name=save.ram size=0x", hex(ram_size), "\n" + " map id=rom address=00-1f,80-9f:8000-ffff\n" + " map id=rom address=40-5f,c0-df:0000-ffff\n" + " map id=ram address=20-3f,a0-bf:6000-7fff\n" + " satellaview\n" + " map id=rom address=20-3f,a0-bf:8000-ffff\n" + " map id=rom address=60-7f,e0-ff:0000-ffff\n" + ); + } + + else if(mapper == BSXROM) { + markup.append( + " mcc\n" + " rom name=program.rom size=0x", hex(rom_size), "\n" + " ram id=save name=save.ram size=0x", hex(ram_size), "\n" + " ram id=download name=bsx.ram size=0x80000\n" + " map id=io address=00-3f,80-bf:5000-5fff\n" + " map id=rom address=00-3f,80-bf:8000-ffff\n" + " map id=rom address=40-7f,c0-ff:0000-ffff\n" + " map id=ram address=20-3f:6000-7fff\n" + ); + } + + else if(mapper == STROM) { + markup.append( + " rom name=program.rom size=0x", hex(rom_size), "\n" + " map id=rom address='00-1f,80-9f:8000-ffff mask=0x8000\n" + " sufamiturbo\n" + " slot id=A\n" + " map id=rom address=20-3f,a0-bf:8000-ffff mask=0x8000\n" + " map id=ram address=60-63,e0-e3:8000-ffff\n" + " slot id=B\n" + " map id=rom address=40-5f,c0-df:8000-ffff mask=0x8000\n" + " map id=ram address=70-73,f0-f3:8000-ffff\n" + ); + } + + if(has_sharprtc) { + markup.append( + " sharprtc\n" + " ram name=rtc.ram size=0x10\n" + " map id=io address=00-3f,80-bf:2800-2801\n" + ); + } + + if(has_epsonrtc) { + markup.append( + " epsonrtc\n" + " ram name=rtc.ram size=0x10\n" + " map id=io address=00-3f,80-bf:4840-4842\n" + ); + } + + if(has_obc1) { + markup.append( + " obc1\n" + " ram name=save.ram size=0x2000\n" + " map id=io address=00-3f,80-bf:6000-7fff\n" + ); + } + + if(has_dsp1) { + markup.append( + " necdsp model=uPD7725 frequency=8000000\n" + " rom id=program name=dsp1b.program.rom size=0x1800\n" + " rom id=data name=dsp1b.data.rom size=0x800\n" + " ram id=data size=0x200\n" + ); + if(dsp1_mapper == DSP1LoROM1MB) markup.append( + " map id=io address=20-3f,a0-bf:8000-ffff select=0x4000\n" + ); + if(dsp1_mapper == DSP1LoROM2MB) markup.append( + " map id=io address=60-6f,e0-ef:0000-7fff select=0x4000\n" + ); + if(dsp1_mapper == DSP1HiROM) markup.append( + " map id=io address=00-1f,80-9f:6000-7fff select=0x1000\n" + ); + } + + if(has_dsp2) { + markup.append( + " necdsp model=uPD7725 frequency=8000000\n" + " rom id=program name=dsp2.program.rom size=0x1800\n" + " rom id=data name=dsp2.data.rom size=0x800\n" + " ram id=data size=0x200\n" + " map id=io address=20-3f,a0-bf:8000-ffff select=0x4000\n" + ); + } + + if(has_dsp3) { + markup.append( + " necdsp model=uPD7725 frequency=8000000\n" + " rom id=program name=dsp3.program.rom size=0x1800\n" + " rom id=data name=dsp3.data.rom size=0x800\n" + " ram id=data size=0x200\n" + " map id=io address=20-3f,a0-bf:8000-ffff select=0x4000\n" + ); + } + + if(has_dsp4) { + markup.append( + " necdsp model=uPD7725 frequency=8000000\n" + " rom id=program name=dsp4.program.rom size=0x1800\n" + " rom id=data name=dsp4.data.rom size=0x800\n" + " ram id=data size=0x200\n" + " map id=io address=30-3f,b0-bf:8000-ffff select=0x4000\n" + ); + } + + if(has_st010) { + markup.append( + " necdsp model=uPD96050 frequency=11000000\n" + " rom id=program name=st010.program.rom size=0xc000\n" + " rom id=data name=st010.data.rom size=0x1000\n" + " ram id=data name=save.ram size=0x1000\n" + " map id=io address=60-67,e0-e7:0000-3fff select=0x0001\n" + " map id=ram address=68-6f,e8-ef:0000-7fff\n" + ); + } + + if(has_st011) { + markup.append( + " necdsp model=uPD96050 frequency=15000000\n" + " rom id=program name=st011.program.rom size=0xc000\n" + " rom id=data name=st011.data.rom size=0x1000\n" + " ram id=data name=save.ram size=0x1000\n" + " map id=io address=60-67,e0-e7:0000-3fff select=0x0001\n" + " map id=ram address=68-6f,e8-ef:0000-7fff\n" + ); + } + + if(has_st018) { + markup.append( + " armdsp frequency=21477272\n" + " rom id=program name=st018.program.rom size=0x20000\n" + " rom id=data name=st018.data.rom size=0x8000\n" + " ram name=save.ram size=0x4000\n" + " map id=io address=00-3f,80-bf:3800-38ff\n" + ); + } +} + +auto SuperFamicomCartridge::readHeader(const uint8_t *data, unsigned size) -> void { + type = TypeUnknown; + mapper = LoROM; + dsp1_mapper = DSP1Unmapped; + region = NTSC; + rom_size = size; + ram_size = 0; + + has_bsx_slot = false; + has_superfx = false; + has_sa1 = false; + has_sharprtc = false; + has_epsonrtc = false; + has_sdd1 = false; + has_spc7110 = false; + has_cx4 = false; + has_dsp1 = false; + has_dsp2 = false; + has_dsp3 = false; + has_dsp4 = false; + has_obc1 = false; + has_st010 = false; + has_st011 = false; + has_st018 = false; + + //===================== + //detect Game Boy carts + //===================== + + if(size >= 0x0140) { + if(data[0x0104] == 0xce && data[0x0105] == 0xed && data[0x0106] == 0x66 && data[0x0107] == 0x66 + && data[0x0108] == 0xcc && data[0x0109] == 0x0d && data[0x010a] == 0x00 && data[0x010b] == 0x0b) { + type = TypeGameBoy; + return; + } + } + + if(size < 32768) { + type = TypeUnknown; + return; + } + + const unsigned index = findHeader(data, size); + const uint8_t mapperid = data[index + Mapper]; + const uint8_t rom_type = data[index + RomType]; + const uint8_t rom_size = data[index + RomSize]; + const uint8_t company = data[index + Company]; + const uint8_t regionid = data[index + CartRegion] & 0x7f; + + ram_size = 1024 << (data[index + RamSize] & 7); + if(ram_size == 1024) ram_size = 0; //no RAM present + if(rom_size == 0 && ram_size) ram_size = 0; //fix for Bazooka Blitzkrieg's malformed header (swapped ROM and RAM sizes) + + //0, 1, 13 = NTSC; 2 - 12 = PAL + region = (regionid <= 1 || regionid >= 13) ? NTSC : PAL; + + //======================= + //detect BS-X flash carts + //======================= + + if(data[index + 0x13] == 0x00 || data[index + 0x13] == 0xff) { + if(data[index + 0x14] == 0x00) { + const uint8_t n15 = data[index + 0x15]; + if(n15 == 0x00 || n15 == 0x80 || n15 == 0x84 || n15 == 0x9c || n15 == 0xbc || n15 == 0xfc) { + if(data[index + 0x1a] == 0x33 || data[index + 0x1a] == 0xff) { + type = TypeBsx; + mapper = BSXROM; + region = NTSC; //BS-X only released in Japan + return; + } + } + } + } + + //========================= + //detect Sufami Turbo carts + //========================= + + if(!memcmp(data, "BANDAI SFC-ADX", 14)) { + if(!memcmp(data + 16, "SFC-ADX BACKUP", 14)) { + type = TypeSufamiTurboBios; + } else { + type = TypeSufamiTurbo; + } + mapper = STROM; + region = NTSC; //Sufami Turbo only released in Japan + return; //RAM size handled outside this routine + } + + //========================== + //detect Super Game Boy BIOS + //========================== + + if(!memcmp(data + index, "Super GAMEBOY2", 14)) { + type = TypeSuperGameBoy2Bios; + return; + } + + if(!memcmp(data + index, "Super GAMEBOY", 13)) { + type = TypeSuperGameBoy1Bios; + return; + } + + //===================== + //detect standard carts + //===================== + + //detect presence of BS-X flash cartridge connector (reads extended header information) + if(data[index - 14] == 'Z') { + if(data[index - 11] == 'J') { + uint8_t n13 = data[index - 13]; + if((n13 >= 'A' && n13 <= 'Z') || (n13 >= '0' && n13 <= '9')) { + if(company == 0x33 || (data[index - 10] == 0x00 && data[index - 4] == 0x00)) { + has_bsx_slot = true; + } + } + } + } + + if(has_bsx_slot) { + if(!memcmp(data + index, "Satellaview BS-X ", 21)) { + //BS-X base cart + type = TypeBsxBios; + mapper = BSXROM; + region = NTSC; //BS-X only released in Japan + return; //RAM size handled internally by load_cart_bsx() -> BSXCart class + } else { + type = TypeBsxSlotted; + mapper = (index == 0x7fc0 ? BSCLoROM : BSCHiROM); + region = NTSC; //BS-X slotted cartridges only released in Japan + } + } else { + //standard cart + type = TypeNormal; + + if(index == 0x7fc0 && size >= 0x401000) { + mapper = ExLoROM; + } else if(index == 0x7fc0 && mapperid == 0x32) { + mapper = ExLoROM; + } else if(index == 0x7fc0) { + mapper = LoROM; + } else if(index == 0xffc0) { + mapper = HiROM; + } else { //index == 0x40ffc0 + mapper = ExHiROM; + } + } + + if(mapperid == 0x20 && (rom_type == 0x13 || rom_type == 0x14 || rom_type == 0x15 || rom_type == 0x1a)) { + has_superfx = true; + mapper = SuperFXROM; + ram_size = 1024 << (data[index - 3] & 7); + if(ram_size == 1024) ram_size = 0; + } + + if(mapperid == 0x23 && (rom_type == 0x32 || rom_type == 0x34 || rom_type == 0x35)) { + has_sa1 = true; + mapper = SA1ROM; + } + + if(mapperid == 0x35 && rom_type == 0x55) { + has_sharprtc = true; + } + + if(mapperid == 0x32 && (rom_type == 0x43 || rom_type == 0x45)) { + has_sdd1 = true; + } + + if(mapperid == 0x3a && (rom_type == 0xf5 || rom_type == 0xf9)) { + has_spc7110 = true; + has_epsonrtc = (rom_type == 0xf9); + mapper = SPC7110ROM; + } + + if(mapperid == 0x20 && rom_type == 0xf3) { + has_cx4 = true; + } + + if((mapperid == 0x20 || mapperid == 0x21) && rom_type == 0x03) { + has_dsp1 = true; + } + + if(mapperid == 0x30 && rom_type == 0x05 && company != 0xb2) { + has_dsp1 = true; + } + + if(mapperid == 0x31 && (rom_type == 0x03 || rom_type == 0x05)) { + has_dsp1 = true; + } + + if(has_dsp1 == true) { + if((mapperid & 0x2f) == 0x20 && size <= 0x100000) { + dsp1_mapper = DSP1LoROM1MB; + } else if((mapperid & 0x2f) == 0x20) { + dsp1_mapper = DSP1LoROM2MB; + } else if((mapperid & 0x2f) == 0x21) { + dsp1_mapper = DSP1HiROM; + } + } + + if(mapperid == 0x20 && rom_type == 0x05) { + has_dsp2 = true; + } + + if(mapperid == 0x30 && rom_type == 0x05 && company == 0xb2) { + has_dsp3 = true; + } + + if(mapperid == 0x30 && rom_type == 0x03) { + has_dsp4 = true; + } + + if(mapperid == 0x30 && rom_type == 0x25) { + has_obc1 = true; + } + + if(mapperid == 0x30 && rom_type == 0xf6 && rom_size >= 10) { + has_st010 = true; + } + + if(mapperid == 0x30 && rom_type == 0xf6 && rom_size < 10) { + has_st011 = true; + } + + if(mapperid == 0x30 && rom_type == 0xf5) { + has_st018 = true; + } +} + +auto SuperFamicomCartridge::findHeader(const uint8_t *data, unsigned size) -> unsigned { + unsigned score_lo = scoreHeader(data, size, 0x007fc0); + unsigned score_hi = scoreHeader(data, size, 0x00ffc0); + unsigned score_ex = scoreHeader(data, size, 0x40ffc0); + if(score_ex) score_ex += 4; //favor ExHiROM on images > 32mbits + + if(score_lo >= score_hi && score_lo >= score_ex) { + return 0x007fc0; + } else if(score_hi >= score_ex) { + return 0x00ffc0; + } else { + return 0x40ffc0; + } +} + +auto SuperFamicomCartridge::scoreHeader(const uint8_t *data, unsigned size, unsigned addr) -> unsigned { + if(size < addr + 64) return 0; //image too small to contain header at this location? + int score = 0; + + uint16_t resetvector = data[addr + ResetVector] | (data[addr + ResetVector + 1] << 8); + uint16_t checksum = data[addr + Checksum ] | (data[addr + Checksum + 1] << 8); + uint16_t complement = data[addr + Complement ] | (data[addr + Complement + 1] << 8); + + uint8_t resetop = data[(addr & ~0x7fff) | (resetvector & 0x7fff)]; //first opcode executed upon reset + uint8_t mapper = data[addr + Mapper] & ~0x10; //mask off irrelevent FastROM-capable bit + + //$00:[000-7fff] contains uninitialized RAM and MMIO. + //reset vector must point to ROM at $00:[8000-ffff] to be considered valid. + if(resetvector < 0x8000) return 0; + + //some images duplicate the header in multiple locations, and others have completely + //invalid header information that cannot be relied upon. + //below code will analyze the first opcode executed at the specified reset vector to + //determine the probability that this is the correct header. + + //most likely opcodes + if(resetop == 0x78 //sei + || resetop == 0x18 //clc (clc; xce) + || resetop == 0x38 //sec (sec; xce) + || resetop == 0x9c //stz $nnnn (stz $4200) + || resetop == 0x4c //jmp $nnnn + || resetop == 0x5c //jml $nnnnnn + ) score += 8; + + //plausible opcodes + if(resetop == 0xc2 //rep #$nn + || resetop == 0xe2 //sep #$nn + || resetop == 0xad //lda $nnnn + || resetop == 0xae //ldx $nnnn + || resetop == 0xac //ldy $nnnn + || resetop == 0xaf //lda $nnnnnn + || resetop == 0xa9 //lda #$nn + || resetop == 0xa2 //ldx #$nn + || resetop == 0xa0 //ldy #$nn + || resetop == 0x20 //jsr $nnnn + || resetop == 0x22 //jsl $nnnnnn + ) score += 4; + + //implausible opcodes + if(resetop == 0x40 //rti + || resetop == 0x60 //rts + || resetop == 0x6b //rtl + || resetop == 0xcd //cmp $nnnn + || resetop == 0xec //cpx $nnnn + || resetop == 0xcc //cpy $nnnn + ) score -= 4; + + //least likely opcodes + if(resetop == 0x00 //brk #$nn + || resetop == 0x02 //cop #$nn + || resetop == 0xdb //stp + || resetop == 0x42 //wdm + || resetop == 0xff //sbc $nnnnnn,x + ) score -= 8; + + //at times, both the header and reset vector's first opcode will match ... + //fallback and rely on info validity in these cases to determine more likely header. + + //a valid checksum is the biggest indicator of a valid header. + if((checksum + complement) == 0xffff && (checksum != 0) && (complement != 0)) score += 4; + + if(addr == 0x007fc0 && mapper == 0x20) score += 2; //0x20 is usually LoROM + if(addr == 0x00ffc0 && mapper == 0x21) score += 2; //0x21 is usually HiROM + if(addr == 0x007fc0 && mapper == 0x22) score += 2; //0x22 is usually ExLoROM + if(addr == 0x40ffc0 && mapper == 0x25) score += 2; //0x25 is usually ExHiROM + + if(data[addr + Company] == 0x33) score += 2; //0x33 indicates extended header + if(data[addr + RomType] < 0x08) score++; + if(data[addr + RomSize] < 0x10) score++; + if(data[addr + RamSize] < 0x08) score++; + if(data[addr + CartRegion] < 14) score++; + + if(score < 0) score = 0; + return score; +} diff --git a/icarus/icarus.cpp b/icarus/icarus.cpp new file mode 100644 index 00000000..73cf5c28 --- /dev/null +++ b/icarus/icarus.cpp @@ -0,0 +1,45 @@ +#include +using namespace nall; + +#include +using namespace hiro; + +#include "settings.cpp" +Settings settings; + +#include "heuristics/famicom.hpp" +#include "heuristics/super-famicom.hpp" +#include "heuristics/game-boy.hpp" +#include "heuristics/game-boy-advance.hpp" +#include "heuristics/bsx-satellaview.hpp" +#include "heuristics/sufami-turbo.hpp" + +#include "core/core.hpp" +#include "core/core.cpp" +#include "core/famicom.cpp" +#include "core/super-famicom.cpp" +#include "core/game-boy.cpp" +#include "core/game-boy-color.cpp" +#include "core/game-boy-advance.cpp" +#include "core/bsx-satellaview.cpp" +#include "core/sufami-turbo.cpp" +Icarus icarus; + +#include "ui/ui.hpp" +#include "ui/scan-dialog.cpp" +#include "ui/import-dialog.cpp" +#include "ui/error-dialog.cpp" + +#include +auto nall::main(lstring args) -> void { + if(args.size() == 3 && args[1] == "-m") { + if(!directory::exists(args[2])) return print("error: directory not found\n"); + return print(icarus.manifest(args[2])); + } + + new ScanDialog; + new ImportDialog; + new ErrorDialog; + scanDialog->show(); + Application::run(); +} diff --git a/icarus/obj/.gitignore b/icarus/obj/.gitignore new file mode 100644 index 00000000..5761abcf --- /dev/null +++ b/icarus/obj/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/icarus/settings.cpp b/icarus/settings.cpp new file mode 100644 index 00000000..45ce7482 --- /dev/null +++ b/icarus/settings.cpp @@ -0,0 +1,27 @@ +struct Settings : Configuration::Document { + Settings(); + ~Settings(); + + Configuration::Node root; + string activePath; + string libraryPath; + bool createManifests = false; +}; + +Settings::Settings() { + root.append(activePath, "ActivePath"); + root.append(libraryPath, "LibraryPath"); + root.append(createManifests, "CreateManifests"); + append(root, "Settings"); + + directory::create({configpath(), "icarus/"}); + load({configpath(), "icarus/settings.bml"}); + save({configpath(), "icarus/settings.bml"}); + + if(!activePath) activePath = userpath(); + if(!libraryPath) libraryPath = {userpath(), "Emulation/"}; +} + +Settings::~Settings() { + save({configpath(), "icarus/settings.bml"}); +} diff --git a/icarus/ui/error-dialog.cpp b/icarus/ui/error-dialog.cpp new file mode 100644 index 00000000..9de2d17d --- /dev/null +++ b/icarus/ui/error-dialog.cpp @@ -0,0 +1,16 @@ +ErrorDialog::ErrorDialog() { + errorDialog = this; + + onClose([&] { setVisible(false); scanDialog->show(); }); + layout.setMargin(5); + errorLog.setEditable(false); + closeButton.setText("Close").onActivate([&] { doClose(); }); + + setSize({800, 360}); + setCentered(); +} + +auto ErrorDialog::show(const string& text) -> void { + errorLog.setText(text); + setVisible(); +} diff --git a/icarus/ui/import-dialog.cpp b/icarus/ui/import-dialog.cpp new file mode 100644 index 00000000..8dec9cc3 --- /dev/null +++ b/icarus/ui/import-dialog.cpp @@ -0,0 +1,52 @@ +ImportDialog::ImportDialog() { + importDialog = this; + + onClose([&] { + stopButton.setEnabled(false).setText("Stopping ..."); + abort = true; + }); + layout.setMargin(5); + stopButton.setText("Stop").onActivate([&] { doClose(); }); + + setTitle("icarus - Importing ..."); + setSize({480, layout.minimumSize().height()}); + setCentered(); +} + +auto ImportDialog::run(lstring locations) -> void { + abort = false; + errors.reset(); + unsigned position = 0; + + setVisible(true); + for(auto& location : locations) { + auto name = location.basename(); + + if(abort) { + errors.append(string{"[", name, "] aborted"}); + continue; + } + + statusLabel.setText(name); + double progress = 100.0 * (double)position++ / (double)locations.size() + 0.5; + progressBar.setPosition((unsigned)progress); + Application::processEvents(); + + if(!icarus.import(location)) { + errors.append(string{"[", name, "] ", icarus.error()}); + } + } + setVisible(false); + + if(errors) { + string message{"Import completed, but with ", errors.size(), " error", errors.size() ? "s" : "", ". View log?"}; + if(MessageDialog().setTitle("icarus").setText(message).question() == "Yes") { + errorDialog->show(errors.merge("\n")); + } else { + scanDialog->show(); + } + } else { + MessageDialog().setTitle("icarus").setText("Import completed successfully.").information(); + scanDialog->show(); + } +} diff --git a/icarus/ui/scan-dialog.cpp b/icarus/ui/scan-dialog.cpp new file mode 100644 index 00000000..9bc6e73f --- /dev/null +++ b/icarus/ui/scan-dialog.cpp @@ -0,0 +1,116 @@ +ScanDialog::ScanDialog() { + scanDialog = this; + + onClose(&Application::quit); + layout.setMargin(5); + pathEdit.onActivate([&] { refresh(); }); + refreshButton.setIcon(Icon::Action::Refresh).setBordered(false).onActivate([&] { + pathEdit.setText(settings.activePath); + refresh(); + }); + homeButton.setIcon(Icon::Go::Home).setBordered(false).onActivate([&] { + pathEdit.setText(userpath()); + refresh(); + }); + upButton.setIcon(Icon::Go::Up).setBordered(false).onActivate([&] { + pathEdit.setText(settings.activePath.dirname()); + refresh(); + }); + scanList.onActivate([&] { activate(); }); + selectAllButton.setText("Select All").onActivate([&] { + for(auto& item : scanList.items()) item.cell(0).setChecked(true); + }); + unselectAllButton.setText("Unselect All").onActivate([&] { + for(auto& item : scanList.items()) item.cell(0).setChecked(false); + }); + createManifestsLabel.setChecked(settings.createManifests).setText("Create Manifests").onToggle([&] { + settings.createManifests = createManifestsLabel.checked(); + }); + importButton.setText("Import ...").onActivate([&] { import(); }); + + setTitle("icarus"); + setSize({800, 480}); + setCentered(); +} + +auto ScanDialog::show() -> void { + setVisible(); + pathEdit.setText(settings.activePath); + refresh(); +} + +auto ScanDialog::refresh() -> void { + scanList.reset(); + scanList.append(ListViewHeader().setVisible(false).append(ListViewColumn().setExpandable())); + + auto pathname = pathEdit.text().transform("\\", "/").rtrim("/").append("/"); + if(!directory::exists(pathname)) return; + + pathEdit.setText(settings.activePath = pathname); + auto contents = directory::icontents(pathname); + + for(auto& name : contents) { + if(!name.endsWith("/")) continue; + if(gamePakType(name.suffixname())) continue; + scanList.append(ListViewItem().append(ListViewCell().setIcon(Icon::Emblem::Folder).setText(name.rtrim("/")))); + } + + for(auto& name : contents) { + if(name.endsWith("/")) continue; + if(!gameRomType(name.suffixname().downcase())) continue; + scanList.append(ListViewItem().append(ListViewCell().setCheckable().setIcon(Icon::Emblem::File).setText(name))); + } + + Application::processEvents(); + scanList.resizeColumns(); + scanList.setFocused(); +} + +auto ScanDialog::activate() -> void { + if(auto item = scanList.selected()) { + string location{settings.activePath, item.cell(0).text()}; + if(directory::exists(location) && !gamePakType(location.suffixname())) { + pathEdit.setText(location); + refresh(); + } + } +} + +auto ScanDialog::import() -> void { + lstring filenames; + for(auto& item : scanList.items()) { + if(item.cell(0).checked()) { + filenames.append(string{settings.activePath, item.cell(0).text()}); + } + } + + if(!filenames) { + MessageDialog().setParent(*this).setText("Nothing selected to import.").error(); + return; + } + + setVisible(false); + importDialog->run(filenames); +} + +auto ScanDialog::gamePakType(const string& type) -> bool { + return type == ".sys" + || type == ".fc" + || type == ".sfc" + || type == ".gb" + || type == ".gbc" + || type == ".gba" + || type == ".bs" + || type == ".st"; +} + +auto ScanDialog::gameRomType(const string& type) -> bool { + return type == ".zip" + || type == ".fc" || type == ".nes" + || type == ".sfc" || type == ".smc" + || type == ".gb" + || type == ".gbc" + || type == ".gba" + || type == ".bs" + || type == ".st"; +} diff --git a/icarus/ui/ui.hpp b/icarus/ui/ui.hpp new file mode 100644 index 00000000..09294d88 --- /dev/null +++ b/icarus/ui/ui.hpp @@ -0,0 +1,54 @@ +struct ScanDialog : Window { + ScanDialog(); + + auto show() -> void; + auto refresh() -> void; + auto activate() -> void; + auto import() -> void; + + auto gamePakType(const string& type) -> bool; + auto gameRomType(const string& type) -> bool; + + VerticalLayout layout{this}; + HorizontalLayout pathLayout{&layout, Size{~0, 0}}; + LineEdit pathEdit{&pathLayout, Size{~0, 0}, 0}; + Button refreshButton{&pathLayout, Size{0, 0}, 0}; + Button homeButton{&pathLayout, Size{0, 0}, 0}; + Button upButton{&pathLayout, Size{0, 0}, 0}; + ListView scanList{&layout, Size{~0, ~0}}; + HorizontalLayout controlLayout{&layout, Size{~0, 0}}; + Button selectAllButton{&controlLayout, Size{100, 0}}; + Button unselectAllButton{&controlLayout, Size{100, 0}}; + CheckLabel createManifestsLabel{&controlLayout, Size{~0, 0}}; + Button importButton{&controlLayout, Size{80, 0}}; +}; + +struct ImportDialog : Window { + ImportDialog(); + auto run(lstring locations) -> void; + + bool abort; + lstring errors; + + VerticalLayout layout{this}; + Label statusLabel{&layout, Size{~0, 0}}; + ProgressBar progressBar{&layout, Size{~0, 0}}; + HorizontalLayout controlLayout{&layout, Size{~0, 0}}; + Widget spacer{&controlLayout, Size{~0, 0}}; + Button stopButton{&controlLayout, Size{80, 0}}; +}; + +struct ErrorDialog : Window { + ErrorDialog(); + auto show(const string& text) -> void; + + VerticalLayout layout{this}; + TextEdit errorLog{&layout, Size{~0, ~0}}; + HorizontalLayout controlLayout{&layout, Size{~0, 0}}; + Widget spacer{&controlLayout, Size{~0, 0}}; + Button closeButton{&controlLayout, Size{80, 0}}; +}; + +ScanDialog* scanDialog = nullptr; +ImportDialog* importDialog = nullptr; +ErrorDialog* errorDialog = nullptr;