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, Track, hash_vec};
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)]
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)]
28#[repr(u8)]
29pub enum Environment {
30 Summer,
31 Winter,
32 Desert,
33}
34
35#[derive(Debug, PartialEq, Eq)]
36pub struct Part {
37 pub id: u8,
38 pub amount: u32,
39 pub blocks: Vec<Block>,
40}
41
42#[derive(Debug, PartialEq, Eq)]
43pub struct Block {
44 pub x: u32,
45 pub y: u32,
46 pub z: u32,
47
48 pub rotation: u8,
50 pub dir: Direction,
51
52 pub color: u8,
53 pub cp_order: Option<u16>,
54 pub start_order: Option<u32>,
55}
56
57#[derive(TryFromPrimitive, Debug, PartialEq, Eq)]
58#[repr(u8)]
59pub enum Direction {
60 YPos,
61 YNeg,
62 XPos,
63 XNeg,
64 ZPos,
65 ZNeg,
66}
67
68#[must_use]
69pub fn decode_track_code(track_code: &str) -> Option<Track> {
72 let track_code = track_code.get(10..)?;
74 let td_start = track_code.find("4p")?;
76 let track_data = track_code.get(td_start..)?;
77
78 let step1 = tools::decode(track_data)?;
80 let step2 = tools::decompress(&step1)?;
81 let step2_str = String::from_utf8(step2).ok()?;
82 let step3 = tools::decode(&step2_str)?;
83 let step4 = tools::decompress(&step3)?;
84
85 let name_len = *step4.first()? as usize;
86 let author_len = *step4.get(1 + name_len)? as usize;
87
88 let name = String::from_utf8(step4.get(1..=name_len)?.to_vec()).ok()?;
89 let author = String::from_utf8(
90 step4
91 .get((name_len + 2)..(name_len + author_len + 2))?
92 .to_vec(),
93 )
94 .ok();
95 let track_data = step4.get((name_len + author_len + 2)..)?.to_vec();
96
97 Some(Track {
98 name,
99 author,
100 track_data,
101 })
102}
103
104#[must_use]
105pub fn decode_track_data(data: &[u8]) -> Option<TrackInfo> {
112 #[inline]
113 fn read_u8(buf: &[u8], offset: &mut usize) -> Option<u8> {
114 let res = buf.get(*offset).copied();
115 *offset += 1;
116 res
117 }
118 #[inline]
119 fn read_u16(buf: &[u8], offset: &mut usize) -> Option<u16> {
120 let res = Some(u16::from(*buf.get(*offset)?) | (u16::from(*buf.get(*offset + 1)?) << 8));
121 *offset += 2;
122 res
123 }
124 #[inline]
125 fn read_u32(buf: &[u8], offset: &mut usize) -> Option<u32> {
126 let res = Some(
127 u32::from(*buf.get(*offset)?)
128 | (u32::from(*buf.get(*offset + 1)?) << 8)
129 | (u32::from(*buf.get(*offset + 2)?) << 16)
130 | (u32::from(*buf.get(*offset + 3)?) << 24),
131 );
132 *offset += 4;
133 res
134 }
135
136 let mut offset = 0;
137
138 let env = Environment::try_from(read_u8(data, &mut offset)?).ok()?;
139 let sun_dir = read_u8(data, &mut offset)?;
140
141 let min_x = read_u32(data, &mut offset)? as i32;
142 let min_y = read_u32(data, &mut offset)? as i32;
143 let min_z = read_u32(data, &mut offset)? as i32;
144
145 let data_bytes = read_u8(data, &mut offset)?;
146 let x_bytes = data_bytes & 3;
147 let y_bytes = (data_bytes >> 2) & 3;
148 let z_bytes = (data_bytes >> 4) & 3;
149
150 let mut parts = Vec::new();
151 while offset < data.len() {
152 let id = read_u8(data, &mut offset)?;
153 let amount = read_u32(data, &mut offset)?;
154
155 let mut blocks = Vec::new();
156 for _ in 0..amount {
157 let mut x = 0;
158 for i in 0..x_bytes {
159 x |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
160 }
161 offset += x_bytes as usize;
162
163 let mut y = 0;
164 for i in 0..x_bytes {
165 y |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
166 }
167 offset += y_bytes as usize;
168
169 let mut z = 0;
170 for i in 0..x_bytes {
171 z |= u32::from(*data.get(offset + (i as usize))?) << (8 * i);
172 }
173 offset += z_bytes as usize;
174
175 let rotation = read_u8(data, &mut offset)?;
176 if rotation > 3 {
177 return None;
178 }
179 let dir = Direction::try_from(read_u8(data, &mut offset)?).ok()?;
180 let color = read_u8(data, &mut offset)?;
181 if color > 3 && color < 32 && color > 40 {
183 return None;
184 }
185
186 let cp_order = if CP_IDS.contains(&id) {
187 Some(read_u16(data, &mut offset)?)
188 } else {
189 None
190 };
191 let start_order = if START_IDS.contains(&id) {
192 Some(read_u32(data, &mut offset)?)
193 } else {
194 None
195 };
196
197 blocks.push(Block {
198 x,
199 y,
200 z,
201
202 rotation,
203 dir,
204
205 color,
206 cp_order,
207 start_order,
208 });
209 }
210 parts.push(Part { id, amount, blocks });
211 }
212
213 Some(TrackInfo {
214 env,
215 sun_dir,
216
217 min_x,
218 min_y,
219 min_z,
220
221 data_bytes,
222 parts,
223 })
224}
225
226#[must_use]
227pub fn export_to_id(track_code: &str) -> Option<String> {
229 let track_data = decode_track_code(track_code)?;
230 let id = hash_vec(track_data.track_data);
231 Some(id)
232}
233
234impl Display for Environment {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self {
237 Self::Summer => write!(f, "Summer"),
238 Self::Winter => write!(f, "Winter"),
239 Self::Desert => write!(f, "Desert"),
240 }
241 }
242}