From a9cf4494aa83545b00aa983b295af7d1b243dce2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 4 Aug 2025 13:56:54 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + gb-player.go | 379 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 + go.sum | 0 4 files changed, 384 insertions(+) create mode 100644 .gitignore create mode 100644 gb-player.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ce9c93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +rom.gb + diff --git a/gb-player.go b/gb-player.go new file mode 100644 index 0000000..65d54c1 --- /dev/null +++ b/gb-player.go @@ -0,0 +1,379 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "os" +) + +func main() { + var count int + + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo + expected_logo := []byte{ + 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, + 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, + 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, + 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, + 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, + 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, + } + + // Cartridge types + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type + cartridge_types := map[byte]string{ + 0x00: "ROM ONLY", + 0x01: "MBC1", + 0x02: "MBC1+RAM", + 0x03: "MBC1+RAM+BATTERY", + 0x05: "MBC2", + 0x06: "MBC2+BATTERY", + 0x08: "ROM+RAM 9", + 0x09: "ROM+RAM+BATTERY 9", + 0x0B: "MMM01", + 0x0C: "MMM01+RAM", + 0x0D: "MMM01+RAM+BATTERY", + 0x0F: "MBC3+TIMER+BATTERY", + 0x10: "MBC3+TIMER+RAM+BATTERY 10", + 0x11: "MBC3", + 0x12: "MBC3+RAM 10", + 0x13: "MBC3+RAM+BATTERY 10", + 0x19: "MBC5", + 0x1A: "MBC5+RAM", + 0x1B: "MBC5+RAM+BATTERY", + 0x1C: "MBC5+RUMBLE", + 0x1D: "MBC5+RUMBLE+RAM", + 0x1E: "MBC5+RUMBLE+RAM+BATTERY", + 0x20: "MBC6", + 0x22: "MBC7+SENSOR+RUMBLE+RAM+BATTERY", + 0xFC: "POCKET CAMERA", + 0xFD: "BANDAI TAMA5", + 0xFE: "HuC3", + 0xFF: "HuC1+RAM+BATTERY", + } + + // RAM sizes + // https://gbdev.io/pandocs/The_Cartridge_Header.html#0149--ram-size + ram_sizes := map[byte]string{ + 0x00: "0 - No RAM ", + 0x01: "UNUSED VALUE", + 0x02: "8 KiB - 1 bank", + 0x03: "32 KiB - 4 banks of 8 KiB each", + 0x04: "128 KiB 16 banks of 8 KiB each", + 0x05: "64 KiB 8 banks of 8 KiB each", + } + + // Old licensees + // https://gbdev.io/pandocs/The_Cartridge_Header.html#014b--old-licensee-code + old_licensees := map[byte]string{ + 0x00: "None", + 0x01: "Nintendo", + 0x08: "Capcom", + 0x09: "HOT-B", + 0x0A: "Jaleco", + 0x0B: "Coconuts Japan", + 0x0C: "Elite Systems", + 0x13: "EA (Electronic Arts)", + 0x18: "Hudson Soft", + 0x19: "ITC Entertainment", + 0x1A: "Yanoman", + 0x1D: "Japan Clary", + 0x1F: "Virgin Games Ltd.3", + 0x24: "PCM Complete", + 0x25: "San-X", + 0x28: "Kemco", + 0x29: "SETA Corporation", + 0x30: "Infogrames5", + 0x31: "Nintendo", + 0x32: "Bandai", + 0x33: "Indicates that the New licensee code should be used instead.", + 0x34: "Konami", + 0x35: "HectorSoft", + 0x38: "Capcom", + 0x39: "Banpresto", + 0x3C: "Entertainment Interactive (stub)", + 0x3E: "Gremlin", + 0x41: "Ubi Soft1", + 0x42: "Atlus", + 0x44: "Malibu Interactive", + 0x46: "Angel", + 0x47: "Spectrum HoloByte", + 0x49: "Irem", + 0x4A: "Virgin Games Ltd.3", + 0x4D: "Malibu Interactive", + 0x4F: "U.S. Gold", + 0x50: "Absolute", + 0x51: "Acclaim Entertainment", + 0x52: "Activision", + 0x53: "Sammy USA Corporation", + 0x54: "GameTek", + 0x55: "Park Place13", + 0x56: "LJN", + 0x57: "Matchbox", + 0x59: "Milton Bradley Company", + 0x5A: "Mindscape", + 0x5B: "Romstar", + 0x5C: "Naxat Soft14", + 0x5D: "Tradewest", + 0x60: "Titus Interactive", + 0x61: "Virgin Games Ltd.3", + 0x67: "Ocean Software", + 0x69: "EA (Electronic Arts)", + 0x6E: "Elite Systems", + 0x6F: "Electro Brain", + 0x70: "Infogrames5", + 0x71: "Interplay Entertainment", + 0x72: "Broderbund", + 0x73: "Sculptured Software6", + 0x75: "The Sales Curve Limited7", + 0x78: "THQ", + 0x79: "Accolade15", + 0x7A: "Triffix Entertainment", + 0x7C: "MicroProse", + 0x7F: "Kemco", + 0x80: "Misawa Entertainment", + 0x83: "LOZC G.", + 0x86: "Tokuma Shoten", + 0x8B: "Bullet-Proof Software2", + 0x8C: "Vic Tokai Corp.16", + 0x8E: "Ape Inc.17", + 0x8F: "I’Max18", + 0x91: "Chunsoft Co.8", + 0x92: "Video System", + 0x93: "Tsubaraya Productions", + 0x95: "Varie", + 0x96: "Yonezawa19/S’Pal", + 0x97: "Kemco", + 0x99: "Arc", + 0x9A: "Nihon Bussan", + 0x9B: "Tecmo", + 0x9C: "Imagineer", + 0x9D: "Banpresto", + 0x9F: "Nova", + 0xA1: "Hori Electric", + 0xA2: "Bandai", + 0xA4: "Konami", + 0xA6: "Kawada", + 0xA7: "Takara", + 0xA9: "Technos Japan", + 0xAA: "Broderbund", + 0xAC: "Toei Animation", + 0xAD: "Toho", + 0xAF: "Namco", + 0xB0: "Acclaim Entertainment", + 0xB1: "ASCII Corporation or Nexsoft", + 0xB2: "Bandai", + 0xB4: "Square Enix", + 0xB6: "HAL Laboratory", + 0xB7: "SNK", + 0xB9: "Pony Canyon", + 0xBA: "Culture Brain", + 0xBB: "Sunsoft", + 0xBD: "Sony Imagesoft", + 0xBF: "Sammy Corporation", + 0xC0: "Taito", + 0xC2: "Kemco", + 0xC3: "Square", + 0xC4: "Tokuma Shoten", + 0xC5: "Data East", + 0xC6: "Tonkin House", + 0xC8: "Koei", + 0xC9: "UFL", + 0xCA: "Ultra Games", + 0xCB: "VAP, Inc.", + 0xCC: "Use Corporation", + 0xCD: "Meldac", + 0xCE: "Pony Canyon", + 0xCF: "Angel", + 0xD0: "Taito", + 0xD1: "SOFEL (Software Engineering Lab)", + 0xD2: "Quest", + 0xD3: "Sigma Enterprises", + 0xD4: "ASK Kodansha Co.", + 0xD6: "Naxat Soft14", + 0xD7: "Copya System", + 0xD9: "Banpresto", + 0xDA: "Tomy", + 0xDB: "LJN", + 0xDD: "Nippon Computer Systems", + 0xDE: "Human Ent.", + 0xDF: "Altron", + 0xE0: "Jaleco", + 0xE1: "Towa Chiki", + 0xE2: "Yutaka # Needs more info", + 0xE3: "Varie", + 0xE5: "Epoch", + 0xE7: "Athena", + 0xE8: "Asmik Ace Entertainment", + 0xE9: "Natsume", + 0xEA: "King Records", + 0xEB: "Atlus", + 0xEC: "Epic/Sony Records", + 0xEE: "IGS", + 0xF0: "A Wave", + 0xF3: "Extreme Entertainment", + 0xFF: "LJN", + } + + file, err := os.Open("rom.gb") + if err != nil { + log.Fatal(err) + } + defer file.Close() + + fmt.Printf("Loading ROM file \"%s\"\n", file.Name()) + + _, err = file.Seek(0X100, 0) + if err != nil { + log.Fatal(err) + } + + entrypoint := make([]byte, 4) + count, err = file.Read(entrypoint) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read entrypoint instructions: %X\n\n", entrypoint[:count]) + + fmt.Println("Reading header") + + logo := make([]byte, 48) + count, err = file.Read(logo) + if err != nil { + log.Fatal(err) + } + + if bytes.Equal(logo, expected_logo) { + fmt.Println("Valid logo found") + } else { + log.Fatal("Invalid ROM file: No valid logo found!") + } + + // NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges. + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#0134-0143--title + raw_title := make([]byte, 16) + count, err = file.Read(raw_title) + if err != nil { + log.Fatal(err) + } + + // Strip padding bytes + title := bytes.ReplaceAll(raw_title, []byte{0x00}, nil) + + fmt.Printf("Read title: %s\n", title) + + new_licensee := make([]byte, 2) + count, err = file.Read(new_licensee) + if err != nil { + log.Fatal(err) + } + + if bytes.Equal(new_licensee, []byte{0x00, 0x00}) { + fmt.Println("Read new licensee: N/A") + } else { + fmt.Printf("Read new licensee: %q\n", new_licensee[:count]) + } + + sgb_flag := make([]byte, 1) + count, err = file.Read(sgb_flag) + if err != nil { + log.Fatal(err) + } + + if bytes.Equal(sgb_flag, []byte{0x03}) { + fmt.Println("Read SGB flag: SGB support") + } else { + fmt.Println("Read SGB flag: No SGB support") + } + + cartridge_type := make([]byte, 1) + count, err = file.Read(cartridge_type) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read cartridge type: %s\n", cartridge_types[cartridge_type[0]]) + + raw_rom_size := make([]byte, 1) + count, err = file.Read(raw_rom_size) + if err != nil { + log.Fatal(err) + } + + rom_size := 32 * (1 << raw_rom_size[0]) + + fmt.Printf("Read ROM size: %d bytes\n", rom_size) + + + ram_size := make([]byte, 1) + count, err = file.Read(ram_size) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read RAM size: %s\n", ram_sizes[ram_size[0]]) + + destination_code := make([]byte, 1) + count, err = file.Read(destination_code) + if err != nil { + log.Fatal(err) + } + + if bytes.Equal(destination_code, []byte{0x00}) { + fmt.Println("Read destination code: Japan (and possibly overseas)") + } else if bytes.Equal(destination_code, []byte{0x01}) { + fmt.Println("Read destination code: Overseas only") + } else { + fmt.Println("Read destination code: Invalid value") + } + + old_licensee := make([]byte, 1) + count, err = file.Read(old_licensee) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read old licensee: %s\n", old_licensees[old_licensee[0]]) + + rom_version := make([]byte, 1) + count, err = file.Read(rom_version) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read Mask ROM version number: %X\n", rom_version[:count]) + + header_checksum := make([]byte, 1) + count, err = file.Read(header_checksum) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read header checksum: %X\n", header_checksum[:count]) + // FIXME(m): Calculate and validate checksum! + + global_checksum := make([]byte, 2) + count, err = file.Read(global_checksum) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Read global checksum: %X\n", global_checksum[:count]) + // NOTE(m): Not used, except by one emulator. + // See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum + + fmt.Printf("Finished reading header\n\n") + + printCurrentPosition(file) +} + +func printCurrentPosition(file *os.File) { + pos, err := file.Seek(0, io.SeekCurrent) + if err != nil { + panic(err) + } + fmt.Printf("Current position: 0x%X\n", pos) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2e62b08 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module example/gb-player + +go 1.24.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29