Skip to main content

polytrack_codes/v6/
mod.rs

1#![allow(clippy::cast_possible_wrap)]
2#[cfg(test)]
3mod tests;
4
5use std::fmt::Display;
6
7use num_enum::TryFromPrimitive;
8
9use crate::tools::{self, hash_vec, prelude::*};
10
11pub const CP_IDS: [u8; 4] = [52, 65, 75, 77];
12pub const START_IDS: [u8; 4] = [5, 91, 92, 93];
13
14#[derive(Debug, PartialEq, Eq, Clone)]
15pub struct TrackInfo {
16    pub env: Environment,
17    pub sun_dir: u8,
18
19    pub min_x: i32,
20    pub min_y: i32,
21    pub min_z: i32,
22
23    pub data_bytes: u8,
24    pub parts: Vec<Part>,
25}
26
27#[derive(TryFromPrimitive, Debug, PartialEq, Eq, Clone, Copy)]
28#[repr(u8)]
29pub enum Environment {
30    Summer,
31    Winter,
32    Desert,
33}
34
35#[derive(Debug, PartialEq, Eq, Clone)]
36pub struct Part {
37    pub id: u8,
38    pub amount: u32,
39    pub blocks: Vec<Block>,
40}
41
42#[derive(Debug, PartialEq, Eq, Clone)]
43pub struct Block {
44    pub x: u32,
45    pub y: u32,
46    pub z: u32,
47
48    pub rotation: u8,
49    pub dir: Direction,
50
51    pub color: u8,
52    pub cp_order: Option<u16>,
53    pub start_order: Option<u32>,
54}
55
56#[derive(TryFromPrimitive, Debug, PartialEq, Eq, Clone, Copy)]
57#[repr(u8)]
58pub enum Direction {
59    YPos,
60    YNeg,
61    XPos,
62    XNeg,
63    ZPos,
64    ZNeg,
65}
66
67#[must_use]
68/// Decodes the given track code and yields a struct containing the track name, track author, and the (raw binary) track data.
69/// Returns [`None`] if something failed in the process.
70pub fn decode_track_code(track_code: &str) -> Option<Track> {
71    // only use the actual data, skipping the "PolyTrack2"
72    let track_code = track_code.get(10..)?;
73    // ZLIB header 0x78DA is always encoded to `4p` and other stuff
74    let td_start = track_code.find("4p")?;
75    let track_data = track_code.get(td_start..)?;
76
77    // (base64-decode and then decompress using zlib) x2
78    let step1 = tools::decode(track_data)?;
79    let step2 = tools::decompress(&step1)?;
80    let step2_str = String::from_utf8(step2).ok()?;
81    let step3 = tools::decode(&step2_str)?;
82    let step4 = tools::decompress(&step3)?;
83
84    let name_len = *step4.first()? as usize;
85    let name = String::from_utf8(step4.get(1..=name_len)?.to_vec()).ok()?;
86
87    let author_len = *step4.get(1 + name_len)? as usize;
88    let author = String::from_utf8(
89        step4
90            .get((name_len + 2)..(name_len + author_len + 2))?
91            .to_vec(),
92    )
93    .ok();
94
95    let lastmod_exists = *step4.get(2 + name_len + author_len)? as usize;
96    if lastmod_exists > 1 {
97        return None;
98    }
99    let last_modified = if lastmod_exists == 1 {
100        let pos = 3 + name_len + author_len;
101        Some(
102            u32::from(*step4.get(pos)?)
103                | u32::from(*step4.get(pos + 1)?) << 8
104                | u32::from(*step4.get(pos + 2)?) << 16
105                | u32::from(*step4.get(pos + 3)?) << 24,
106        )
107    } else {
108        None
109    };
110    let track_data = step4
111        .get((name_len + author_len + 3 + if last_modified.is_some() { 4 } else { 0 })..)?
112        .to_vec();
113
114    Some(Track {
115        name,
116        author,
117        last_modified,
118        track_data,
119    })
120}
121
122#[must_use]
123/// Encodes the given track struct into a track code.
124/// Returns [`None`] if something failed in the process.
125///
126/// Output might differ slightly from Polytrack's output
127/// because of Zlib shenanigans, but is still compatible.
128pub fn encode_track_code(track: &Track) -> Option<String> {
129    let mut data: Vec<u8> = Vec::new();
130
131    let mut name = track.name.as_bytes().to_vec();
132    data.push(name.len().try_into().ok()?);
133    data.append(&mut name);
134
135    if let Some(author) = &track.author {
136        let mut author = author.as_bytes().to_vec();
137        data.push(author.len().try_into().ok()?);
138        data.append(&mut author);
139    } else {
140        data.push(0);
141    }
142
143    if let Some(last_modified) = track.last_modified {
144        data.push(1);
145        data.append(&mut last_modified.to_le_bytes().to_vec());
146    } else {
147        data.push(0);
148    }
149
150    data.append(&mut track.track_data.clone());
151
152    // (compress using zlib and then base64-encode) x2
153    let step1 = tools::compress_first(&data)?;
154    let step2_str = tools::encode(&step1)?;
155    let step2 = step2_str.as_bytes();
156    let step3 = tools::compress_final(step2)?;
157    let step4 = tools::encode(&step3)?;
158
159    // prepend the "PolyTrack2"
160    let track_code = String::from("PolyTrack2") + &step4;
161    Some(track_code)
162}
163
164#[must_use]
165/// Decodes the (raw binary) track data into a struct
166/// representing everything that is in the data.
167///
168/// Fields of all involved structs correspond exactly to how
169/// the data is stored in Polytrack itself.
170/// Returns [`None`] if the data is not valid track data.
171pub fn decode_track_data(data: &[u8]) -> Option<TrackInfo> {
172    let mut offset = 0;
173
174    let env = Environment::try_from(read_u8(data, &mut offset)?).ok()?;
175    let sun_dir = read_u8(data, &mut offset)?;
176
177    let min_x = read_u32(data, &mut offset)?.cast_signed();
178    let min_y = read_u32(data, &mut offset)?.cast_signed();
179    let min_z = read_u32(data, &mut offset)?.cast_signed();
180
181    let data_bytes = read_u8(data, &mut offset)?;
182    let x_bytes = data_bytes & 3;
183    let y_bytes = (data_bytes >> 2) & 3;
184    let z_bytes = (data_bytes >> 4) & 3;
185
186    let mut parts = Vec::new();
187    while offset < data.len() {
188        let id = read_u8(data, &mut offset)?;
189        let amount = read_u32(data, &mut offset)?;
190
191        let mut blocks = Vec::new();
192        for _ in 0..amount {
193            let mut x = 0;
194            for i in 0..x_bytes {
195                x |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
196            }
197            offset += x_bytes as usize;
198
199            let mut y = 0;
200            for i in 0..y_bytes {
201                y |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
202            }
203            offset += y_bytes as usize;
204
205            let mut z = 0;
206            for i in 0..z_bytes {
207                z |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
208            }
209            offset += z_bytes as usize;
210
211            let rot_dir = read_u8(data, &mut offset)?;
212            let rotation = rot_dir & 3;
213            if rotation > 3 {
214                return None;
215            }
216            let dir = Direction::try_from((rot_dir >> 2) & 7).ok()?;
217            let color = read_u8(data, &mut offset)?;
218            // no custom color support for now
219            if color > 3 && color < 32 && color > 40 {
220                return None;
221            }
222
223            let cp_order = if CP_IDS.contains(&id) {
224                Some(read_u16(data, &mut offset)?)
225            } else {
226                None
227            };
228            let start_order = if START_IDS.contains(&id) {
229                Some(read_u32(data, &mut offset)?)
230            } else {
231                None
232            };
233
234            blocks.push(Block {
235                x,
236                y,
237                z,
238
239                rotation,
240                dir,
241
242                color,
243                cp_order,
244                start_order,
245            });
246        }
247        parts.push(Part { id, amount, blocks });
248    }
249
250    Some(TrackInfo {
251        env,
252        sun_dir,
253
254        min_x,
255        min_y,
256        min_z,
257
258        data_bytes,
259        parts,
260    })
261}
262
263#[must_use]
264/// Encodes the `TrackInfo` struct into raw binary data.
265pub fn encode_track_data(track_info: &TrackInfo) -> Option<Vec<u8>> {
266    let mut data = Vec::new();
267
268    data.push(track_info.env as u8);
269    data.push(track_info.sun_dir);
270    write_u32(&mut data, track_info.min_x.cast_unsigned());
271    write_u32(&mut data, track_info.min_y.cast_unsigned());
272    write_u32(&mut data, track_info.min_z.cast_unsigned());
273    data.push(track_info.data_bytes);
274    let x_bytes = track_info.data_bytes & 3;
275    let y_bytes = (track_info.data_bytes >> 2) & 3;
276    let z_bytes = (track_info.data_bytes >> 4) & 3;
277    for part in &track_info.parts {
278        data.push(part.id);
279        write_u32(&mut data, part.amount);
280        for block in &part.blocks {
281            match x_bytes {
282                1 => write_u8(&mut data, block.x),
283                2 => write_u16(&mut data, block.x),
284                3 => write_u24(&mut data, block.x),
285                4 => write_u32(&mut data, block.x),
286                _ => {}
287            }
288            match y_bytes {
289                1 => write_u8(&mut data, block.y),
290                2 => write_u16(&mut data, block.y),
291                3 => write_u24(&mut data, block.y),
292                4 => write_u32(&mut data, block.y),
293                _ => {}
294            }
295            match z_bytes {
296                1 => write_u8(&mut data, block.z),
297                2 => write_u16(&mut data, block.z),
298                3 => write_u24(&mut data, block.z),
299                4 => write_u32(&mut data, block.z),
300                _ => {}
301            }
302            data.push(block.rotation & 3 | (block.dir as u8 & 7) << 2);
303            data.push(block.color);
304            if let Some(cp_order) = block.cp_order {
305                write_u16(&mut data, cp_order.into());
306            }
307            if let Some(start_order) = block.start_order {
308                write_u32(&mut data, start_order);
309            }
310        }
311    }
312
313    Some(data)
314}
315
316#[must_use]
317/// Computes the track ID for a given track code. Returns [`None`] if something failed in the process.
318pub fn export_to_id(track_code: &str) -> Option<String> {
319    let track_data = decode_track_code(track_code)?;
320    let id = hash_vec(track_data.track_data);
321    Some(id)
322}
323
324impl Display for Environment {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        match self {
327            Self::Summer => write!(f, "Summer"),
328            Self::Winter => write!(f, "Winter"),
329            Self::Desert => write!(f, "Desert"),
330        }
331    }
332}