const TITLE_START: usize = 0x34;
const TITLE_END: usize = 0x43;
const FOURTH_LETTER_OFFSET: usize = 0x37;
const NEW_LICENSEE_HIGH: usize = 0x44;
const NEW_LICENSEE_LOW: usize = 0x45;
const OLD_LICENSEE: usize = 0x4B;
const FIRST_DUPLICATE_INDEX: usize = 65;
#[rustfmt::skip]
const TITLE_CHECKSUMS: [u8; 79] = [
0x00, 0x88, 0x16, 0x36, 0xD1, 0xDB, 0xF2, 0x3C, 0x8C, 0x92, 0x3D, 0x5C, 0x58, 0xC9, 0x3E, 0x70, 0x1D, 0x59, 0x69, 0x19, 0x35, 0xA8, 0x14, 0xAA, 0x75, 0x95, 0x99, 0x34, 0x6F, 0x15, 0xFF, 0x97, 0x4B, 0x90, 0x17, 0x10, 0x39, 0xF7, 0xF6, 0xA2, 0x49, 0x4E, 0x43, 0x68, 0xE0, 0x8B, 0xF0, 0xCE, 0x0C, 0x29, 0xE8, 0xB7, 0x86, 0x9A, 0x52, 0x01, 0x9D, 0x71, 0x9C, 0xBD, 0x5D, 0x6D, 0x67, 0x3F, 0x6B, 0xB3, 0x46, 0x28, 0xA5, 0xC6, 0xD3, 0x27, 0x61, 0x18, 0x66, 0x6A, 0xBF, 0x0D, 0xF4, ];
#[rustfmt::skip]
const FOURTH_LETTER_TIEBREAKER: [u8; 29] = *b"BEFAARBEKEK R-URAR INAILICE R";
#[rustfmt::skip]
const PALETTE_PER_CHECKSUM: [u8; 94] = [
0, 4, 5, 35, 34, 3, 31, 15, 10, 5, 19, 36, 7, 37, 30, 44, 21, 32, 31, 20, 5, 33, 13, 14, 5, 29, 5, 18, 9, 3, 2, 26, 25, 25, 41, 42, 26, 45, 42, 45, 36, 38, 26, 42, 30, 41, 34, 34, 5, 42, 6, 5, 33, 25, 42, 42, 40, 2, 16, 25, 42, 42, 5, 0, 39, 36, 22, 25, 6, 32, 12, 36, 11, 39, 18, 39, 24, 31, 50, 17, 46, 6, 27, 0, 47, 41, 41, 0, 0, 19, 34, 23, 18, 29, ];
#[rustfmt::skip]
const PALETTE_COMBINATIONS: [[u8; 3]; 55] = [
[ 4, 4, 29], [18, 18, 18], [20, 20, 20], [24, 24, 24], [ 9, 9, 9], [ 0, 0, 0], [27, 27, 27], [ 5, 5, 5], [12, 12, 12], [26, 26, 26], [16, 8, 8], [ 4, 28, 28], [ 4, 2, 2], [ 3, 4, 4], [ 4, 29, 29], [28, 4, 28], [ 2, 17, 2], [16, 16, 8], [ 4, 4, 7], [ 4, 4, 18], [ 4, 4, 20], [19, 19, 9], [15, 15, 11], [17, 17, 2], [ 4, 4, 2], [ 4, 4, 3], [28, 28, 0], [ 3, 3, 0], [ 0, 0, 1], [18, 22, 18], [20, 22, 20], [24, 22, 24], [16, 22, 8], [17, 4, 13], [27, 0, 14], [27, 4, 15], [19, 23, 9], [16, 28, 10], [ 4, 23, 28], [17, 22, 2], [ 4, 0, 2], [ 4, 28, 3], [28, 3, 0], [ 3, 28, 4], [21, 28, 4], [ 3, 28, 0], [25, 3, 28], [ 0, 28, 8], [ 4, 3, 28], [28, 3, 6], [ 4, 28, 29], [30, 30, 30], [31, 31, 31], [28, 4, 1], [ 0, 0, 2], ];
#[rustfmt::skip]
const PALETTES: [[u16; 4]; 32] = [
[0x7FFF, 0x32BF, 0x00D0, 0x0000], [0x639F, 0x4279, 0x15B0, 0x04CB], [0x7FFF, 0x6E31, 0x454A, 0x0000], [0x7FFF, 0x1BEF, 0x0200, 0x0000], [0x7FFF, 0x421F, 0x1CF2, 0x0000], [0x7FFF, 0x5294, 0x294A, 0x0000], [0x7FFF, 0x03FF, 0x012F, 0x0000], [0x7FFF, 0x03EF, 0x01D6, 0x0000], [0x7FFF, 0x42B5, 0x3DC8, 0x0000], [0x7E74, 0x03FF, 0x0180, 0x0000], [0x67FF, 0x77AC, 0x1A13, 0x2D6B], [0x7ED6, 0x4BFF, 0x2175, 0x0000], [0x53FF, 0x4A5F, 0x7E52, 0x0000], [0x4FFF, 0x7ED2, 0x3A4C, 0x1CE0], [0x03ED, 0x7FFF, 0x255F, 0x0000], [0x036A, 0x021F, 0x03FF, 0x7FFF], [0x7FFF, 0x01DF, 0x0112, 0x0000], [0x231F, 0x035F, 0x00F2, 0x0009], [0x7FFF, 0x03EA, 0x011F, 0x0000], [0x299F, 0x001A, 0x000C, 0x0000], [0x7FFF, 0x027F, 0x001F, 0x0000], [0x7FFF, 0x03E0, 0x0206, 0x0120], [0x7FFF, 0x7EEB, 0x001F, 0x7C00], [0x7FFF, 0x3FFF, 0x7E00, 0x001F], [0x7FFF, 0x03FF, 0x001F, 0x0000], [0x03FF, 0x001F, 0x000C, 0x0000], [0x7FFF, 0x033F, 0x0193, 0x0000], [0x0000, 0x4200, 0x037F, 0x7FFF], [0x7FFF, 0x7E8C, 0x7C00, 0x0000], [0x7FFF, 0x1BEF, 0x6180, 0x0000], [0x7FFF, 0x7FEA, 0x7D5F, 0x0000], [0x4778, 0x3290, 0x1D87, 0x0861], ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DmgCompatPalette {
pub bg0: [u16; 4],
pub obj0: [u16; 4],
pub obj1: [u16; 4],
}
impl Default for DmgCompatPalette {
fn default() -> Self {
Self {
bg0: PALETTES[0],
obj0: PALETTES[0],
obj1: PALETTES[0],
}
}
}
pub fn compute_title_checksum(header: &[u8]) -> u8 {
if header.len() <= TITLE_END {
return 0;
}
header[TITLE_START..=TITLE_END]
.iter()
.fold(0u8, |acc, &b| acc.wrapping_add(b))
}
pub fn is_nintendo_licensee(header: &[u8]) -> bool {
if header.len() <= OLD_LICENSEE {
return false;
}
if header[OLD_LICENSEE] == 0x01 {
return true;
}
if header[OLD_LICENSEE] == 0x33 && header.len() > NEW_LICENSEE_LOW {
return header[NEW_LICENSEE_HIGH] == b'0' && header[NEW_LICENSEE_LOW] == b'1';
}
false
}
pub fn get_palette_id(header: &[u8]) -> u8 {
let checksum = compute_title_checksum(header);
let index = TITLE_CHECKSUMS
.iter()
.position(|&c| c == checksum)
.unwrap_or(0);
let final_index = if index >= FIRST_DUPLICATE_INDEX {
resolve_duplicate(header, index)
} else {
index
};
if final_index < PALETTE_PER_CHECKSUM.len() {
PALETTE_PER_CHECKSUM[final_index] & 0x7F
} else {
0 }
}
fn resolve_duplicate(header: &[u8], initial_index: usize) -> usize {
if header.len() <= FOURTH_LETTER_OFFSET {
return initial_index;
}
let fourth_letter = header[FOURTH_LETTER_OFFSET];
let tiebreaker_offset = initial_index - FIRST_DUPLICATE_INDEX;
if tiebreaker_offset < FOURTH_LETTER_TIEBREAKER.len()
&& fourth_letter == FOURTH_LETTER_TIEBREAKER[tiebreaker_offset]
{
initial_index
} else {
initial_index + 14
}
}
pub fn get_palette_colors(header: &[u8]) -> DmgCompatPalette {
let palette_id = get_palette_id(header) as usize;
palette_colors_from_combination_id(palette_id)
}
fn palette_colors_from_combination_id(palette_id: usize) -> DmgCompatPalette {
if palette_id >= PALETTE_COMBINATIONS.len() {
return DmgCompatPalette::default();
}
let combination = PALETTE_COMBINATIONS[palette_id];
let palette_at = |idx: u8| PALETTES.get(idx as usize).copied().unwrap_or(PALETTES[0]);
DmgCompatPalette {
obj0: palette_at(combination[0]),
obj1: palette_at(combination[1]),
bg0: palette_at(combination[2]),
}
}
pub fn get_palette_colors_by_id(palette_id: u8) -> DmgCompatPalette {
palette_colors_from_combination_id(palette_id as usize)
}
pub fn button_combo_to_palette_id(buttons: u8) -> Option<u8> {
let a = buttons & 0x01 != 0;
let b = buttons & 0x02 != 0;
let up = buttons & 0x10 != 0;
let down = buttons & 0x20 != 0;
let left = buttons & 0x40 != 0;
let right = buttons & 0x80 != 0;
let dpad_count = up as u8 + down as u8 + left as u8 + right as u8;
if dpad_count != 1 {
return None;
}
if a && b {
return None;
}
match (up, down, left, right, a, b) {
(true, false, false, false, false, false) => Some(5), (true, false, false, false, true, false) => Some(43), (true, false, false, false, false, true) => Some(28), (false, true, false, false, false, false) => Some(8), (false, true, false, false, true, false) => Some(3), (false, true, false, false, false, true) => Some(49), (false, false, true, false, false, false) => Some(48), (false, false, true, false, true, false) => Some(40), (false, false, true, false, false, true) => Some(7), (false, false, false, true, false, false) => Some(1), (false, false, false, true, true, false) => Some(0), (false, false, false, true, false, true) => Some(6), _ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_header(title: &[u8; 16], old_licensee: u8) -> [u8; 0x4C] {
let mut header = [0u8; 0x4C];
header[TITLE_START..TITLE_START + 16].copy_from_slice(title);
header[OLD_LICENSEE] = old_licensee;
header
}
fn make_header_new_licensee(title: &[u8; 16], new_licensee: &[u8; 2]) -> [u8; 0x4C] {
let mut header = [0u8; 0x4C];
header[TITLE_START..TITLE_START + 16].copy_from_slice(title);
header[NEW_LICENSEE_HIGH] = new_licensee[0];
header[NEW_LICENSEE_LOW] = new_licensee[1];
header[OLD_LICENSEE] = 0x33; header
}
#[test]
fn test_compute_title_checksum_tetris() {
let title: [u8; 16] = *b"TETRIS\0\0\0\0\0\0\0\0\0\0";
let header = make_header(&title, 0x01);
let checksum = compute_title_checksum(&header);
assert_eq!(checksum, 0xDB, "TETRIS checksum should be 0xDB");
}
#[test]
fn test_compute_title_checksum_empty() {
let title: [u8; 16] = [0u8; 16];
let header = make_header(&title, 0x01);
assert_eq!(compute_title_checksum(&header), 0x00);
}
#[test]
fn test_is_nintendo_licensee_old() {
let title: [u8; 16] = [0u8; 16];
let header = make_header(&title, 0x01);
assert!(is_nintendo_licensee(&header));
}
#[test]
fn test_is_nintendo_licensee_new() {
let title: [u8; 16] = [0u8; 16];
let header = make_header_new_licensee(&title, b"01");
assert!(is_nintendo_licensee(&header));
}
#[test]
fn test_is_nintendo_licensee_non_nintendo() {
let title: [u8; 16] = [0u8; 16];
let header = make_header(&title, 0x00);
assert!(!is_nintendo_licensee(&header));
}
#[test]
fn test_get_palette_id_non_nintendo_uses_checksum_lookup() {
let title: [u8; 16] = [0u8; 16];
let header = make_header(&title, 0x00); assert_eq!(get_palette_id(&header), 0);
}
#[test]
fn test_get_palette_id_tetris() {
let title: [u8; 16] = *b"TETRIS\0\0\0\0\0\0\0\0\0\0";
let header = make_header(&title, 0x01);
assert_eq!(get_palette_id(&header), 3);
}
#[test]
fn test_get_palette_colors_non_nintendo_all_zero_title() {
let title: [u8; 16] = [0u8; 16];
let header = make_header(&title, 0x00);
let palette = get_palette_colors(&header);
assert_eq!(palette.bg0, PALETTES[29]);
assert_eq!(palette.obj0, PALETTES[4]);
assert_eq!(palette.obj1, PALETTES[4]);
}
#[test]
fn test_get_palette_colors_tetris() {
let title: [u8; 16] = *b"TETRIS\0\0\0\0\0\0\0\0\0\0";
let header = make_header(&title, 0x01);
let palette = get_palette_colors(&header);
assert_eq!(palette.bg0, PALETTES[24]);
assert_eq!(palette.obj0, PALETTES[24]);
assert_eq!(palette.obj1, PALETTES[24]);
}
#[test]
fn test_palette_combination_indices_in_bounds() {
for (i, combination) in PALETTE_COMBINATIONS.iter().enumerate() {
assert!(
(combination[0] as usize) < PALETTES.len(),
"OBJ0 index {} out of bounds in combination {}",
combination[0],
i
);
assert!(
(combination[1] as usize) < PALETTES.len(),
"OBJ1 index {} out of bounds in combination {}",
combination[1],
i
);
assert!(
(combination[2] as usize) < PALETTES.len(),
"BG index {} out of bounds in combination {}",
combination[2],
i
);
}
}
#[test]
fn test_palette_per_checksum_indices_in_bounds() {
for (i, &id) in PALETTE_PER_CHECKSUM.iter().enumerate() {
let masked = (id & 0x7F) as usize;
assert!(
masked < PALETTE_COMBINATIONS.len(),
"Palette ID {} (masked {}) out of bounds at index {}",
id,
masked,
i
);
}
}
#[test]
fn test_duplicate_checksum_first_variant() {
let title: [u8; 16] = *b"POKEMON BLUE\0\0\0\0";
let header = make_header(&title, 0x01);
let checksum = compute_title_checksum(&header);
assert_eq!(checksum, 0x61);
let palette_id = get_palette_id(&header);
assert_eq!(palette_id, 11);
}
#[test]
fn test_duplicate_checksum_second_variant() {
let title: [u8; 16] = *b"VEGAS STAKES\0\0\0\0";
let header = make_header(&title, 0x01);
let checksum = compute_title_checksum(&header);
assert_eq!(checksum, 0x61);
let palette_id = get_palette_id(&header);
assert_eq!(palette_id, 41);
}
#[test]
fn test_get_palette_colors_by_id_valid() {
let palette = get_palette_colors_by_id(5);
assert_eq!(palette.bg0, PALETTES[0]);
assert_eq!(palette.obj0, PALETTES[0]);
assert_eq!(palette.obj1, PALETTES[0]);
}
#[test]
fn test_get_palette_colors_by_id_out_of_bounds() {
let palette = get_palette_colors_by_id(255);
assert_eq!(palette, DmgCompatPalette::default());
}
#[test]
fn test_get_palette_colors_by_id_all_manual_palettes() {
let manual_ids = [5, 43, 28, 8, 3, 49, 48, 40, 7, 1, 0, 6];
for &id in &manual_ids {
let palette = get_palette_colors_by_id(id);
assert!(
palette.bg0[0] != 0
|| palette.bg0[1] != 0
|| palette.bg0[2] != 0
|| palette.bg0[3] != 0
);
}
}
#[test]
fn test_button_combo_up_only() {
assert_eq!(button_combo_to_palette_id(0x10), Some(5));
}
#[test]
fn test_button_combo_up_a() {
assert_eq!(button_combo_to_palette_id(0x11), Some(43));
}
#[test]
fn test_button_combo_up_b() {
assert_eq!(button_combo_to_palette_id(0x12), Some(28));
}
#[test]
fn test_button_combo_down_only() {
assert_eq!(button_combo_to_palette_id(0x20), Some(8));
}
#[test]
fn test_button_combo_down_a() {
assert_eq!(button_combo_to_palette_id(0x21), Some(3));
}
#[test]
fn test_button_combo_down_b() {
assert_eq!(button_combo_to_palette_id(0x22), Some(49));
}
#[test]
fn test_button_combo_left_only() {
assert_eq!(button_combo_to_palette_id(0x40), Some(48));
}
#[test]
fn test_button_combo_left_a() {
assert_eq!(button_combo_to_palette_id(0x41), Some(40));
}
#[test]
fn test_button_combo_left_b() {
assert_eq!(button_combo_to_palette_id(0x42), Some(7));
}
#[test]
fn test_button_combo_right_only() {
assert_eq!(button_combo_to_palette_id(0x80), Some(1));
}
#[test]
fn test_button_combo_right_a() {
assert_eq!(button_combo_to_palette_id(0x81), Some(0));
}
#[test]
fn test_button_combo_right_b() {
assert_eq!(button_combo_to_palette_id(0x82), Some(6));
}
#[test]
fn test_button_combo_no_dpad() {
assert_eq!(button_combo_to_palette_id(0x00), None);
assert_eq!(button_combo_to_palette_id(0x01), None); assert_eq!(button_combo_to_palette_id(0x02), None); assert_eq!(button_combo_to_palette_id(0x03), None); }
#[test]
fn test_button_combo_multiple_dpad() {
assert_eq!(button_combo_to_palette_id(0x30), None); assert_eq!(button_combo_to_palette_id(0xC0), None); assert_eq!(button_combo_to_palette_id(0x50), None); assert_eq!(button_combo_to_palette_id(0xF0), None); }
#[test]
fn test_button_combo_a_and_b() {
assert_eq!(button_combo_to_palette_id(0x13), None); assert_eq!(button_combo_to_palette_id(0x23), None); assert_eq!(button_combo_to_palette_id(0x43), None); assert_eq!(button_combo_to_palette_id(0x83), None); }
#[test]
fn test_button_combo_select_start_ignored() {
assert_eq!(button_combo_to_palette_id(0x14), Some(5));
assert_eq!(button_combo_to_palette_id(0x18), Some(5));
assert_eq!(button_combo_to_palette_id(0x1D), Some(43));
}
}