Skip to content

Instantly share code, notes, and snippets.

@tomdaley92
Last active September 27, 2024 10:35
Show Gist options
  • Save tomdaley92/a78c4d460283af4893f7aad878218e5f to your computer and use it in GitHub Desktop.
Save tomdaley92/a78c4d460283af4893f7aad878218e5f to your computer and use it in GitHub Desktop.
Gameboy Cartridge Parser. Optimized by using a few O(1) lookup tables
#include "Cartridge.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Cartridge::Cartridge() {
rom_loaded = 0;
}
Cartridge::~Cartridge() {
free (rom_name);
free(rom);
}
int Cartridge::Load(char *filename) {
FILE *file;
file = fopen(filename, "rb");
if(file == NULL){
fprintf(stderr, "Error opening file.\n");
return 1;
}
/* Jump to the end of the file */
fseek(file, 0, SEEK_END);
/* Get the current byte offset in the file */
rom_len = ftell(file);
/* Jump back to the beginning of the file */
rewind(file);
if (rom_len > MAX_SIZE) {
printf("File is too large or not formatted properly.\n");
return 1;
}
/* Allocate memory for rom file */
this->rom = (unsigned char *) malloc(rom_len);
if (!rom) {
fprintf(stderr, "Unable to allocate memory for rom.\n");
return 1;
}
/* Copy the entire rom file to memory */
if (!fread(rom, 1, rom_len, file)) {
fprintf(stderr, "Error reading file.\n");
return 1;
}
fclose(file);
char *base_name;
char *temp = strtok (filename, "\\/");
while (temp != NULL) {
base_name = temp;
temp = strtok(NULL, "\\/");
}
this->rom_name = (char *) malloc(strlen(base_name) + 1);
if (!rom_name) {
fprintf(stderr, "Unable to allocate memory for rom name.\n");
return 1;
}
memset(this->rom_name, '\0', strlen(base_name) + 1);
memcpy(this->rom_name, base_name, strlen(base_name));
ParseHeader();
CalculateChecksums();
rom_loaded = 1;
return 0;
}
void Cartridge::ParseHeader() {
int offset = HEADER_OFFSET;
/* Grab the title while ignoring any NULL bytes */
memset(title, '\0', 17);
int k = 0;
for (int i = 0; i < 16; i++) {
if (rom[offset+i] > 0x1F && rom[offset+i] < 0x7F) {
title[k] = rom[offset+i];
k++;
}
}
offset += 15;
/* Grab the CGB flag from the end of the title */
cgb_flag = rom[offset];
if (cgb_flag == 0xC0 || cgb_flag == 0x80) title[15] = '\0';
offset++;
/* Grab new licensee code */
memset(new_licensee_code, '\0', 3);
memcpy(new_licensee_code, rom+offset, 2);
offset += 2;
/* Grab all the other single byte flags */
sgb_flag = rom[offset];
cartridge_type = rom[++offset];
rom_size = rom[++offset];
ram_size = rom[++offset];
country_code = rom[++offset];
old_licensee_code = rom[++offset];
rom_version_number = rom[++offset];
/* Grab the two checksums */
header_checksum = rom[++offset];
global_checksum = rom[++offset] << 8 | rom[offset + 1];
}
void Cartridge::CalculateChecksums() {
calculated_header_checksum = 0;
calculated_global_checksum = 0;
for (int i = HEADER_OFFSET; i <= 0x014C; i++)
calculated_header_checksum -= rom[i] + 1;
for (int i = 0; i < 0x014E; i++)
calculated_global_checksum += rom[i];
for (int i = 0x0150; i < rom_len; i++)
calculated_global_checksum += rom[i];
}
void Cartridge::DisplayInfo() {
if (!rom_loaded) return;
const char *checksum_warning =
"\nWarning: failed to verify one or more checksums! \n"
"This ROM file might be corrupt, dumped \n"
"incorrectly, or was modified (un)intentionally.";
const char *rom_size_note =
"\nNote: when using a MBC2 chip, 0x00 (None) must \n"
"be specified as the RAM size, even though the \n"
"MBC2 includes a built-in RAM of 512 x 4 bits.";
/* Displays all the header fields/flags in a human readable format */
fprintf(stdout, "------------------- ROM INFO -------------------\n");
fprintf(stdout, "%s\n", rom_name);
fprintf(stdout, "Size: %d ", rom_len);
rom_len/MiB > 0 ?
fprintf(stdout, "(%d MiB)\n", rom_len/MiB):
fprintf(stdout, "(%d KiB)\n", rom_len/KiB);
fprintf(stdout, "\nTitle %s\n", title);
fprintf(stdout, "CGB Flag 0x%02X ", cgb_flag);
if (cgb_flag == 0x80) fprintf(stdout, "(Supports CGB)\n");
else if (cgb_flag == 0xC0) fprintf(stdout, "(CGB Only)\n");
else fprintf(stdout, "(No CGB Support)\n");
fprintf(stdout, "SGB Flag 0x%02X ", sgb_flag);
if (sgb_flag == 0x03) fprintf(stdout, "(Supports SGB)\n");
else fprintf(stdout, "(No SGB Support)\n");
fprintf(stdout, "Cartridge Type %s\n",
cart_type_map[cartridge_type % SIZE_CART_TYPES]);
fprintf(stdout, "ROM Size %s\n",
rom_size_map[rom_size % NUM_ROM_SIZES]);
fprintf(stdout, "RAM Size %s\n",
ram_size_map[ram_size % NUM_RAM_SIZES]);
fprintf(stdout, "Destination ");
country_code ?
fprintf(stdout, "NON-JAPAN\n"):
fprintf(stdout, "JAPAN\n");
unsigned char new_licensee_index = atoi((const char *)new_licensee_code);
old_licensee_code == 0x33 ?
fprintf(stdout, "New Licensee %s\n",
new_licensee_map[new_licensee_index % SIZE_NEW_LICENSEES]):
fprintf(stdout, "Old Licensee %s\n",
old_licensee_map[old_licensee_code % SIZE_OLD_LICENSEES]);
fprintf(stdout, "ROM Version 0x%02X\n", rom_version_number);
fprintf(stdout, "Header Checksum 0x%02X ", header_checksum);
calculated_header_checksum == header_checksum ?
fprintf(stdout, "(Pass)\n"):
fprintf(stdout, "(Fail)\n");
fprintf(stdout, "Global Checksum 0x%04X ", global_checksum);
calculated_global_checksum == global_checksum ?
fprintf(stdout, "(Pass)\n"):
fprintf(stdout, "(Fail)\n");
if(!ram_size && (cartridge_type == 0x05 || cartridge_type == 0x06))
fprintf(stdout, "%s\n", rom_size_note);
if (calculated_header_checksum != header_checksum ||
calculated_global_checksum != global_checksum) {
fprintf(stdout, "%s\n", checksum_warning);
}
}
#ifndef CARTRIDGE_H
#define CARTRIDGE_H
#define KiB 1024
#define MiB 1048576
/* 8 MiB - biggest known GBC ROM */
#define MAX_SIZE 8388608
/* 16 KiB bank slots */
#define BANK_SIZE 16384
/* Rom Header entry point */
#define HEADER_OFFSET 0x0134
/* Sizes used for info maps */
#define SIZE_CART_TYPES 0x100
#define SIZE_NEW_LICENSEES 0x64
#define SIZE_OLD_LICENSEES 0x100
#define NUM_ROM_SIZES 0x09
#define NUM_RAM_SIZES 0x06
class Cartridge {
private:
const char *cart_type_map[SIZE_CART_TYPES] = {
"ROM_ONLY", "MBC1", "MBC1 + RAM", "MBC1 + RAM + BATTERY", "\0", "MBC2", "MBC2 + RAM + BATTERY", "\0",
"ROM + RAM", "ROM + RAM + BATTERY", "\0", "MMM01", "MMM01 + RAM", "MMM01 + RAM + BATTERY", "\0", "MBC3 + TIMER + BATTERY",
"MBC3 + RAM + TIMER + BATTERY", "MBC3", "MBC3 + RAM", "MBC3 + RAM + BATTERY", "\0", "\0", "\0", "\0",
"\0", "MBC5", "MBC5 + RAM", "MBC5 + RAM + BATTERY", "MBC5 + RUMBLE", "MBC5 + RAM + RUMBLE", "MBC5 + RAM + BATTERY + RUMBLE", "\0",
"MBC6 + RAM + BATTERY", "\0", "MBC7 + RAM + BATTERY + ACCELEROMETER", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "POCKET CAMERA", "BANDAI TAMA5", "HUC3", "HUC1 + RAM + BATTERY"
};
const char *new_licensee_map[SIZE_NEW_LICENSEES] = {
"NONE", "NINTENDO", "\0", "\0", "\0",
"\0", "\0", "\0", "CAPCOM", "\0",
"\0", "\0", "\0", "ELECTRONIC ARTS", "\0",
"\0", "\0", "\0", "HUDSONSOFT", "B-AI",
"KSS", "\0", "POW", "\0", "PCM COMPLETE",
"SAN-X", "\0", "\0", "KEMCO JAPAN", "SETA",
"VIACOM", "NINTENDO", "BANDIA", "OCEAN/ACCLAIM", "KONAMI",
"HECTOR", "\0", "TAITO", "HUDSON", "BANPRESTO",
"\0", "UBI SOFT", "ATLUS", "\0", "MALIBU",
"\0", "ANGEL", "BULLET-PROOF", "\0", "IREM",
"ABSOLUTE", "ACCLAIM", "ACTIVISION", "AMERICAN SAMMY", "KONAMI",
"HI TECH ENTERTAINMENT", "LJN", "MATCHBOX", "MATTEL", "MILTON BRADLEY",
"TITUS", "VIRGIN", "\0", "\0", "LUCASARTS",
"\0", "\0", "OCEAN", "\0", "ELECTRONIC ARTS",
"INFOGRAMES", "INTERPLAY", "BRODERBUND", "SCULPTURED", "\0",
"SCI", "\0", "\0", "T*HQ", "ACCOLADE",
"MISAWA", "\0", "\0", "LOZC", "\0",
"\0", "TOKUMA SHOTEN I*", "TSUKUDA ORI*", "\0", "\0",
"\0", "CHUN SOFT", "VIDEO SYSTEM", "OCEAN/ACCLAIM", "\0",
"VARIE", "YONEZAWA/S'PAL", "KANEKO", "\0", "PACK IN SOFT"
};
const char *old_licensee_map[SIZE_OLD_LICENSEES] = {
"NONE", "NINTENDO", "\0", "\0", "\0", "\0", "\0", "\0",
"CAPCOM", "HOT-B", "JALECO", "COCONUTS", "EILTE SYSTEMS", "\0", "\0", "\0",
"\0", "\0", "\0", "ELECTRONIC ARTS", "\0", "\0", "\0", "\0",
"HUDSONSOFT", "ITC ENTERTAINMENT", "YANOMAN", "\0", "\0", "CLARY", "\0", "VIRGIN",
"\0", "\0", "\0", "\0", "PCM COMPLETE", "SAN-X", "\0", "\0",
"KOTOBUKI SYSTEMS", "SETA", "\0", "\0", "\0", "\0", "\0", "\0",
"INFOGRAMES", "NINTENDO", "BANDAI", "SEE NEW LICENSE CODE", "KONAMI", "HECTOR", "\0", "\0",
"CAPCOM", "BANPRESTO", "\0", "\0", "*ENTERTAINMENT I", "\0", "GREMLIN", "\0",
"\0", "UBI SOFT", "ATLUS", "\0", "MALIBU", "\0", "ANGEL", "SPECTRUM HOLOBY",
"\0", "IREM", "VIRGIN", "\0", "\0", "MALIBU", "\0", "U.S. GOLD",
"ABSOLUTE", "ACCLAIM", "ACTIVISION", "AMERICAN SAMMY", "GAMETEK", "PARK PLACE", "LJN", "MATCHBOX",
"\0", "MILTON BRADLEY", "MINDSCAPE", "ROMSTAR", "NAXAT SOFT", "TRADEWEST", "\0", "\0",
"TITUS", "VIRGIN", "\0", "\0", "\0", "\0", "\0", "OCEAN",
"\0", "ELECTRONIC ARTS", "\0", "\0", "\0", "\0", "ELITE SYSTEMS", "ELECTRO BRAIN",
"INFOGRAMES", "INTERPLAY", "BRODERBUND", "SCULPTURED SOFT", "\0", "THE SALES CURVE", "\0", "\0",
"T*HQ", "ACCOLADE", "TRIFFIX ENTERTAINMENT", "\0", "MICROPROSE", "\0", "\0", "KEMCO",
"MISAWA ENTERTAINMENT", "\0", "\0", "LOZC", "\0", "\0", "*TOKUMA SHOTEN I", "\0",
"\0", "\0", "\0", "BULLET_PROOF SOFTWARE", "VIC TOKAI", "\0", "APE", "I'MAX",
"\0", "CHUN SOFT", "VIDEO SYSTEM", "TSUBURAVA", "\0", "VARIE", "YONEZAWA/S'PAL", "KANEKO",
"\0", "ARC", "NIHON BUSSAN", "TECMO", "IMAGINEER", "BANPRESTO", "\0", "NOVA",
"\0", "HORI ELECTRIC", "BANDAI", "\0", "KONAMI", "\0", "KAWADA", "TAKARA",
"\0", "TECHNOS JAPAN", "BRODERBUND", "\0", "TOEI ANIMATION", "TOHO", "\0", "NAMCO",
"ACCLAIM", "ASCII OR NEXOFT", "BANDAI", "\0", "ENIX", "\0", "HAL", "SNK",
"\0", "PONY CANYON", "*CULTURE BRAIN O", "SUNSOFT", "\0", "SONY IMAGESOFT", "\0", "SAMMY",
"TAITO", "\0", "KEMCO", "SQUARESOFT", "*TOKUMA SHOTEN I", "DATA EAST", "TONKIN HOUSE", "\0",
"KOEI", "UFL", "ULTRA", "VAP", "USE", "MELDAC", "*PONY CANYON OR", "ANGEL",
"TAITO", "SOFEL", "QUEST", "SIGMA ENTERPRISES", "ASK KODANSHA", "\0", "NAXAT SOFT", "COPYA SYSTEMS",
"\0", "BANPRESTO", "TOMY", "LJN", "\0", "NCS", "HUMAN", "ALTRON",
"JALECO", "TOWACHIKI", "UUTAKA", "VARIE", "\0", "EPOCH", "\0", "ATHENA",
"ASMIK", "NATSUME", "KING RECORDS", "ATLUS", "EPIC/SONY RECORDS", "\0", "IGS", "\0",
"A WAVE", "\0", "\0", "EXTREME ENTERTAINMENT", "\0", "\0", "\0", "\0",
"\0", "\0", "\0", "\0", "\0", "\0", "\0", "LJN"
};
const char *rom_size_map[NUM_ROM_SIZES] = {
"32 KiB (2 banks)",
"64 KiB (4 banks)",
"128 KiB (8 banks)",
"256 KiB (16 banks)",
"512 KiB (32 banks)",
"1 MiB (64 banks)", // only 63 banks used by MBC1
"2 MiB (128 banks)", // only 125 banks used by MBC1
"4 MiB (256 banks)",
"8 MiB (512 banks)"
};
const char *ram_size_map[NUM_RAM_SIZES] = {
"0 KiB (None)",
"2 KiB",
"8 KiB",
"32 KiB (4 banks of 8 KiB)",
"128 KiB (16 banks of 8 KiB)",
"64 KiB (8 banks of 8 KiB)"
};
/* Contains contents of loaded ROM file */
unsigned char *rom;
char *rom_name;
int rom_loaded;
long int rom_len;
/* Rom header fields below: */
char title[17];
/* In older cartridges this byte has been part of the title.
In CGB cartridges the upper bit is used to enable CGB functions.
This is required, otherwise the CGB switches itself into Non-CGB-Mode.
common values:
0x80 - Game supports CGB functions, but works on old gameboys also
0xC0 - Game works on CGB only (physically the same as 80h) */
unsigned char cgb_flag;
/* Specifies a two character ASCII licensee code,
indicating the company or publisher of the game.
These two bytes are used in newer games only
(games that have been released after the SGB has been invented).
Older games are using the header entry at 014B instead. */
unsigned char new_licensee_code[3];
/* Specifies whether the game supports SGB functions,
common values:
00h = No SGB functions (Normal Gameboy or CGB only game)
03h = Game supports SGB functions */
unsigned char sgb_flag;
/* Specifies MBC type, if any */
unsigned char cartridge_type;
/* Specifies the size of read only memory in cartridge */
unsigned char rom_size;
/* also known as save-ram (sram) size.
When using a MBC2 chip 00h must be specified in this entry,
even though the MBC2 includes a built-in RAM of 512 x 4 bits. */
unsigned char ram_size;
unsigned char country_code;
/* if this is equal to 0x33, use new licensee code
Super GameBoy functions won't work if != 33 */
unsigned char old_licensee_code;
/* Specifies the version number of the game. That is usually 00h. */
unsigned char rom_version_number;
/* Contains an 8-bit checksum across
cartridge header bytes 0x0134-0x014C
The checksum is calculated by:
x=0:FOR i=0134h TO 014Ch:x=x-MEM[i]-1:NEXT */
unsigned char header_checksum;
unsigned char calculated_header_checksum;
/* Contains an 16-bit checksum (upper byte first)
across the whole cartridge rom. Produced by
adding all bytes of the cartridge (except for
the two checksum bytes). The Gameboy doesn't
verify this checksum. */
unsigned short global_checksum;
unsigned short calculated_global_checksum;
void ParseHeader();
void CalculateChecksums();
public:
Cartridge();
~Cartridge();
int Load(char *filename);
void DisplayInfo();
};
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment