Skip to main content

rgpre/import/
cht.rs

1//! Parser for REDGUARD.CHT cheat persistence files.
2//!
3//! The CHT file is a raw 256-byte dump of the engine's cheat state array:
4//! 64 little-endian `u32` values. The first 13 entries correspond to the
5//! built-in cheat codes; the remaining 51 slots are unused (always zero).
6
7use crate::{Result, error::Error};
8
9/// Total file size in bytes (64 × 4).
10pub const CHT_FILE_SIZE: usize = 256;
11
12/// Number of u32 slots in the cheat state array.
13pub const CHT_SLOT_COUNT: usize = 64;
14
15/// Number of named cheats recognised by the engine.
16pub const CHT_CHEAT_COUNT: usize = 13;
17
18/// Cheat names in index order, matching the engine's XOR-decoded name table.
19pub 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/// A single cheat entry with its index, name, and raw state value.
36#[derive(Debug, Clone)]
37pub struct CheatEntry {
38    /// Slot index (0–12 for named cheats, 13–63 for unused slots).
39    pub index: usize,
40    /// Cheat name, or `None` for unnamed slots beyond the first 13.
41    pub name: Option<&'static str>,
42    /// Raw state value. 0 = off, 1 = on (the engine also supports
43    /// arbitrary integer values set via `cheat = N` console syntax).
44    pub value: u32,
45}
46
47impl CheatEntry {
48    /// Returns whether the stored value is nonzero.
49    /// This does not establish a gameplay effect; some engine checks require exactly one.
50    #[must_use]
51    pub const fn is_on(&self) -> bool {
52        self.value != 0
53    }
54}
55
56/// Parsed CHT file.
57#[derive(Debug, Clone)]
58pub struct ChtFile {
59    /// All 64 cheat state entries.
60    pub entries: Vec<CheatEntry>,
61}
62
63impl ChtFile {
64    /// Returns only the 13 named cheat entries.
65    #[must_use]
66    pub fn named_cheats(&self) -> &[CheatEntry] {
67        &self.entries[..CHT_CHEAT_COUNT]
68    }
69
70    /// Returns cheat entries for unused slots that have nonzero values (unexpected).
71    #[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
80/// Parse a REDGUARD.CHT file from raw bytes.
81pub 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/// Encode all 64 raw state slots as a compatible REDGUARD.CHT file.
111///
112/// Values, including unnamed slots and integers other than zero and one, are
113/// preserved without interpreting their gameplay meaning. See `docs/formats/CHT.md`.
114#[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}