1use bincode::{Decode, Encode};
2use jgenesis_common::num::GetBit;
3use regex::Regex;
4use std::fmt::{Display, Formatter};
5use std::sync::LazyLock;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum GenesisCheatCodeType {
9    GameGenie,
10    ActionReplay,
11    MemoryOverride,
12}
13
14impl GenesisCheatCodeType {
15    #[must_use]
16    pub fn guess_from(code: &str) -> Option<Self> {
17        static GAME_GENIE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
18            Regex::new(r"^(?i)[A-HJ-NPR-TV-Z0-9]{4}-[A-HJ-NPR-TV-Z0-9]{4}$").unwrap()
19        });
20
21        static PRO_ACTION_REPLAY_REGEX: LazyLock<Regex> =
22            LazyLock::new(|| Regex::new(r"^[[:xdigit:]]{5}[ -]?[[:xdigit:]]{5}$").unwrap());
23
24        static MEMORY_OVERRIDE_REGEX: LazyLock<Regex> =
25            LazyLock::new(|| Regex::new(r"^[[:xdigit:]]{6}:[[:xdigit:]]{4}$").unwrap());
26
27        if GAME_GENIE_REGEX.is_match(code) {
28            return Some(Self::GameGenie);
29        }
30
31        if PRO_ACTION_REPLAY_REGEX.is_match(code) {
32            return Some(Self::ActionReplay);
33        }
34
35        if MEMORY_OVERRIDE_REGEX.is_match(code) {
36            return Some(Self::MemoryOverride);
37        }
38
39        None
40    }
41
42    #[must_use]
43    pub fn decode(self, code: &str) -> Option<(u32, u16)> {
44        match self {
45            Self::GameGenie => decode_game_genie(code),
46            Self::ActionReplay => decode_action_replay(code),
47            Self::MemoryOverride => decode_memory_override(code),
48        }
49    }
50}
51
52impl Display for GenesisCheatCodeType {
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::GameGenie => write!(f, "Game Genie"),
56            Self::ActionReplay => write!(f, "Action Replay"),
57            Self::MemoryOverride => write!(f, "Memory Override"),
58        }
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[cfg_attr(feature = "serde", serde(default))]
65pub struct GenesisCheat {
66    pub name: String,
67    pub enabled: bool,
68    pub codes: Vec<String>,
69}
70
71impl GenesisCheat {
72    #[must_use]
73    pub fn to_memory_override_vec(&self) -> Vec<(u32, u16)> {
74        if !self.enabled {
75            return vec![];
76        }
77
78        self.codes
79            .iter()
80            .filter_map(|code| {
81                GenesisCheatCodeType::guess_from(code).and_then(|code_type| code_type.decode(code))
82            })
83            .collect()
84    }
85}
86
87impl GenesisCheat {
88    #[must_use]
89    pub fn new(name: String) -> Self {
90        Self { name, enabled: true, codes: vec![] }
91    }
92}
93
94impl Default for GenesisCheat {
95    fn default() -> Self {
96        Self::new(String::new())
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Default, Encode, Decode)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102#[cfg_attr(feature = "serde", serde(default))]
103pub struct GenesisCheats {
104    pub cheats: Vec<GenesisCheat>,
105}
106
107impl GenesisCheats {
108    /// Convert to a [`Vec`] of 68000 address/value pairs
109    ///
110    /// Excludes disabled cheats and any cheats that have invalid codes
111    #[must_use]
112    pub fn to_memory_override_vec(&self) -> Vec<(u32, u16)> {
113        self.cheats
114            .iter()
115            .filter(|cheat| cheat.enabled)
116            .flat_map(GenesisCheat::to_memory_override_vec)
117            .collect()
118    }
119}
120
121#[must_use]
122pub fn decode_game_genie(code: &str) -> Option<(u32, u16)> {
123    let code = code.as_bytes();
124
125    // Codes should always be in ABCD-EFGH format
126    if code.len() != 9 || code[4] != b'-' {
127        return None;
128    }
129
130    // The 8 non-hyphen characters decode into a scrambled 40-bit value, 5 bits per char
131    let mut scrambled: u64 = 0;
132
133    for &c in &code[..4] {
134        scrambled = (scrambled << 5) | decode_game_genie_char(c)?;
135    }
136
137    for &c in &code[5..] {
138        scrambled = (scrambled << 5) | decode_game_genie_char(c)?;
139    }
140
141    // Scrambled bit layout:
142    //     A     B     C     D  -  E     F     G     H
143    //   ijklm nopIJ KLMNO PABCD EFGHd efgha bcQRS TUVWX
144    //      35    30    25    20    15    10     5     0
145    //
146    // Descrambled:
147    //   Address (24-bit): ABCD EFGH IJKL MNOP QRST UVWX
148    //   Value (16-bit):   abcd efgh ijkl mnop
149
150    let address =
151        (scrambled.bits(16..=23) << 16) | (scrambled.bits(24..=31) << 8) | scrambled.bits(0..=7);
152
153    let value =
154        (scrambled.bits(8..=10) << 13) | (scrambled.bits(11..=15) << 8) | scrambled.bits(32..=39);
155
156    Some((address as u32, value as u16))
157}
158
159fn decode_game_genie_char(c: u8) -> Option<u64> {
160    // Codes are encoded in base 32 using the characters A-Z and 0-9 excluding I, O, Q, and U
161    // Any other character is invalid
162    let bits = match c {
163        b'A'..=b'H' => c - b'A',
164        b'J'..=b'N' => c - b'A' - 1,
165        b'P' => c - b'A' - 2,
166        b'R'..=b'T' => c - b'A' - 3,
167        b'V'..=b'Z' => c - b'A' - 4,
168        b'0'..=b'9' => c - b'0' + 22,
169        // Support lowercase letters too, why not
170        b'a'..=b'h' => c - b'a',
171        b'j'..=b'n' => c - b'a' - 1,
172        b'p' => c - b'a' - 2,
173        b'r'..=b't' => c - b'a' - 3,
174        b'v'..=b'z' => c - b'a' - 4,
175        _ => return None,
176    };
177
178    debug_assert!(bits < 32);
179
180    Some(bits.into())
181}
182
183#[must_use]
184pub fn decode_action_replay(code: &str) -> Option<(u32, u16)> {
185    // Pro Action Replay codes are in format "98765-43210" or "98765 43210"
186    // With hyphen/space removed, first 6 digits are address and last 4 are value (in hex)
187    let bytes = code.as_bytes();
188
189    if bytes.len() != 11 || !matches!(bytes[5], b'-' | b' ') {
190        return None;
191    }
192
193    let address =
194        16 * u32::from_str_radix(code.get(..5)?, 16).ok()? + (bytes[6] as char).to_digit(16)?;
195    let value = u16::from_str_radix(code.get(7..)?, 16).ok()?;
196
197    // Action Replay memory values seem to be little-endian instead of big-endian; byteswap
198    let value = value.swap_bytes();
199
200    Some((address, value))
201}
202
203#[must_use]
204pub fn decode_memory_override(code: &str) -> Option<(u32, u16)> {
205    // Memory override codes are in format 987654:3210 (address:value in hex)
206    if code.len() != 11 || code.as_bytes()[6] != b':' {
207        return None;
208    }
209
210    let address = u32::from_str_radix(code.get(..6)?, 16).ok()?;
211    let value = u16::from_str_radix(code.get(7..)?, 16).ok()?;
212
213    Some((address, value))
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_decode_game_genie() {
222        assert_eq!(decode_game_genie("97JT-EVSG"), Some((0x0251E6, 0xA8FF)));
223        assert_eq!(decode_game_genie("AFJT-EVSG"), Some((0x0251E6, 0xA801)));
224
225        assert_eq!(decode_game_genie(""), None);
226        assert_eq!(decode_game_genie("AAAI-AAAA"), None);
227    }
228
229    #[test]
230    fn test_decode_pro_action_replay() {
231        assert_eq!(decode_action_replay("98765-43210"), Some((0x987654, 0x1032)));
232        assert_eq!(decode_action_replay("FFFFF-EABCD"), Some((0xFFFFFE, 0xCDAB)));
233
234        assert_eq!(decode_action_replay(""), None);
235        assert_eq!(decode_action_replay("QWERT-YUIOP"), None);
236    }
237
238    #[test]
239    fn test_decode_memory_override() {
240        assert_eq!(decode_memory_override("987654:3210"), Some((0x987654, 0x3210)));
241        assert_eq!(decode_memory_override("FFFFFE:ABCD"), Some((0xFFFFFE, 0xABCD)));
242
243        assert_eq!(decode_memory_override(""), None);
244        assert_eq!(decode_memory_override("QWERTY:UIOP"), None);
245    }
246
247    #[test]
248    fn guess_code_type() {
249        assert_eq!(GenesisCheatCodeType::guess_from(""), None);
250        assert_eq!(GenesisCheatCodeType::guess_from("ABCDEFGH"), None);
251
252        assert_eq!(
253            GenesisCheatCodeType::guess_from("ABCD-EFGH"),
254            Some(GenesisCheatCodeType::GameGenie)
255        );
256        assert_eq!(
257            GenesisCheatCodeType::guess_from("abcd-efgh"),
258            Some(GenesisCheatCodeType::GameGenie)
259        );
260        assert_eq!(GenesisCheatCodeType::guess_from("ABCD-EFGI"), None);
261
262        assert_eq!(
263            GenesisCheatCodeType::guess_from("02468-ACEFF"),
264            Some(GenesisCheatCodeType::ActionReplay)
265        );
266        assert_eq!(
267            GenesisCheatCodeType::guess_from("02468-aceff"),
268            Some(GenesisCheatCodeType::ActionReplay)
269        );
270        assert_eq!(
271            GenesisCheatCodeType::guess_from("02468 ACEFF"),
272            Some(GenesisCheatCodeType::ActionReplay)
273        );
274
275        assert_eq!(
276            GenesisCheatCodeType::guess_from("02468A:CEFF"),
277            Some(GenesisCheatCodeType::MemoryOverride)
278        );
279        assert_eq!(
280            GenesisCheatCodeType::guess_from("02468a:ceff"),
281            Some(GenesisCheatCodeType::MemoryOverride)
282        );
283    }
284}