gbx_header/gbx/
mod.rs

1pub mod parser;
2
3use fmt::{Debug, Display};
4use serde::{Deserialize, Serialize};
5use std::{convert::TryFrom, fmt};
6
7use enum_repr::EnumRepr;
8
9/// Container for raw image data (assumed to be a valid jpg)
10#[derive(Serialize, Deserialize)]
11pub struct JPEGData(pub Vec<u8>);
12
13impl fmt::Display for JPEGData {
14    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
15        write!(f, "JPEG({}B)", self.0.len())
16    }
17}
18
19impl fmt::Debug for JPEGData {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        fmt::Display::fmt(self, f)
22    }
23}
24
25/// Container for any data extracted from a GBX file.
26///
27/// See [parse_from_file](parser::parse_from_file) and [parse_from_buffer](parser::parse_from_buffer).
28/// Serde is not used internally to Deserialize, but added to enable easier integration of these datatypes in other applications.
29#[derive(Debug, Serialize, Deserialize)]
30pub struct GBX {
31    pub origin: GBXOrigin,
32    pub filesize: usize,
33    header_start: usize,
34    header_length: usize,
35    thumbnail_start: Option<usize>,
36    thumbnail_length: Option<usize>,
37    pub thumbnail: Option<JPEGData>,
38    pub bin_header: GBXBinaryHeader,
39    pub challenge_header: Option<ChallengeXMLHeader>,
40    pub replay_header: Option<ReplayXMLHeader>,
41    pub header_xml: String,
42}
43
44impl Display for GBX {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        fn unoption<T: Display>(o: &Option<&T>) -> String {
47            o.map(|x| format!("{}", x))
48                .unwrap_or_else(|| "Not present".to_owned())
49        }
50        write!(
51                f,
52                "GBX Info Dump (Size={}B)\nFrom file={}\n\nMagic\n=====\n{}\n\nChallenge\n=========\n{}\n\nReplay\n======\n{}",
53                self.filesize, self.origin, self.bin_header, unoption(&self.challenge_header.as_ref()), unoption(&self.replay_header.as_ref())
54        )
55    }
56}
57
58/// Stores the source of the GBX struct.
59/// By default a GBX struct will be `Unknown`, the [parser](parser) methods set the
60/// `origin` field of the [GBX](GBX) struct accordingly. If you don't want to expose
61/// this information about your local filesystem remember to overwrite that field.
62#[derive(Debug, Serialize, Deserialize)]
63pub enum GBXOrigin {
64    File {
65        path: String,
66    },
67    Buffer,
68    Unknown,
69    /// Added field to allow hiding the origin (library will never use this)
70    Hidden,
71}
72
73impl Default for GBXOrigin {
74    fn default() -> Self {
75        GBXOrigin::Unknown
76    }
77}
78
79impl Display for GBXOrigin {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            GBXOrigin::File { path } => write!(f, "{}", path),
83            GBXOrigin::Buffer => write!(f, "<buffer>"),
84            GBXOrigin::Unknown => write!(f, "<unknown>"),
85            GBXOrigin::Hidden => write!(f, "<hidden>"),
86        }
87    }
88}
89
90#[derive(Debug, Serialize, Deserialize, Default)]
91pub struct ChallengeXMLHeader {
92    pub maptype: MapType,
93    pub mapversion: GBXVersion,
94    pub exever: String,
95    pub uid: String,
96    pub name: String,
97    pub author: String,
98    pub envir: Environment,
99    pub mood: Mood,
100    pub desctype: DescType,
101    pub nblaps: u32,
102    pub price: u32,
103    /// Completion times and scores for the challenge, None if none set.
104    pub times: Option<Times>,
105    pub dependencies: Vec<Dependency>,
106}
107
108impl fmt::Display for ChallengeXMLHeader {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let dependency_files: Vec<&String> = self.dependencies.iter().map(|x| &x.file).collect();
111        write!(f, "Map is {:?}/{:?} made in {:?}/{}\nUID: {}\nName: {}\nAuthor: {}\nSetting: {:?}/{:?}\nNumber of laps: {}\nPrice: {}\nTimes: {}\nDependencies[{}]: {:?}",
112            self.maptype, self.desctype, self.mapversion, self.exever, self.uid, self.name, self.author, self.envir, self.mood, self.nblaps, self.price, self.times.as_ref().map_or(String::from("<not set>"), |x| format!("{}", x)), self.dependencies.len(), dependency_files,
113        )
114    }
115}
116
117#[derive(Debug, Serialize, Deserialize, Default)]
118pub struct GBXBinaryHeader {
119    pub version: u16,
120    pub class_id: u32,
121}
122
123impl Display for GBXBinaryHeader {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(
126            f,
127            "v{}, class id: {:08x} ({:?})",
128            self.version,
129            self.class_id,
130            MapClass::try_from(self.class_id).map_or("unknown".to_owned(), |c| format!("{:?}", c))
131        )
132    }
133}
134
135#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
136pub struct ReplayXMLHeader {
137    /// Version of the replay file format
138    pub version: GBXVersion,
139    /// Version on executable player used to make the replay
140    pub exever: String,
141    /// UID of Challenge
142    pub challenge_uid: String,
143    /// Score and time
144    pub score: ReplayScore,
145}
146
147impl Display for ReplayXMLHeader {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(
150            f,
151            "Version: {:?}\nExever.: {}\nChallenge: {}\nScore: {}",
152            self.version, self.exever, self.challenge_uid, self.score
153        )
154    }
155}
156
157#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
158pub struct ReplayScore {
159    /// Best time in ms
160    pub best: u32,
161    /// Number of respawns in attempt
162    pub respawns: u32,
163    pub stuntscore: u32,
164    pub validable: bool,
165}
166
167impl Display for ReplayScore {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(
170            f,
171            "best={}, respawns={}, stuntscore={}, validable={}",
172            self.best, self.respawns, self.stuntscore, self.validable
173        )
174    }
175}
176
177// Times measured in ms
178#[derive(Debug, Serialize, Deserialize, Default)]
179pub struct Times {
180    pub bronze: Option<u32>,
181    pub silver: Option<u32>,
182    pub gold: Option<u32>,
183    pub authortime: Option<u32>,
184    pub authorscore: Option<u32>,
185}
186
187impl fmt::Display for Times {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(
190            f,
191            "Bronze: {}, Silver: {}, Gold: {}, Authortime: {}, Authorscore: {}",
192            self.bronze
193                .map_or(String::from("<not set>"), |x| format!("{}", x)),
194            self.silver
195                .map_or(String::from("<not set>"), |x| format!("{}", x)),
196            self.gold
197                .map_or(String::from("<not set>"), |x| format!("{}", x)),
198            self.authortime
199                .map_or(String::from("<not set>"), |x| format!("{}", x)),
200            self.authorscore
201                .map_or(String::from("<not set>"), |x| format!("{}", x))
202        )
203    }
204}
205
206#[derive(Debug, Serialize, Deserialize, Default)]
207pub struct Dependency {
208    pub file: String,
209}
210
211#[derive(Debug, Serialize, Deserialize)]
212pub enum MapType {
213    Challenge,
214}
215
216impl TryFrom<&str> for MapType {
217    type Error = String;
218
219    fn try_from(value: &str) -> Result<Self, Self::Error> {
220        match value.to_lowercase().as_str() {
221            "challenge" => Ok(MapType::Challenge),
222            _ => Err(format!("Unknown map type: {}", value)),
223        }
224    }
225}
226
227impl Default for MapType {
228    fn default() -> Self {
229        MapType::Challenge
230    }
231}
232
233#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
234pub enum GBXVersion {
235    /// Unknown Type/Version
236    Unknown,
237    /// Challenge v6
238    TMc6,
239    /// Replay v7
240    TMr7,
241}
242
243impl TryFrom<&str> for GBXVersion {
244    type Error = String;
245
246    fn try_from(value: &str) -> Result<Self, Self::Error> {
247        match value.to_lowercase().as_str() {
248            "tmc.6" => Ok(GBXVersion::TMc6),
249            "tmr.7" => Ok(GBXVersion::TMr7),
250            _ => Err(format!("Unknown GBX file version: {}", value)),
251        }
252    }
253}
254
255impl GBXVersion {
256    /// Converts specific GBX file version and type (GBXVersion) into more generic GBXType.
257    pub fn content_type(&self) -> GBXType {
258        match self {
259            GBXVersion::TMc6 => GBXType::Challenge,
260            GBXVersion::TMr7 => GBXType::Replay,
261            GBXVersion::Unknown => GBXType::Unknown,
262        }
263    }
264}
265
266#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
267pub enum GBXType {
268    Challenge,
269    Replay,
270    /// GBX files can be many types of objects, this repo doesn't aim to implement parsing for most of them.
271    Unknown,
272}
273
274impl Default for GBXVersion {
275    fn default() -> Self {
276        GBXVersion::Unknown
277    }
278}
279
280#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
281pub enum Environment {
282    Stadium,
283}
284
285impl TryFrom<&str> for Environment {
286    type Error = String;
287
288    fn try_from(value: &str) -> Result<Self, Self::Error> {
289        match value.to_lowercase().as_str() {
290            "stadium" => Ok(Environment::Stadium),
291            _ => Err(format!("Unknown environment: {}", value)),
292        }
293    }
294}
295
296impl Default for Environment {
297    fn default() -> Self {
298        Environment::Stadium
299    }
300}
301
302#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
303pub enum Mood {
304    Day,
305    Sunset,
306    Sunrise,
307    Night,
308}
309
310impl TryFrom<&str> for Mood {
311    type Error = String;
312
313    fn try_from(value: &str) -> Result<Self, Self::Error> {
314        match value.to_lowercase().as_str() {
315            "day" => Ok(Mood::Day),
316            "sunset" => Ok(Mood::Sunset),
317            "sunrise" => Ok(Mood::Sunrise),
318            "night" => Ok(Mood::Night),
319            _ => Err(format!("Unknown mood: {}", value)),
320        }
321    }
322}
323
324impl Default for Mood {
325    fn default() -> Self {
326        Mood::Day
327    }
328}
329
330#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
331pub enum DescType {
332    Race,
333}
334
335impl TryFrom<&str> for DescType {
336    type Error = String;
337
338    fn try_from(value: &str) -> Result<Self, Self::Error> {
339        match value.to_lowercase().as_str() {
340            "race" => Ok(DescType::Race),
341            _ => Err(format!("Unknown desc.type: {}", value)),
342        }
343    }
344}
345
346impl Default for DescType {
347    fn default() -> Self {
348        DescType::Race
349    }
350}
351
352#[EnumRepr(type = "u32")]
353/// IDs and names taken from [wiki.xaseco.org](https://wiki.xaseco.org/wiki/GBX).
354#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
355pub enum MapClass {
356    CGameCtnChallenge = 0x03043000,
357    CGameCtnCollectorList = 0x0301B000,
358    CGameCtnChallengeParameters = 0x0305B000,
359    CGameCtnBlockSkin = 0x03059000,
360    CGameWaypointSpecialProperty = 0x0313B000,
361    CGameCtnReplayRecord = 0x03093000,
362    CGameGhost = 0x0303F005,
363    CGameCtnGhost = 0x03092000,
364    CGameCtnCollector = 0x0301A000,
365    CGameCtnObjectInfo = 0x0301C000,
366    CGameCtnDecoration = 0x03038000,
367    CGameCtnCollection = 0x03033000,
368    CGameSkin = 0x03031000,
369    CGamePlayerProfile = 0x0308C000,
370    CMwNod = 0x01001000,
371}
372
373impl TryFrom<u32> for MapClass {
374    type Error = ();
375
376    fn try_from(value: u32) -> Result<Self, Self::Error> {
377        MapClass::from_repr(value).ok_or(())
378    }
379}