1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Parser for REDGUARD.CHT cheat persistence files.
//!
//! The CHT file is a raw 256-byte dump of the engine's cheat state array:
//! 64 little-endian `u32` values. The first 13 entries correspond to the
//! built-in cheat codes; the remaining 51 slots are unused (always zero).
use crate::{Result, error::Error};
/// Total file size in bytes (64 × 4).
pub const CHT_FILE_SIZE: usize = 256;
/// Number of u32 slots in the cheat state array.
pub const CHT_SLOT_COUNT: usize = 64;
/// Number of named cheats recognised by the engine.
pub const CHT_CHEAT_COUNT: usize = 13;
/// Cheat names in index order, matching the engine's XOR-decoded name table.
pub const CHEAT_NAMES: [&str; CHT_CHEAT_COUNT] = [
"oracle",
"nodemarker",
"moonraker",
"task",
"animation",
"magiccarpet",
"savecheats",
"drevil",
"drno",
"goldfinger",
"neversaydie",
"oddjob",
"yeahbaby",
];
/// A single cheat entry with its index, name, and raw state value.
#[derive(Debug, Clone)]
pub struct CheatEntry {
/// Slot index (0–12 for named cheats, 13–63 for unused slots).
pub index: usize,
/// Cheat name, or `None` for unnamed slots beyond the first 13.
pub name: Option<&'static str>,
/// Raw state value. 0 = off, 1 = on (the engine also supports
/// arbitrary integer values set via `cheat = N` console syntax).
pub value: u32,
}
impl CheatEntry {
/// Returns whether the stored value is nonzero.
/// This does not establish a gameplay effect; some engine checks require exactly one.
#[must_use]
pub const fn is_on(&self) -> bool {
self.value != 0
}
}
/// Parsed CHT file.
#[derive(Debug, Clone)]
pub struct ChtFile {
/// All 64 cheat state entries.
pub entries: Vec<CheatEntry>,
}
impl ChtFile {
/// Returns only the 13 named cheat entries.
#[must_use]
pub fn named_cheats(&self) -> &[CheatEntry] {
&self.entries[..CHT_CHEAT_COUNT]
}
/// Returns cheat entries for unused slots that have nonzero values (unexpected).
#[must_use]
pub fn nonzero_unnamed(&self) -> Vec<&CheatEntry> {
self.entries[CHT_CHEAT_COUNT..]
.iter()
.filter(|e| e.value != 0)
.collect()
}
}
/// Parse a REDGUARD.CHT file from raw bytes.
pub fn parse_cht_file(data: &[u8]) -> Result<ChtFile> {
if data.len() != CHT_FILE_SIZE {
return Err(Error::Parse(format!(
"CHT file must be exactly {} bytes, got {}",
CHT_FILE_SIZE,
data.len()
)));
}
let entries = (0..CHT_SLOT_COUNT)
.map(|i| {
let offset = i * 4;
let value = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
CheatEntry {
index: i,
name: CHEAT_NAMES.get(i).copied(),
value,
}
})
.collect();
Ok(ChtFile { entries })
}
/// Encode all 64 raw state slots as a compatible REDGUARD.CHT file.
///
/// Values, including unnamed slots and integers other than zero and one, are
/// preserved without interpreting their gameplay meaning. See `docs/formats/CHT.md`.
#[must_use]
pub fn encode_cht_file(values: &[u32; CHT_SLOT_COUNT]) -> [u8; CHT_FILE_SIZE] {
let mut bytes = [0; CHT_FILE_SIZE];
for (chunk, value) in bytes.as_chunks_mut::<4>().0.iter_mut().zip(values) {
chunk.copy_from_slice(&value.to_le_bytes());
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encodes_raw_values_in_named_and_unnamed_slots() {
let mut values = [0; CHT_SLOT_COUNT];
values[10] = 0x1234_5678;
values[63] = u32::MAX;
let bytes = encode_cht_file(&values);
let mut expected = [0; 256];
expected[40..44].copy_from_slice(&[0x78, 0x56, 0x34, 0x12]);
expected[252..256].copy_from_slice(&[255, 255, 255, 255]);
assert_eq!(bytes, expected);
let parsed = parse_cht_file(&bytes).unwrap();
assert_eq!(parsed.named_cheats()[10].name, Some("neversaydie"));
assert_eq!(parsed.named_cheats()[10].value, 0x1234_5678);
assert_eq!(parsed.nonzero_unnamed()[0].index, 63);
assert_eq!(parsed.nonzero_unnamed()[0].value, u32::MAX);
}
#[test]
fn rejects_truncated_and_extended_files() {
for length in [0, 255, 257] {
assert!(parse_cht_file(&vec![0; length]).is_err());
}
}
}