1use jgenesis_common::cheats::ByteCheatCodeU16Address; 2use regex::Regex; 3use std::sync::LazyLock; 4 5#[derive(Debug, Clone, Copy, PartialEq, Eq)] 6pub enum SmsGgCheatCodeType { 7 GameGenie, 8 ProActionReplay, 9} 10 11impl SmsGgCheatCodeType { 12 #[must_use] 13 pub fn decode(self, code: &str) -> Option<ByteCheatCodeU16Address> { 14 match self { 15 Self::GameGenie => decode_game_genie(code), 16 Self::ProActionReplay => decode_pro_action_replay(code), 17 } 18 } 19 20 #[must_use] 21 pub fn guess_from(code: &str) -> Option<Self> { 22 static GAME_GENIE_REGEX: LazyLock<Regex> = LazyLock::new(|| { 23 Regex::new(r"^[[:xdigit:]]{3}-[[:xdigit:]]{3}(-[[:xdigit:]]{3})?$").unwrap() 24 }); 25 26 static PRO_ACTION_REPLAY_REGEX: LazyLock<Regex> = 27 LazyLock::new(|| Regex::new(r"^..[[:xdigit:]]{2}[- ]?[[:xdigit:]]{4}$").unwrap()); 28 29 if GAME_GENIE_REGEX.is_match(code) { 30 return Some(Self::GameGenie); 31 } 32 33 if PRO_ACTION_REPLAY_REGEX.is_match(code) { 34 return Some(Self::ProActionReplay); 35 } 36 37 None 38 } 39} 40 41#[derive(Debug, Clone, PartialEq, Eq)] 42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 43#[cfg_attr(feature = "serde", serde(default))] 44pub struct SmsGgCheat { 45 pub name: String, 46 pub enabled: bool, 47 pub codes: Vec<String>, 48} 49 50impl Default for SmsGgCheat { 51 fn default() -> Self { 52 Self { name: String::new(), enabled: true, codes: vec![] } 53 } 54} 55 56impl SmsGgCheat { 57 #[must_use] 58 pub fn to_memory_override_vec(&self) -> Vec<ByteCheatCodeU16Address> { 59 if !self.enabled { 60 return vec![]; 61 } 62 63 self.codes 64 .iter() 65 .filter_map(|code| { 66 SmsGgCheatCodeType::guess_from(code).and_then(|code_type| code_type.decode(code)) 67 }) 68 .collect() 69 } 70} 71 72#[derive(Debug, Clone, PartialEq, Eq, Default)] 73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 74#[cfg_attr(feature = "serde", serde(default))] 75pub struct SmsGgCheats { 76 pub cheats: Vec<SmsGgCheat>, 77} 78 79impl SmsGgCheats { 80 #[must_use] 81 pub fn to_memory_override_vec(&self) -> Vec<ByteCheatCodeU16Address> { 82 self.cheats.iter().flat_map(SmsGgCheat::to_memory_override_vec).collect() 83 } 84} 85 86fn decode_game_genie(code: &str) -> Option<ByteCheatCodeU16Address> { 87 // Game Genie codes are in format DDA-AAA or DDA-AAA-RRR with hex chars, per: 88 // https://www.smspower.org/Development/GameGenie 89 // First 2 chars are value, next 4 chars are address (obfuscated), and last 3 chars are 90 // reference value (optional and obfuscated) 91 let bytes = code.as_bytes(); 92 93 if bytes.len() < 7 || bytes[3] != b'-' { 94 return None; 95 } 96 97 let has_reference = bytes.len() > 7; 98 if has_reference && (bytes.len() != 11 || bytes[7] != b'-') { 99 return None; 100 } 101 102 // Parse into a 36-bit value to simplify deobfuscation 103 let mut parsed = (u64::from_str_radix(code.get(..3)?, 16).ok()? << 24) 104 | (u64::from_str_radix(code.get(4..7)?, 16).ok()? << 12); 105 if has_reference { 106 parsed |= u64::from_str_radix(code.get(8..)?, 16).ok()?; 107 } 108 109 // Override value is first 2 chars, not obfuscated 110 let value = (parsed >> 28) as u8; 111 112 // Address is next 4 chars, obfuscated 113 // 16-bit value is rotated right by 4, then the highest 4 bits are inverted 114 let address = ((parsed >> 12) as u16).rotate_right(4) ^ 0xF000; 115 116 // Reference (if present) is last 3 chars, obfuscated 117 // First and third digits form an 8-bit value, that gets rotated right by 2, then XORed with 0xBA 118 // There's also a "cloak" value in here but that doesn't seem useful for emulation purposes 119 let reference = has_reference.then(|| { 120 let scrambled = (((parsed >> 4) & 0xF0) | (parsed & 0xF)) as u8; 121 scrambled.rotate_right(2) ^ 0xBA 122 }); 123 124 Some(ByteCheatCodeU16Address { address, value, reference }) 125} 126 127fn decode_pro_action_replay(code: &str) -> Option<ByteCheatCodeU16Address> { 128 // Pro Action Replay codes are in format "00AA AADD", per: 129 // https://www.smspower.org/Development/ProActionReplay 130 // Contains unobfuscated address and value, no support for reference values 131 132 let bytes = code.as_bytes(); 133 let valid_len = bytes.len() == 8 || (bytes.len() == 9 && matches!(bytes[4], b' ' | b'-')); 134 if !valid_len { 135 return None; 136 } 137 138 let (address, value) = if bytes.len() == 8 { 139 let address = u16::from_str_radix(code.get(2..6)?, 16).ok()?; 140 let value = u8::from_str_radix(code.get(6..)?, 16).ok()?; 141 (address, value) 142 } else { 143 // Skip 5th char 144 let address = (u16::from_str_radix(code.get(2..4)?, 16).ok()? << 8) 145 | u16::from_str_radix(code.get(5..7)?, 16).ok()?; 146 let value = u8::from_str_radix(code.get(7..)?, 16).ok()?; 147 (address, value) 148 }; 149 150 Some(ByteCheatCodeU16Address { address, value, reference: None }) 151} 152 153#[cfg(test)] 154mod tests { 155 use super::*; 156 157 #[test] 158 fn guess_code_type() { 159 assert_eq!(SmsGgCheatCodeType::guess_from(""), None); 160 161 assert_eq!(SmsGgCheatCodeType::guess_from("FFF-FFF"), Some(SmsGgCheatCodeType::GameGenie)); 162 assert_eq!( 163 SmsGgCheatCodeType::guess_from("000-000-000"), 164 Some(SmsGgCheatCodeType::GameGenie) 165 ); 166 167 assert_eq!( 168 SmsGgCheatCodeType::guess_from("00FF FFFF"), 169 Some(SmsGgCheatCodeType::ProActionReplay) 170 ); 171 assert_eq!( 172 SmsGgCheatCodeType::guess_from("00FFFFFF"), 173 Some(SmsGgCheatCodeType::ProActionReplay) 174 ); 175 } 176}