insim_pth/
lib.rs

1//! # insim_pth
2//!
3//! Parse a Live for Speed pth (path) file.
4//!
5//! A pth file consists of a series points [Node], with direction and width ([Limit]),
6//! that describe the track that you drive along.
7//!
8//! Historically LFS has used the PTH to watch your progress along the track, decides
9//! if you are driving in reverse, the yellow and blue flag systems, the position list,
10//! timing, etc.
11//!
12//! On a standard LFS track the [Node] is communicated via MCI and NLP Insim packets.
13//!
14//! On an open configuration [Node] are not used and are unavailable via Insim MCI packets.
15//!
16//! The distance between each [Node] is not constant. According to the LFS developers
17//! there is approximately 0.2 seconds of time between passing one node and the next,
18//! when you are "driving at a reasonable speed".
19
20#[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/// Describes the Left and Right limit, of a given node.
56#[derive(Debug, Copy, Clone, Default, PartialEq)]
57#[binrw]
58pub struct Limit {
59    /// Left track limit
60    pub left: f32,
61
62    /// Right track limit
63    pub right: f32,
64}
65
66/// Node / or point on a track
67#[derive(Debug, Copy, Clone, Default, PartialEq)]
68#[binrw]
69pub struct Node {
70    /// Center point of this node
71    pub center: Point<i32>,
72
73    /// Expected direction of travel
74    pub direction: Point<f32>,
75
76    /// Track outer limit, relative to the center point and direction of travel
77    pub outer_limit: Limit,
78
79    /// Road limit, relative to the center point and direction of travel
80    pub road_limit: Limit,
81}
82
83impl Node {
84    /// Get the center point of this node, optionally scaled
85    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    /// Calculate the absolute position of the left and right road limits
96    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    /// Calculate the absolute position of the left and right track limits
101    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)]
140/// PTH file
141pub struct Pth {
142    /// File format version
143    pub version: u8,
144    /// File format revision
145    pub revision: u8,
146
147    #[bw(calc = nodes.len() as i32)]
148    num_nodes: i32,
149
150    /// Which node is the finishing line
151    pub finish_line_node: i32,
152
153    #[br(count = num_nodes)]
154    /// A list of nodes
155    pub nodes: Vec<Node>,
156}
157
158impl Pth {
159    /// Read and parse a PTH file into a [Pth] struct.
160    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    /// Read and parse a PTH file into a [Pth] struct.
165    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}