1use crate::{Result, error::Error};
8
9pub const CHT_FILE_SIZE: usize = 256;
11
12pub const CHT_SLOT_COUNT: usize = 64;
14
15pub const CHT_CHEAT_COUNT: usize = 13;
17
18pub const CHEAT_NAMES: [&str; CHT_CHEAT_COUNT] = [
20 "oracle",
21 "nodemarker",
22 "moonraker",
23 "task",
24 "animation",
25 "magiccarpet",
26 "savecheats",
27 "drevil",
28 "drno",
29 "goldfinger",
30 "neversaydie",
31 "oddjob",
32 "yeahbaby",
33];
34
35#[derive(Debug, Clone)]
37pub struct CheatEntry {
38 pub index: usize,
40 pub name: Option<&'static str>,
42 pub value: u32,
45}
46
47impl CheatEntry {
48 #[must_use]
51 pub const fn is_on(&self) -> bool {
52 self.value != 0
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ChtFile {
59 pub entries: Vec<CheatEntry>,
61}
62
63impl ChtFile {
64 #[must_use]
66 pub fn named_cheats(&self) -> &[CheatEntry] {
67 &self.entries[..CHT_CHEAT_COUNT]
68 }
69
70 #[must_use]
72 pub fn nonzero_unnamed(&self) -> Vec<&CheatEntry> {
73 self.entries[CHT_CHEAT_COUNT..]
74 .iter()
75 .filter(|e| e.value != 0)
76 .collect()
77 }
78}
79
80pub fn parse_cht_file(data: &[u8]) -> Result<ChtFile> {
82 if data.len() != CHT_FILE_SIZE {
83 return Err(Error::Parse(format!(
84 "CHT file must be exactly {} bytes, got {}",
85 CHT_FILE_SIZE,
86 data.len()
87 )));
88 }
89
90 let entries = (0..CHT_SLOT_COUNT)
91 .map(|i| {
92 let offset = i * 4;
93 let value = u32::from_le_bytes([
94 data[offset],
95 data[offset + 1],
96 data[offset + 2],
97 data[offset + 3],
98 ]);
99 CheatEntry {
100 index: i,
101 name: CHEAT_NAMES.get(i).copied(),
102 value,
103 }
104 })
105 .collect();
106
107 Ok(ChtFile { entries })
108}
109
110#[must_use]
115pub fn encode_cht_file(values: &[u32; CHT_SLOT_COUNT]) -> [u8; CHT_FILE_SIZE] {
116 let mut bytes = [0; CHT_FILE_SIZE];
117 for (chunk, value) in bytes.as_chunks_mut::<4>().0.iter_mut().zip(values) {
118 chunk.copy_from_slice(&value.to_le_bytes());
119 }
120 bytes
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn encodes_raw_values_in_named_and_unnamed_slots() {
129 let mut values = [0; CHT_SLOT_COUNT];
130 values[10] = 0x1234_5678;
131 values[63] = u32::MAX;
132 let bytes = encode_cht_file(&values);
133 let mut expected = [0; 256];
134 expected[40..44].copy_from_slice(&[0x78, 0x56, 0x34, 0x12]);
135 expected[252..256].copy_from_slice(&[255, 255, 255, 255]);
136 assert_eq!(bytes, expected);
137 let parsed = parse_cht_file(&bytes).unwrap();
138 assert_eq!(parsed.named_cheats()[10].name, Some("neversaydie"));
139 assert_eq!(parsed.named_cheats()[10].value, 0x1234_5678);
140 assert_eq!(parsed.nonzero_unnamed()[0].index, 63);
141 assert_eq!(parsed.nonzero_unnamed()[0].value, u32::MAX);
142 }
143
144 #[test]
145 fn rejects_truncated_and_extended_files() {
146 for length in [0, 255, 257] {
147 assert!(parse_cht_file(&vec![0; length]).is_err());
148 }
149 }
150}