Skip to main content

f1_game_library_models_25/
lib.rs

1//! # F1 Game Library Models 25
2//!
3//! Data types and zero-copy deserialization for the UDP telemetry packets emitted
4//! by EA Sports F1 25. Based on the
5//! [official UDP specification](https://forums.ea.com/blog/f1-games-game-info-hub-en/ea-sports%E2%84%A2-f1%C2%AE25-udp-specification/12187347).
6//! Previous game versions are not supported.
7//!
8//! ## Quick start
9//! ```no_run
10//! use f1_game_library_models_25::parse;
11//!
12//! fn handle(buf: &[u8]) {
13//!     match parse::parse(buf) {
14//!         Ok(packet) => println!("{packet:?}"),
15//!         Err(e) => eprintln!("parse error: {e}"),
16//!     }
17//! }
18//! ```
19//!
20//! ## UDP client
21//! Enable the `client` feature for a ready-made async listener:
22//! ```ignore
23//! use f1_game_library_models_25::client::{TelemetryClient, HandlePacket, TelemetryControl};
24//! use f1_game_library_models_25::packets::lap_data::PacketLapData;
25//!
26//! struct MyHandler;
27//!
28//! impl HandlePacket for MyHandler {
29//!     async fn handle_lap_data(&mut self, p: PacketLapData) -> anyhow::Result<TelemetryControl> {
30//!         let frame = p.header.frame_identifier();
31//!         println!("lap data frame {frame}");
32//!         Ok(TelemetryControl::Continue)
33//!     }
34//! }
35//!
36//! #[tokio::main(flavor = "current_thread")]
37//! async fn main() -> anyhow::Result<()> {
38//!     TelemetryClient::new("0.0.0.0:20777").await?.listen(&mut MyHandler).await
39//! }
40//! ```
41//!
42//! ## Disclaimer
43//! Not affiliated with Formula 1, the FIA, any F1 team, or EA/Codemasters.
44//! Team names and identifiers are derived from the publicly available UDP specification
45//! and are used solely for data interoperability.
46
47pub mod endian;
48pub mod enums;
49pub mod macros;
50pub mod packet_id;
51pub mod packets;
52pub mod parse;
53pub mod wheel_data;
54
55#[cfg(feature = "client")]
56pub mod client;
57
58pub use endian::FixEndianness;
59pub use parse::parse;
60pub use wheel_data::WheelData;
61
62use std::mem::size_of;
63
64use crate::constants::PACKET_HEADER_SIZE;
65use macros::{wire_field_accessors, wire_index_accessors};
66
67/// Wire-format packet header — the 29-byte prefix common to every packet.
68///
69/// All fields are primitive types; `packet_id` is a raw `u8` rather than an
70/// enum so that no invalid-discriminant UB is possible when casting from bytes.
71#[derive(Debug, Clone, Copy)]
72#[repr(C, packed)]
73pub struct PacketHeader {
74    packet_format: u16,
75    game_year: u8,
76    game_major_version: u8,
77    game_minor_version: u8,
78    packet_version: u8,
79    /// Raw packet-id discriminant. See [`packet_id::PacketId`] for values.
80    packet_id: u8,
81    session_uid: u64,
82    session_time: f32,
83    frame_identifier: u32,
84    overall_frame_identifier: u32,
85    player_car_index: u8,
86    /// `255` if no second player (split-screen).
87    secondary_player_car_index: u8,
88}
89
90const _: () = assert!(size_of::<PacketHeader>() == PACKET_HEADER_SIZE);
91
92/// A typed UDP packet: the 29-byte wire header followed by a packet-specific payload.
93///
94/// `T` is the wire-format payload struct. It must be `Copy` and `repr(C, packed)`
95/// so that the overall `Packet<T>` has no hidden padding.
96#[derive(Debug, Clone, Copy)]
97#[repr(C, packed)]
98pub struct Packet<T> {
99    pub header: PacketHeader,
100    pub payload: T,
101}
102
103impl PacketHeader {
104    /// Decode the raw packet-id byte into the typed [`packet_id::PacketId`] enum.
105    ///
106    /// Returns `Err(raw)` if the byte doesn't match any known variant.
107    pub fn packet_id(self) -> Result<packet_id::PacketId, u8> {
108        packet_id::PacketId::try_from(self.packet_id).map_err(|e| e.number)
109    }
110
111    wire_index_accessors!(player_car_index, secondary_player_car_index);
112
113    wire_field_accessors!(
114        packet_format: u16,
115        game_year: u8,
116        game_major_version: u8,
117        game_minor_version: u8,
118        packet_version: u8,
119        session_uid: u64,
120        session_time: f32,
121        frame_identifier: u32,
122        overall_frame_identifier: u32,
123    );
124}
125
126impl FixEndianness for PacketHeader {
127    fn fix_endianness(self) -> Self {
128        Self {
129            packet_format: self.packet_format.fix_endianness(),
130            session_uid: self.session_uid.fix_endianness(),
131            session_time: self.session_time.fix_endianness(),
132            frame_identifier: self.frame_identifier.fix_endianness(),
133            overall_frame_identifier: self.overall_frame_identifier.fix_endianness(),
134            ..self
135        }
136    }
137}
138
139impl<T: Copy + FixEndianness> FixEndianness for Packet<T> {
140    fn fix_endianness(self) -> Self {
141        Self {
142            header: self.header.fix_endianness(),
143            payload: self.payload.fix_endianness(),
144        }
145    }
146}
147
148pub mod constants {
149    pub const MAX_CARS_IN_SESSION: usize = 22;
150    pub const MAX_TYRE_SETS: usize = 20; // 13 slick + 7 wet
151    pub const PACKET_HEADER_SIZE: usize = 29;
152    pub const PARTICIPANT_PACKET_SIZE: usize = 1284;
153    pub const CAR_SETUP_PACKET_SIZE: usize = 1133;
154    pub const CAR_STATUS_PACKET_SIZE: usize = 1239;
155    pub const LAP_DATA_PACKET_SIZE: usize = 1285;
156    pub const MOTION_DATA_PACKET_SIZE: usize = 1349;
157    pub const EXTENDED_MOTION_DATA_PACKET_SIZE: usize = 273;
158    pub const TELEMETRY_DATA_PACKET_SIZE: usize = 1352;
159    pub const CLASSIFICATION_DATA_PACKET_SIZE: usize = 1042;
160    pub const CAR_DAMAGE_DATA_PACKET_SIZE: usize = 1041;
161    pub const SESSION_DATA_PACKET_SIZE: usize = 753;
162    pub const SESSION_HISTORY_DATA_PACKET_SIZE: usize = 1460;
163    pub const EVENT_DATA_PACKET_SIZE: usize = 45;
164    pub const LOBBY_INFO_DATA_PACKET_SIZE: usize = 954;
165    pub const TYRE_SETS_DATA_PACKET_SIZE: usize = 231;
166    pub const TIME_TRIAL_DATA_PACKET_SIZE: usize = 101;
167    pub const LAP_POSITIONS_DATA_PACKET_SIZE: usize = 1131;
168}