f1_game_packet_parser/lib.rs
1//! Convert binary data from F1 24, F1 23, and F1 22 UDP telemetry into organised structs.
2//! ## Getting started
3//!
4//! Add `f1-game-packet-parser` to your project by running the following command:
5//!
6//! ```sh
7//! cargo add f1-game-packet-parser
8//! ```
9//!
10//! ### Basic UDP client
11//!
12//! This crate doesn't provide a UDP client out of the box.
13//! Here's how to write one that will parse and pretty-print incoming packets:
14//!
15//! ```no_run
16//! use f1_game_packet_parser::parse;
17//! use std::error::Error;
18//! use std::net::UdpSocket;
19//!
20//! fn main() -> Result<(), Box<dyn Error>> {
21//!     // This IP and port should be set in the game's options by default.
22//!     let socket = UdpSocket::bind("127.0.0.1:20777")?;
23//!     let mut buf = [0u8; 1464];
24//!
25//!     loop {
26//!         // Receive raw packet data from the game.
27//!         // The buf array should be large enough for all types of packets.
28//!         let (amt, _) = socket.recv_from(&mut buf)?;
29//!
30//!         // Convert received bytes to an F1Packet struct and print it.
31//!         let packet = parse(&buf[..amt])?;
32//!         println!("{:#?}", packet);
33//!     }
34//! }
35//! ```
36//!
37//! ### Determining a packet's type and extracting its payload
38//!
39//! An [`F1Packet`] consists of a universal [`header`](field@F1Packet::header)
40//! and an [`Option`] field for a payload of every single packet type.
41//! Only one of these can be set to [`Some`] for a given [`F1Packet`] instance.
42//!
43//! Therefore, you can use the following if-else-if chain to
44//! differentiate between all packet types and extract their payloads.
45//! Of course, you can remove the branches you don't need.
46//!
47//! ```ignore
48//! use f1_game_packet_parser::parse;
49//!
50//! let placeholder_data = include_bytes!("placeholder.bin");
51//! let packet = parse(placeholder_data)?;
52//!
53//! if let Some(motion) = &packet.motion {
54//!     // Do whatever with motion.
55//! } else if let Some(session) = &packet.session {
56//!     // Do whatever with session.
57//! } else if let Some(laps) = &packet.laps {
58//!     // Do whatever with laps.
59//! } else if let Some(event) = &packet.event {
60//!     // Do whatever with event.
61//! } else if let Some(participants) = &packet.participants {
62//!     // Do whatever with participants.
63//! } else if let Some(car_setups) = &packet.car_setups {
64//!     // Do whatever with car_setups.
65//! } else if let Some(car_telemetry) = &packet.car_telemetry {
66//!     // Do whatever with car_telemetry.
67//! } else if let Some(car_status) = &packet.car_status {
68//!     // Do whatever with car_status.
69//! } else if let Some(final_classification) = &packet.final_classification {
70//!     // Do whatever with final_classification.
71//! } else if let Some(lobby) = &packet.lobby {
72//!     // Do whatever with lobby.
73//! } else if let Some(car_damage) = &packet.car_damage {
74//!     // Do whatever with car_damage.
75//! } else if let Some(session_history) = &packet.session_history {
76//!     // Do whatever with session_history.
77//! } else if let Some(tyre_sets) = &packet.tyre_sets {
78//!     // Available from the 2023 format onwards.
79//!     // Do whatever with tyre_sets.
80//! } else if let Some(motion_ex) = &packet.motion_ex {
81//!     // Available from the 2023 format onwards.
82//!     // Do whatever with motion_ex.
83//! } else if let Some(time_trial) = &packet.time_trial {
84//!     // Available from the 2024 format onwards.
85//!     // Do whatever with time_trial.
86//! }
87//! ```
88//!
89//! ### Working with [event packets](F1PacketEvent)
90//!
91//! [`F1PacketEvent`] is unique among other kinds of packets.
92//! Its payload consists of a 4-letter code that determines
93//! the type of the event, followed by optional details
94//! about this event.
95//!
96//! These extra details are represented by the [`EventDetails`](enum@packets::event::EventDetails)
97//! enum (even if a certain event doesn't come with additional data).
98//! You can import the enum and use a matcher to determine an event's type
99//! and extract its payload (if available) like so:
100//!
101//! ```ignore
102//! use f1_game_packet_parser::packets::event::EventDetails;
103//! use f1_game_packet_parser::parse;
104//!
105//! let placeholder_data = include_bytes!("placeholder.bin");
106//! let packet = parse(placeholder_data)?;
107//!
108//! if let Some(event) = &packet.event {
109//!     match event.details {
110//!         /// Event with no extra details.
111//!         EventDetails::LightsOut => {
112//!             println!("It's lights out, and away we go!");
113//!         }
114//!         /// You can skip the details if you don't need them.
115//!         EventDetails::Flashback { .. } => {
116//!             println!("Flashback has been triggered!");
117//!         }
118//!         /// Extracting details from an event.
119//!         EventDetails::RaceWinner { vehicle_index } => {
120//!             println!(
121//!                 "Driver at index {} is the winner!",
122//!                 vehicle_index
123//!             );
124//!         }
125//!         _ => (),
126//!     }
127//! }
128//! ```
129//!
130//! ### Working with bitmaps
131//!
132//! There are 3 fields that use a [`bitflags`]-powered bitmap struct:
133//!
134//! - [`EventDetails::Buttons::button_status`](field@packets::event::EventDetails::Buttons::button_status)
135//! - [`CarTelemetryData::rev_lights_bit_value`](field@packets::car_telemetry::CarTelemetryData::rev_lights_bit_value)
136//! - [`LapHistoryData::lap_valid_bit_flags`](field@packets::session_history::LapHistoryData::lap_valid_bit_flags)
137//!
138//! Each bitmap struct is publicly available via the [`constants`] module
139//! and comes with a handful of constants representing specific bit values,
140//! as well as methods and operator overloads for common bit operations.
141//!
142//! Here's an example that checks if a given binary file is
143//! a [car telemetry packet](F1PacketCarTelemetry).
144//! If so, it will grab player car's telemetry data
145//! and determine whether the revs are high, medium or low
146//! based on the specific bit values being set.
147//!
148//! ```ignore
149//! use f1_game_packet_parser::constants::RevLights;
150//! use f1_game_packet_parser::parse;
151//!
152//! let placeholder_data = include_bytes!("placeholder.bin");
153//! let packet = parse(placeholder_data)?;
154//! let player_car_index = packet.header.player_car_index;
155//!
156//! if let Some(car_telemetry) = &packet.car_telemetry {
157//!     let player = car_telemetry.data[player_car_index];
158//!     let is_high_rev =
159//!         player.rev_lights_bit_value.contains(RevLights::RIGHT_1);
160//!     let is_medium_rev =
161//!         player.rev_lights_bit_value.contains(RevLights::MIDDLE_1);
162//!
163//!     let revs_desc = if is_high_rev {
164//!         "High"
165//!     } else if is_medium_rev {
166//!         "Medium"
167//!     } else {
168//!         "Low"
169//!     };
170//!
171//!     println!("{} revs", revs_desc);
172//! }
173//! ```
174//!
175//! See the respective structs' documentation for a complete list of constants and methods.
176//!
177//! ## Original documentation links
178//!
179//! - [F1 24](https://forums.ea.com/discussions/f1-24-general-discussion-en/f1-24-udp-specification/8369125)
180//! - [F1 23](https://forums.ea.com/discussions/f1-23-en/f1-23-udp-specification/8390745)
181//! - [F1 22](https://forums.ea.com/discussions/f1-games-franchise-discussion-en/f1-22-udp-specification/8418392)
182
183/// Contains appendix constants and enums for various packet-specific struct field values.
184pub mod constants;
185/// Contains structures for each kind of packet payload
186/// and submodules for packet-specific structs.
187pub mod packets;
188
189use crate::constants::PacketId;
190use crate::packets::{
191    u8_to_usize, F1PacketCarDamage, F1PacketCarSetups, F1PacketCarStatus,
192    F1PacketCarTelemetry, F1PacketEvent, F1PacketFinalClassification, F1PacketLaps,
193    F1PacketLobby, F1PacketMotion, F1PacketMotionEx, F1PacketParticipants,
194    F1PacketSession, F1PacketSessionHistory, F1PacketTimeTrial, F1PacketTyreSets,
195};
196
197use binrw::io::Cursor;
198use binrw::{BinRead, BinReaderExt, BinResult};
199use serde::{Deserialize, Serialize};
200
201/// Attempts to extract F1 game packet data from a byte buffer
202/// (such as a [`Vec<u8>`], [`[u8; N]`](array), or [`&[u8]`](slice)).
203///
204/// ## Errors
205///
206/// - [`binrw::Error::AssertFail`] when a certain field's value
207///   is outside the expected range. This generally applies to
208///   `_index` fields, percentage values, and fields that
209///   have the aforementioned range specified in their documentation
210/// - [`binrw::Error::BadMagic`] when [`F1PacketEvent`] has a code
211///   that doesn't match any [known event type](packets::event::EventDetails)
212/// - [`binrw::Error::Custom`] when the parser encounters an invalid bool value
213///   in a read byte (i.e. neither 0, nor 1)
214/// - [`binrw::Error::EnumErrors`] when there's no matching value for an enum field
215///
216/// ## Examples
217///
218/// ### Basic UDP client
219///
220/// ```no_run
221/// use f1_game_packet_parser::parse;
222/// use std::error::Error;
223/// use std::net::UdpSocket;
224///
225/// fn main() -> Result<(), Box<dyn Error>> {
226///     // This IP and port should be set in the game's options by default.
227///     let socket = UdpSocket::bind("127.0.0.1:20777")?;
228///     let mut buf = [0u8; 1464];
229///
230///     loop {
231///         // Receive raw packet data from the game.
232///         // The buf array should be large enough for all types of packets.
233///         let (amt, _) = socket.recv_from(&mut buf)?;
234///
235///         // Convert received bytes to an F1Packet struct and print it.
236///         let packet = parse(&buf[..amt])?;
237///         println!("{:#?}", packet);
238///     }
239/// }
240/// ```
241///
242/// ### Invalid/unsupported packet format
243///
244/// ```
245/// let invalid_format = 2137u16.to_le_bytes();
246/// let parse_result = f1_game_packet_parser::parse(invalid_format);
247///
248/// assert!(parse_result.is_err());
249/// assert_eq!(
250///     parse_result.unwrap_err().root_cause().to_string(),
251///     "Invalid or unsupported packet format: 2137 at 0x0"
252/// );
253/// ```
254pub fn parse<T: AsRef<[u8]>>(data: T) -> BinResult<F1Packet> {
255    let mut cursor = Cursor::new(data);
256    let packet: F1Packet = cursor.read_le()?;
257
258    Ok(packet)
259}
260
261/// Structured representation of raw F1 game packet data that's
262/// returned as a successful result of the [`parse`] function.
263///
264/// Each [`Option`] field acts as a slot for a payload of a packet of a certain type.
265/// Only one of these fields can be [`Some`] for a given `F1Packet` instance.
266#[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)]
267#[br(little)]
268pub struct F1Packet {
269    /// Universal packet header.
270    pub header: F1PacketHeader,
271    /// Physics data for all cars in the ongoing session.
272    #[br(if(header.packet_id == PacketId::Motion), args(header.packet_format))]
273    pub motion: Option<F1PacketMotion>,
274    /// Data about the ongoing session.
275    #[br(if(header.packet_id == PacketId::Session), args(header.packet_format))]
276    pub session: Option<F1PacketSession>,
277    /// Lap data for all cars on track.
278    #[br(if(header.packet_id == PacketId::Laps), args(header.packet_format))]
279    pub laps: Option<F1PacketLaps>,
280    /// Details of events that happen during the course of the ongoing session.
281    #[br(if(header.packet_id == PacketId::Event), args(header.packet_format))]
282    pub event: Option<F1PacketEvent>,
283    /// List of participants in the session.
284    #[br(if(header.packet_id == PacketId::Participants), args(header.packet_format))]
285    pub participants: Option<F1PacketParticipants>,
286    /// Setup data for all cars in the ongoing session.
287    #[br(if(header.packet_id == PacketId::CarSetups), args(header.packet_format))]
288    pub car_setups: Option<F1PacketCarSetups>,
289    /// Telemetry data for all cars in the ongoing session.
290    #[br(if(header.packet_id == PacketId::CarTelemetry), args(header.packet_format))]
291    pub car_telemetry: Option<F1PacketCarTelemetry>,
292    /// Status data for all cars in the ongoing session.
293    #[br(if(header.packet_id == PacketId::CarStatus), args(header.packet_format))]
294    pub car_status: Option<F1PacketCarStatus>,
295    /// Final classification confirmation at the end of the session.
296    #[br(
297        if(header.packet_id == PacketId::FinalClassification),
298        args(header.packet_format)
299    )]
300    pub final_classification: Option<F1PacketFinalClassification>,
301    /// Details of players in a multiplayer lobby.
302    #[br(if(header.packet_id == PacketId::LobbyInfo), args(header.packet_format))]
303    pub lobby: Option<F1PacketLobby>,
304    /// Car damage parameters for all cars in the ongoing session.
305    #[br(if(header.packet_id == PacketId::CarDamage), args(header.packet_format))]
306    pub car_damage: Option<F1PacketCarDamage>,
307    /// Session history data for a specific car.
308    #[br(if(header.packet_id == PacketId::SessionHistory), args(header.packet_format))]
309    pub session_history: Option<F1PacketSessionHistory>,
310    /// Details of tyre sets assigned to a vehicle during the session.
311    /// Available from the 2023 format onwards.
312    #[br(if(header.packet_id == PacketId::TyreSets), args(header.packet_format))]
313    pub tyre_sets: Option<F1PacketTyreSets>,
314    /// Extended player car only motion data.
315    /// Available from the 2023 format onwards.
316    #[br(if(header.packet_id == PacketId::MotionEx), args(header.packet_format))]
317    pub motion_ex: Option<F1PacketMotionEx>,
318    /// Extra information that's only relevant to the time trial game mode.
319    /// Available from the 2024 format onwards.
320    #[br(if(header.packet_id == PacketId::TimeTrial), args(header.packet_format))]
321    pub time_trial: Option<F1PacketTimeTrial>,
322}
323
324/// F1 game packet's header. It contains metadata about the game,
325/// the ongoing session, the frame this packet was sent on, and player car indexes.
326#[non_exhaustive]
327#[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)]
328#[br(little)]
329pub struct F1PacketHeader {
330    /// Value of the "UDP Format" option in the game's telemetry settings.
331    /// This crate currently supports formats in range `(2022..=2024)`.
332    #[br(
333        assert(
334            (2022..=2024).contains(&packet_format),
335            "Invalid or unsupported packet format: {}",
336            packet_format
337        )
338    )]
339    pub packet_format: u16,
340    /// Game year (last two digits).
341    /// Available from the 2023 format onwards.
342    #[br(if(packet_format >= 2023))]
343    pub game_year: u8,
344    /// Game's major version - "X.00".
345    pub game_major_version: u8,
346    /// Game's minor version - "1.XX".
347    pub game_minor_version: u8,
348    /// Version of this packet type, all start from 1.
349    pub packet_version: u8,
350    /// Unique identifier for the packet type.
351    pub packet_id: PacketId,
352    /// Unique identifier for the session.
353    pub session_uid: u64,
354    /// Session timestamp.
355    pub session_time: f32,
356    /// Identifier for the frame the data was retrieved on.
357    /// Goes back after a flashback is triggered.
358    pub frame_identifier: u32,
359    /// Overall identifier for the frame the data was retrieved on
360    /// (i.e. it doesn't go back after flashbacks).
361    /// Available from the 2023 format onwards.
362    #[br(if(packet_format >= 2023))]
363    pub overall_frame_identifier: u32,
364    /// Index of player 1's car (255 if in spectator mode).
365    #[br(map(u8_to_usize))]
366    pub player_car_index: usize,
367    /// Index of player 2's car in splitscreen mode.
368    /// Set to 255 if not in splitscreen mode.
369    #[br(map(u8_to_usize))]
370    pub secondary_player_car_index: usize,
371}