// This file is part of Gopher2600. // // Gopher2600 is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Gopher2600 is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Gopher2600. If not, see . package cartridge import ( "bytes" "fmt" "io" "github.com/jetsetilly/gopher2600/cartridgeloader" ) // if anwhere parameter is true then the ELF magic number can appear anywhere // in the data (the b parameter). otherwise it must appear at the beginning of // the data func fingerprintElf(loader cartridgeloader.Loader, anywhere bool) bool { if anywhere { if loader.Contains([]byte{0x7f, 'E', 'L', 'F'}) { return true } } else { b := make([]byte, 4) loader.Seek(0, io.SeekStart) if n, err := loader.Read(b); n != len(b) || err != nil { return false } if bytes.Equal(b, []byte{0x7f, 'E', 'L', 'F'}) { return true } } return false } func fingerprintAce(loader cartridgeloader.Loader) (bool, bool) { // some ACE files embed an ELF file inside the ACE data. these files are // identified by the presence of "elf-relocatable" in the data premable wrappedELF := loader.ContainsLimit(144, []byte("elf-relocatable")) // make double sure this is actually an elf file. otherwise it's just an // ACE file with elf-relocatable in the data preamble wrappedELF = wrappedELF && fingerprintElf(loader, true) if loader.ContainsLimit(144, []byte("ACE-2600")) { return true, wrappedELF } if loader.ContainsLimit(144, []byte("ACE-PC00")) { return true, wrappedELF } if loader.ContainsLimit(144, []byte("ACE-UF00")) { return true, wrappedELF } return false, false } func (cart *Cartridge) fingerprintPlusROM(loader cartridgeloader.Loader) bool { // there is a second fingerprint that occurs in the NewPlusROM() function b := make([]byte, 3) loader.Seek(0, io.SeekStart) for { n, err := loader.Read(b) if n < len(b) { break } if b[0] == 0x8d && b[1] == 0xf1 && b[2]&0x10 == 0x10 { return true } if err == io.EOF { break } loader.Seek(int64(1-len(b)), io.SeekCurrent) } return false } func fingerprint3e(loader cartridgeloader.Loader) bool { // 3E cart bankswitching is triggered by storing the bank number in address // 3E using 'STA $3E', commonly followed by an immediate mode LDA // // fingerprint method taken from: // // https://gitlab.com/firmaplus/atari-2600-pluscart/-/blob/master/source/STM32firmware/PlusCart/Src/cartridge_detection.c#L140 return loader.Contains([]byte{0x85, 0x3e, 0xa9, 0x00}) } func fingerprint3ePlus(loader cartridgeloader.Loader) bool { // previous versions of this function worked similarly to the tigervision // method but this is more accurate // // fingerprint method taken from: // // https://gitlab.com/firmaplus/atari-2600-pluscart/-/blob/master/source/STM32firmware/PlusCart/Src/cartridge_detection.c#L148 return loader.Contains([]byte{'T', 'J', '3', 'E'}) } func fingerprintMnetwork(loader cartridgeloader.Loader) bool { // Bump 'n' Jump is the fussiest mnetwork cartridge I've found. Matching // hotspots: // // $fdd5 LDA BANK5 // $fde3 LDA BANK6 // $fe0e LDA BANK5 // $fe16 LDA BANK4 // // This also catches modern games not created by mnetwork but which use the // format, eg Pitkat // // (24/01/21) // // Splendidnut's Congobongo demo is even fussier than Bump 'n' Jump. While // I expect the ROM to get more complex we should support the demo ROM if // only because it exists. // // notably, it uses a non-primary-mirror cartridge address and there are // only two bankswitch instructions in the data (although I didn't search // it exhaustively - just LDA instructions). // // threshold has been reduced to two. // // (19/06/21) // // Thomas Jentzch's Elite demo requires a threshold of one // // https://atariage.com/forums/topic/155657-elite-3d-graphics/?do=findComment&comment=2444328 // // with such a low threshold, mnetwork should probably be the very last type to check for threshold := 1 b := make([]byte, 3) loader.Seek(0, io.SeekStart) for { n, err := loader.Read(b) if n < len(b) { break } if b[0] == 0xad && b[1] >= 0xe0 && b[1] <= 0xe7 { // bank switching can address any cartidge mirror so mask off // insignificant bytes // // (09/03/21) mask wasn't correct (0x0f selects non-cartridge // mirrors too) correct mask is 0x1f. // // the incorrect mask caused a false positive for Solaris when the // threshold is 2. // // (20/06/21) this caused a falso positive for "Hack Em Hangly Pacman" // when the threshold is 1 // // change to only look for mirrors 0x1f and 0xff if b[2] == 0x1f || b[2] == 0xff { threshold-- if threshold == 0 { return true } } } if err == io.EOF { break } loader.Seek(int64(1-len(b)), io.SeekCurrent) } return false } func fingerprintParkerBros(loader cartridgeloader.Loader) bool { // parker bros fingerprint taken from Stella fingerprint := [][]byte{ {0x8d, 0xe0, 0x1f}, // STA $1FE0 {0x8d, 0xe0, 0x5f}, // STA $5FE0 {0x8d, 0xe9, 0xff}, // STA $FFE9 {0x0c, 0xe0, 0x1f}, // NOP $1FE0 {0xad, 0xe0, 0x1f}, // LDA $1FE0 {0xad, 0xe9, 0xff}, // LDA $FFE9 {0xad, 0xed, 0xff}, // LDA $FFED {0xad, 0xf3, 0xbf}, // LDA $BFF3 } for _, f := range fingerprint { if loader.Contains(f) { return true } } return false } func fingerprintDF(loader cartridgeloader.Loader) bool { b := make([]byte, 4) loader.Seek(0x0ff8, io.SeekStart) if n, err := loader.Read(b); n != len(b) || err != nil { return false } return bytes.Equal(b, []byte{'D', 'F', 'S', 'C'}) } func fingerprintWickstead(loader cartridgeloader.Loader) bool { // wickstead design fingerprint taken from Stella return loader.Contains([]byte{0xa5, 0x39, 0x4c}) } func fingerprintSCABS(loader cartridgeloader.Loader) bool { // SCABS fingerprint taken from Stella fingerprint := [][]byte{ {0x20, 0x00, 0xd0, 0xc6, 0xc5}, // JSR $D000; DEC $C5 {0x20, 0xc3, 0xf8, 0xa5, 0x82}, // JSR $F8C3; LDA $82 {0xd0, 0xfB, 0x20, 0x73, 0xfe}, // BNE $FB; JSR $FE73 {0x20, 0x00, 0xf0, 0x84, 0xd6}, // JSR $F000; $84, $D6 } for _, f := range fingerprint { if loader.Contains(f) { return true } } return false } func fingerprintUA(loader cartridgeloader.Loader) bool { // ua fingerprint taken from Stella fingerprint := [][]byte{ {0x8D, 0x40, 0x02}, // STA $240 (Funky Fish, Pleiades) {0xAD, 0x40, 0x02}, // LDA $240 (???) {0xBD, 0x1F, 0x02}, // LDA $21F,X (Gingerbread Man) {0x2C, 0xC0, 0x02}, // BIT $2C0 (Time Pilot) {0x8D, 0xC0, 0x02}, // STA $2C0 (Fathom, Vanguard) {0xAD, 0xC0, 0x02}, // LDA $2C0 (Mickey) } for _, f := range fingerprint { if loader.Contains(f) { return true } } return false } func fingerprintDPCplus(loader cartridgeloader.Loader) bool { b := make([]byte, 4) loader.Seek(0x0020, io.SeekStart) if n, err := loader.Read(b); n != len(b) || err != nil { return false } return bytes.Equal(b, []byte{0x1e, 0xab, 0xad, 0x10}) } func fingerprintCDF(loader cartridgeloader.Loader) (bool, string) { if loader.ContainsLimit(2048, []byte("PLUSCDFJ")) { return true, "CDFJ+" } if loader.ContainsLimit(2048, []byte("CDFJ")) { return true, "CDFJ" } // old-school CDF version detection b := make([]byte, 4) loader.Seek(0, io.SeekStart) for { n, err := loader.Read(b) if n < len(b) { break } if bytes.Equal(b[:3], []byte("CDF")) { return true, fmt.Sprintf("CDF%1d", b[3]) } if err == io.EOF { break } loader.Seek(int64(1-len(b)), io.SeekCurrent) } return false, "" } func fingerprintSuperchargerFastLoad(cartload cartridgeloader.Loader) bool { return cartload.Size() > 0 && cartload.Size()%8448 == 0 } func fingerprintTigervision(loader cartridgeloader.Loader) bool { // tigervision cartridges change banks by writing to memory address 0x3f. we // can hypothesise that these types of cartridges will have that instruction // sequence "85 3f" many times in a ROM whereas other cartridge types will not threshold := 5 return loader.Count([]byte{0x85, 0x3f}) > threshold } func fingerprint8k(loader cartridgeloader.Loader) string { if fingerprintTigervision(loader) { return "3F" } if fingerprintParkerBros(loader) { return "E0" } // mnetwork has the lowest threshold so place it at the end if fingerprintMnetwork(loader) { return "E7" } if fingerprintWickstead(loader) { return "WD" } if fingerprintSCABS(loader) { return "FE" } if fingerprintUA(loader) { return "UA" } return "F8" } func fingerprint16k(loader cartridgeloader.Loader) string { if fingerprintTigervision(loader) { return "3F" } if fingerprintMnetwork(loader) { return "E7" } return "F6" } func fingerprint32k(loader cartridgeloader.Loader) string { if fingerprintTigervision(loader) { return "3F" } return "F4" } func fingerprint64k(loader cartridgeloader.Loader) string { return "EF" } func fingerprint128k(loader cartridgeloader.Loader) string { if fingerprintDF(loader) { return "DF" } return "SB" } func fingerprint256k(loader cartridgeloader.Loader) string { return "SB" } func (cart *Cartridge) fingerprint(cartload cartridgeloader.Loader) (string, error) { // moviecart fingerprinting is done in cartridge loader. this is to avoid // loading the entire file into memory, which we definitely don't want to do // with moviecart files due to the large size if ok := fingerprintElf(cartload, false); ok { return "ELF", nil } if ok, wrappedElf := fingerprintAce(cartload); ok { _ = wrappedElf return "ACE", nil } if ok, version := fingerprintCDF(cartload); ok { return version, nil } if fingerprintDPCplus(cartload) { return "DPC+", nil } if fingerprintSuperchargerFastLoad(cartload) { return "AR", nil } if fingerprint3ePlus(cartload) { return "3E+", nil } if fingerprint3e(cartload) { return "3E", nil } switch cartload.Size() { case 4096: return "4K", nil case 8195: // a widely distributed bad ROM dump of the Pink Panther prototype is // 8195 bytes long. we'll treat it like an 8k ROM and see if it's // recognised as a Wickstead Design ROM. if it's not then it's just a // file that's 8195 bytes long and will be rejected fallthrough case 8192: return fingerprint8k(cartload), nil case 10240: fallthrough case 10495: return "DPC", nil case 12288: return "FA", nil case 16384: return fingerprint16k(cartload), nil case 32768: return fingerprint32k(cartload), nil case 65536: return fingerprint64k(cartload), nil case 131072: return fingerprint128k(cartload), nil case 262144: return fingerprint256k(cartload), nil } if cartload.Size() >= 4096 { return "", fmt.Errorf("unrecognised size (%d bytes)", cartload.Size()) } return "2K", nil }