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}