1#[cfg(test)]
21use std::io::{Cursor, Read, Seek, SeekFrom};
22use std::{
23 fs::{self, File},
24 io::ErrorKind,
25 path::PathBuf,
26};
27
28use binrw::BinRead;
29use insim_core::{
30 binrw::{self, binrw},
31 point::Point,
32};
33use thiserror::Error;
34
35#[non_exhaustive]
36#[derive(Error, Debug)]
37#[allow(missing_docs)]
38pub enum Error {
39 #[error("IO Error: {kind}: {message}")]
40 IO { kind: ErrorKind, message: String },
41
42 #[error("BinRw Err {0:?}")]
43 BinRwErr(#[from] binrw::Error),
44}
45
46impl From<std::io::Error> for Error {
47 fn from(e: std::io::Error) -> Self {
48 Error::IO {
49 kind: e.kind(),
50 message: e.to_string(),
51 }
52 }
53}
54
55#[derive(Debug, Copy, Clone, Default, PartialEq)]
57#[binrw]
58pub struct Limit {
59 pub left: f32,
61
62 pub right: f32,
64}
65
66#[derive(Debug, Copy, Clone, Default, PartialEq)]
68#[binrw]
69pub struct Node {
70 pub center: Point<i32>,
72
73 pub direction: Point<f32>,
75
76 pub outer_limit: Limit,
78
79 pub road_limit: Limit,
81}
82
83impl Node {
84 pub fn get_center(&self, scale: Option<f32>) -> Point<f32> {
86 let scale = scale.unwrap_or(1.0);
87
88 Point {
89 x: self.center.x as f32 / scale,
90 y: self.center.y as f32 / scale,
91 z: self.center.z as f32 / scale,
92 }
93 }
94
95 pub fn get_road_limit(&self, scale: Option<f32>) -> (Point<f32>, Point<f32>) {
97 self.calculate_limit_position(&self.road_limit, scale)
98 }
99
100 pub fn get_outer_limit(&self, scale: Option<f32>) -> (Point<f32>, Point<f32>) {
102 self.calculate_limit_position(&self.outer_limit, scale)
103 }
104
105 fn calculate_limit_position(
106 &self,
107 limit: &Limit,
108 scale: Option<f32>,
109 ) -> (Point<f32>, Point<f32>) {
110 let left_cos = f32::cos(90.0 * std::f32::consts::PI / 180.0);
111 let left_sin = f32::sin(90.0 * std::f32::consts::PI / 180.0);
112 let right_cos = f32::cos(-90.0 * std::f32::consts::PI / 180.0);
113 let right_sin = f32::sin(-90.0 * std::f32::consts::PI / 180.0);
114
115 let center = self.get_center(scale);
116
117 let left: Point<f32> = Point {
118 x: ((self.direction.x * left_cos) - (self.direction.y * left_sin)) * limit.left
119 + (center.x),
120 y: ((self.direction.y * left_cos) + (self.direction.x * left_sin)) * limit.left
121 + (center.y),
122 z: (center.z),
123 };
124
125 let right: Point<f32> = Point {
126 x: ((self.direction.x * right_cos) - (self.direction.y * right_sin)) * -limit.right
127 + (center.x),
128 y: ((self.direction.y * right_cos) + (self.direction.x * right_sin)) * -limit.right
129 + (center.y),
130 z: (center.z),
131 };
132
133 (left, right)
134 }
135}
136
137#[binrw]
138#[brw(little, magic = b"LFSPTH")]
139#[derive(Debug, Default, PartialEq)]
140pub struct Pth {
142 pub version: u8,
144 pub revision: u8,
146
147 #[bw(calc = nodes.len() as i32)]
148 num_nodes: i32,
149
150 pub finish_line_node: i32,
152
153 #[br(count = num_nodes)]
154 pub nodes: Vec<Node>,
156}
157
158impl Pth {
159 pub fn from_file(i: &mut File) -> Result<Self, Error> {
161 Pth::read(i).map_err(Error::from).map_err(Error::from)
162 }
163
164 pub fn from_pathbuf(i: &PathBuf) -> Result<Self, Error> {
166 if !i.exists() {
167 return Err(Error::IO {
168 kind: std::io::ErrorKind::NotFound,
169 message: format!("Path {i:?} does not exist"),
170 });
171 }
172
173 let mut input = fs::File::open(i).map_err(Error::from)?;
174
175 Self::from_file(&mut input)
176 }
177}
178
179#[cfg(test)]
180fn assert_valid_as1_pth(p: &Pth) {
181 assert_eq!(p.version, 0);
182 assert_eq!(p.revision, 0);
183 assert_eq!(p.finish_line_node, 250);
184}
185
186#[test]
187fn test_pth_decode_from_pathbuf() {
188 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./tests/AS1.pth");
189 let p = Pth::from_pathbuf(&path).expect("Expected PTH file to be parsed");
190
191 assert_valid_as1_pth(&p)
192}
193
194#[test]
195fn test_pth_decode_from_file() {
196 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./tests/AS1.pth");
197 let mut file = File::open(path).expect("Expected Autocross_3DH.smx to exist");
198 let p = Pth::from_file(&mut file).expect("Expected PTH file to be parsed");
199
200 let pos = file.stream_position().unwrap();
201 let end = file.seek(SeekFrom::End(0)).unwrap();
202
203 assert_eq!(pos, end, "Expected the whole file to be completely read");
204
205 assert_valid_as1_pth(&p)
206}
207
208#[test]
209fn test_pth_encode() {
210 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./tests/AS1.pth");
211 let p = Pth::from_pathbuf(&path).expect("Expected SMX file to be parsed");
212
213 let mut file = File::open(path).expect("Expected AS1.pth to exist");
214 let mut raw: Vec<u8> = Vec::new();
215 let _ = file
216 .read_to_end(&mut raw)
217 .expect("Expected to read whole file");
218
219 let mut writer = Cursor::new(Vec::new());
220 binrw::BinWrite::write(&p, &mut writer).expect("Expected to write the whole file");
221
222 let inner = writer.into_inner();
223 assert_eq!(inner, raw);
224}