tetanes_core/
genie.rs

1//! Game Genie code parsing.
2
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, sync::OnceLock};
5use thiserror::Error;
6
7static GENIE_MAP: OnceLock<HashMap<char, u8>> = OnceLock::new();
8
9pub type Result<T> = std::result::Result<T, Error>;
10
11#[derive(Error, Debug)]
12#[error("invalid genie code {code:?}. {kind}")]
13pub struct Error {
14    code: String,
15    kind: ErrorKind,
16}
17
18impl Error {
19    fn new(code: impl Into<String>, kind: ErrorKind) -> Self {
20        Self {
21            code: code.into(),
22            kind,
23        }
24    }
25
26    pub const fn kind(&self) -> ErrorKind {
27        self.kind
28    }
29}
30
31#[derive(Error, Debug, Copy, Clone)]
32#[must_use]
33pub enum ErrorKind {
34    #[error("length must be 6 or 8 characters. found `{0}`")]
35    InvalidLength(usize),
36    #[error("invalid character: `{0}`")]
37    InvalidCharacter(char),
38}
39
40/// Game Genie Code
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct GenieCode {
43    code: String,
44    addr: u16,
45    data: u8,
46    compare: Option<u8>,
47}
48
49impl GenieCode {
50    /// Creates a new `GenieCode` instance.
51    ///
52    /// # Errors
53    ///
54    /// This function will return an error if the given code is not the correct format.
55    pub fn new(code: String) -> Result<Self> {
56        let hex = Self::parse(&code)?;
57        Ok(Self::from_raw(code, hex))
58    }
59
60    /// Creates a new `GenieCode` instance from raw hex values. `GenieCode` may not be valid if
61    /// `hex` is not the correct length. Use `GenieCode::parse` to validate the code.
62    pub fn from_raw(code: String, hex: Vec<u8>) -> Self {
63        let addr = 0x8000
64            + (((u16::from(hex[3]) & 7) << 12)
65                | ((u16::from(hex[5]) & 7) << 8)
66                | ((u16::from(hex[4]) & 8) << 8)
67                | ((u16::from(hex[2]) & 7) << 4)
68                | ((u16::from(hex[1]) & 8) << 4)
69                | (u16::from(hex[4]) & 7)
70                | (u16::from(hex[3]) & 8));
71        let data = if hex.len() == 6 {
72            ((hex[1] & 7) << 4) | ((hex[0] & 8) << 4) | (hex[0] & 7) | (hex[5] & 8)
73        } else {
74            ((hex[1] & 7) << 4) | ((hex[0] & 8) << 4) | (hex[0] & 7) | (hex[7] & 8)
75        };
76        let compare = if hex.len() == 8 {
77            Some(((hex[7] & 7) << 4) | ((hex[6] & 8) << 4) | (hex[6] & 7) | (hex[5] & 8))
78        } else {
79            None
80        };
81        Self {
82            code: code.to_ascii_uppercase(),
83            addr,
84            data,
85            compare,
86        }
87    }
88
89    fn generate_genie_map() -> HashMap<char, u8> {
90        // Game genie maps these letters to binary representations as a form of code obfuscation
91        HashMap::from([
92            ('A', 0x0),
93            ('P', 0x1),
94            ('Z', 0x2),
95            ('L', 0x3),
96            ('G', 0x4),
97            ('I', 0x5),
98            ('T', 0x6),
99            ('Y', 0x7),
100            ('E', 0x8),
101            ('O', 0x9),
102            ('X', 0xA),
103            ('U', 0xB),
104            ('K', 0xC),
105            ('S', 0xD),
106            ('V', 0xE),
107            ('N', 0xF),
108        ])
109    }
110
111    pub fn parse(code: &str) -> Result<Vec<u8>> {
112        if code.len() != 6 && code.len() != 8 {
113            return Err(Error::new(code, ErrorKind::InvalidLength(code.len())));
114        }
115        let mut hex: Vec<u8> = Vec::with_capacity(code.len());
116        for s in code.chars() {
117            if let Some(h) = GENIE_MAP
118                .get_or_init(Self::generate_genie_map)
119                .get(&s.to_ascii_uppercase())
120            {
121                hex.push(*h);
122            } else {
123                return Err(Error::new(code, ErrorKind::InvalidCharacter(s)));
124            }
125        }
126        Ok(hex)
127    }
128
129    #[must_use]
130    #[allow(clippy::missing_const_for_fn)] // false positive on non-const deref coercion
131    pub fn code(&self) -> &str {
132        &self.code
133    }
134
135    #[must_use]
136    pub const fn addr(&self) -> u16 {
137        self.addr
138    }
139
140    #[must_use]
141    pub const fn read(&self, val: u8) -> u8 {
142        if let Some(compare) = self.compare {
143            if val == compare { self.data } else { val }
144        } else {
145            self.data
146        }
147    }
148}
149
150impl std::fmt::Display for GenieCode {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        write!(f, "{}", &self.code)
153    }
154}