diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b6b286 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# f1-game-packet-parser + +This is a Rust crate that allows you to convert raw binary data from F1 24, F1 23, and F1 22 UDP telemetry into organised structs. + +## Getting started + +Add `f1_game_packet_parser` to your project's `Cargo.toml` file: + +```toml +[dependencies] +f1_game_packet_parser = "1.0.0" +``` + +## Example + +This crate doesn't provide a UDP client out of the box. Here's how to write one that will parse and pretty-print incoming packets: + +```rust +use f1_game_packet_parser::parse; +use std::error::Error; +use std::net::UdpSocket; + +fn main() -> Result<(), Box> { + // This IP and port should be set in the game's options by default. + let socket = UdpSocket::bind("127.0.0.1:20777")?; + let mut buf = [0u8; 1460]; + + loop { + // Receive raw packet data from the game. + // The buf array should be large enough for all types of packets. + let (amt, _) = socket.recv_from(&mut buf)?; + + // Convert received bytes to an F1Packet struct and print it. + let packet = parse(&buf[..amt])?; + println!("{:#?}", packet); + } +} +``` + +## Minimum supported Rust version + +The minimum supported Rust version is documented in the Cargo.toml file. This may be bumped in minor releases if necessary. + +## Original documentation links + +- [F1 24](https://forums.ea.com/discussions/f1-24-general-discussion-en/f1-24-udp-specification/8369125) +- [F1 23](https://forums.ea.com/discussions/f1-23-en/f1-23-udp-specification/8390745) +- [F1 22](https://forums.ea.com/discussions/f1-games-franchise-discussion-en/f1-22-udp-specification/8418392) + +## License + +MIT diff --git a/src/constants/mod.rs b/src/constants/mod.rs index 73fce6a..fcf137e 100644 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -1,8 +1,16 @@ +/// Unique identifiers of drivers. pub mod driver_id; -pub mod team_id; -/// The wheel order is: `REAR_LEFT`, `REAR_RIGHT`, `FRONT_LEFT`, `FRONT_RIGHT`. -pub mod wheel_index; +/// Unique identifiers of session types. pub mod session_type; +/// Unique identifiers of teams. +pub mod team_id; +/// Indexes of wheels in wheel-oriented arrays. +/// The order is: +/// [`REAR_LEFT`](const@wheel_index::REAR_LEFT), +/// [`REAR_RIGHT`](const@wheel_index::REAR_RIGHT), +/// [`FRONT_LEFT`](const@wheel_index::FRONT_LEFT), +/// [`FRONT_RIGHT`](const@wheel_index::FRONT_RIGHT). +pub mod wheel_index; use binrw::BinRead; use bitflags::bitflags; @@ -10,6 +18,8 @@ use serde::{Deserialize, Serialize}; pub(crate) const MAX_NUM_CARS: usize = 22; +/// Unique identifier of the type of this packet. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -28,7 +38,7 @@ pub(crate) const MAX_NUM_CARS: usize = 22; pub enum PacketId { Motion = 0, Session = 1, - LapData = 2, + Laps = 2, Event = 3, Participants = 4, CarSetups = 5, @@ -43,6 +53,9 @@ pub enum PacketId { TimeTrial = 14, } +/// Flag that's currently being waved in +/// a [`MarshalZone`](crate::packets::session::MarshalZone). +/// Represents an [`i8`]. #[non_exhaustive] #[derive( BinRead, @@ -67,6 +80,7 @@ pub enum MarshalZoneFlag { Red = 4, } +/// Session/forecast weather type. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -91,6 +105,7 @@ pub enum Weather { Storm = 5, } +/// Temperature change direction. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -112,6 +127,7 @@ pub enum TemperatureChange { NoChange = 2, } +/// Unique circuit ID. Represents an [`i8`]. #[non_exhaustive] #[derive( BinRead, @@ -128,42 +144,79 @@ pub enum TemperatureChange { )] #[br(little, repr(i8))] pub enum TrackId { + /// Unknown circuit. Unknown = -1, - Melbourne = 0, + /// Australian Grand Prix. + AlbertPark = 0, + /// French Grand Prix. PaulRicard = 1, + /// Chinese Grand Prix. Shanghai = 2, + /// Bahrain Grand Prix. Sakhir = 3, + /// Spanish Grand Prix. Catalunya = 4, - Monaco = 5, + /// Monaco Grand Prix. + MonteCarlo = 5, + /// Canadian Grand Prix. Montreal = 6, + /// British Grand Prix. Silverstone = 7, + /// German Grand Prix. Hockenheim = 8, + /// Hungarian Grand Prix. Hungaroring = 9, + /// Belgian Grand Prix. Spa = 10, + /// Italian Grand Prix. Monza = 11, - Singapore = 12, + /// Singapore Grand Prix. + MarinaBay = 12, + /// Japanese Grand Prix. Suzuka = 13, - AbuDhabi = 14, - Texas = 15, - Brazil = 16, - Austria = 17, + /// Abu Dhabi Grand Prix. + YasMarina = 14, + /// Circuit of the Americas. United States (Texas) Grand Prix. + Cota = 15, + /// Brazilian (Sao Paulo) Grand Prix. + Interlagos = 16, + /// Austrian Grand Prix. + RedBullRing = 17, + /// Russian Grand Prix. Sochi = 18, - Mexico = 19, + /// Mexican Grand Prix. + MexicoCity = 19, + /// Azerbaijan Grand Prix. Baku = 20, + /// Short variant of the [`Sakhir`](TrackId::Sakhir) circuit. SakhirShort = 21, + /// Short variant of the [`Silverstone`](TrackId::Silverstone) circuit. SilverstoneShort = 22, - TexasShort = 23, + /// Short variant of the [`Cota`](TrackId::Cota) circuit. + CotaShort = 23, + /// Short variant of the [`Suzuka`](TrackId::Suzuka) circuit. SuzukaShort = 24, + /// Vietnamese Grand Prix. Hanoi = 25, + /// Dutch Grand Prix. Zandvoort = 26, + /// ~~San Marino~~ Emilia-Romagna Grand Prix. Imola = 27, + /// Portuguese Grand Prix. Portimao = 28, + /// Saudi Arabian Grand Prix. Jeddah = 29, + /// Miami Grand Prix. Miami = 30, + /// Las Vegas Grand Prix. LasVegas = 31, + /// Qatar Grand Prix. Losail = 32, } +/// Type of cars being raced in +/// [`F1PacketSession`](struct@crate::F1PacketSession). +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -192,6 +245,8 @@ pub enum Formula { F1Elimination = 9, } +/// Safety car deployment status in [`F1PacketSession`](struct@crate::F1PacketSession). +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -214,6 +269,9 @@ pub enum SafetyCarStatus { FormationLap = 3, } +/// Accuracy of a +/// [`WeatherForecastSample`](struct@crate::packets::session::WeatherForecastSample). +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -234,6 +292,7 @@ pub enum ForecastAccuracy { Approximate = 1, } +/// Type of enabled braking assist. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -256,6 +315,7 @@ pub enum BrakingAssist { High = 3, } +/// Type of enabled gearbox assist. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -278,6 +338,7 @@ pub enum GearboxAssist { Automatic = 3, } +/// Type of enabled racing line assist. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -299,6 +360,7 @@ pub enum DynamicRacingLine { Full = 2, } +/// Shape of the racing line. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -319,6 +381,7 @@ pub enum DynamicRacingLineType { ThreeDimensional = 1, } +/// Game mode that's currently in use. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -359,6 +422,7 @@ pub enum GameMode { Benchmark = 127, } +/// Set of rules that's in use for this session. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -386,6 +450,7 @@ pub enum RuleSet { RivalDuel = 11, } +/// Length of the ongoing session. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -411,6 +476,7 @@ pub enum SessionLength { Full = 7, } +/// Whether the car is outside/entering/in the pit lane. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -432,6 +498,29 @@ pub enum PitStatus { InPitArea = 2, } +/// Zero-based sector number. Represents a [`u8`]. +#[non_exhaustive] +#[derive( + BinRead, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Debug, + Hash, + Serialize, + Deserialize, +)] +#[br(little, repr(u8))] +pub enum Sector { + First = 0, + Second = 1, + Third = 2, +} + +/// Status of a driver in the current session. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -455,6 +544,8 @@ pub enum DriverStatus { OnTrack = 4, } +/// Status of a driver's result in the current session and final classification. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -481,6 +572,7 @@ pub enum ResultStatus { Retired = 7, } +/// Type of penalty awarded to a driver. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -517,6 +609,7 @@ pub enum PenaltyType { BlackFlagTimer = 17, } +/// Type of offence commited by a driver. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -588,6 +681,9 @@ pub enum InfringementType { AttributeAssigned = 54, } +/// Bit flags of specific controller buttons being pressed +/// in a [`Buttons` event](variant@crate::packets::event::EventDetails::Buttons). +/// Represents a [`u32`]. #[derive( Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, )] @@ -672,6 +768,7 @@ bitflags! { } } +/// Unique identifier of a driver's nationality. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -781,6 +878,7 @@ pub enum Nationality { Filipino = 90, } +/// "Your telemetry" UDP setting value. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -801,6 +899,7 @@ pub enum YourTelemetry { Public = 1, } +/// Type of surface a tyre is on. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -831,6 +930,8 @@ pub enum Surface { Ridged = 11, } +/// Bit flags of lit rev lights on a steering wheel. +/// Represents a [`u16`]. #[derive( Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, )] @@ -871,6 +972,7 @@ bitflags! { } } +/// Index of currently open multi-function display panel. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -895,6 +997,7 @@ pub enum MfdPanelIndex { Closed = 255, } +/// Type of enabled traction control assist. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -916,6 +1019,7 @@ pub enum TractionControl { Full = 2, } +/// Type of fuel mix that's currently in use. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -938,6 +1042,7 @@ pub enum FuelMix { Max = 3, } +/// ERS deployment mode that's currently in use. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -960,6 +1065,7 @@ pub enum ErsDeployMode { Hotlap = 3, } +/// Flag the driver is currently being shown. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -984,6 +1090,7 @@ pub enum VehicleFiaFlag { Red = 4, } +/// Global DRS activation permission status. Represents an [`i8`]. #[non_exhaustive] #[derive( BinRead, @@ -1005,6 +1112,7 @@ pub enum DrsAllowed { Allowed = 1, } +/// Session-independent tyre compound type. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1022,14 +1130,14 @@ pub enum DrsAllowed { #[br(little, repr(u8))] pub enum ActualTyreCompound { Unknown = 0, - F1C5 = 16, - F1C4 = 17, - F1C3 = 18, - F1C2 = 19, - F1C1 = 20, - F1C0 = 21, - F1Inter = 7, - F1Wet = 8, + C5 = 16, + C4 = 17, + C3 = 18, + C2 = 19, + C1 = 20, + C0 = 21, + Inter = 7, + Wet = 8, ClassicDry = 9, ClassicWet = 10, F2SuperSoft = 11, @@ -1039,6 +1147,8 @@ pub enum ActualTyreCompound { F2Wet = 15, } +/// Visual indicator of a tyre compound's type in a given session. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1070,6 +1180,7 @@ pub enum VisualTyreCompound { F2Wet = 15, } +/// Readiness of a player in an online lobby. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1091,6 +1202,8 @@ pub enum ReadyStatus { Spectating = 2, } +/// Bit flags of lap validity across all three sectors and overall. +/// Represents a [`u8`]. #[derive( Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, )] @@ -1109,6 +1222,7 @@ bitflags! { } } +/// Speed unit used by a player. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1129,6 +1243,7 @@ pub enum SpeedUnit { KilometresPerHour = 1, } +/// Temperature unit used by a player. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1149,6 +1264,8 @@ pub enum TemperatureUnit { Fahrenheit = 1, } +/// Console or PC game distribution platform used by a player. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1173,6 +1290,8 @@ pub enum Platform { Unknown = 255, } +/// Recovery mode assist that's currently enabled. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1194,6 +1313,8 @@ pub enum RecoveryMode { AutoRecovery = 2, } +/// Flashback usage limit that's currently enabled. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1216,6 +1337,8 @@ pub enum FlashbackLimit { Unlimited = 3, } +/// Type of surface simulation that's currently enabled. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1231,11 +1354,12 @@ pub enum FlashbackLimit { Deserialize, )] #[br(little, repr(u8))] -pub enum SurfaceType { +pub enum SurfaceSimType { Simplified = 0, Realistic = 1, } +/// Difficulty of driving with low fuel. Represent a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1256,6 +1380,8 @@ pub enum LowFuelMode { Hard = 1, } +/// Race starts assist that's currently in use. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1276,6 +1402,8 @@ pub enum RaceStarts { Assisted = 1, } +/// Type of tyre temperature simulation that's currently in use. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1296,6 +1424,8 @@ pub enum TyreTemperature { SurfaceAndCarcass = 1, } +/// Type of car damage simulation that's currently in use. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1318,6 +1448,7 @@ pub enum CarDamage { Simulation = 3, } +/// Car damage severity. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1339,6 +1470,8 @@ pub enum CarDamageRate { Simulation = 2, } +/// Type of collision simulation that's currently enabled. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1360,6 +1493,8 @@ pub enum Collisions { On = 2, } +/// Type of corner cutting and track limits punishability. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1380,6 +1515,7 @@ pub enum CornerCuttingStringency { Strict = 1, } +/// The way the game handles pit stops. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1401,6 +1537,8 @@ pub enum PitStopExperience { Immersive = 2, } +/// The likelihood of safety car getting deployed with hazard on track. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1416,13 +1554,14 @@ pub enum PitStopExperience { Deserialize, )] #[br(little, repr(u8))] -pub enum SafetyCar { +pub enum SafetyCarIntensity { Off = 0, Reduced = 1, Standard = 2, Increased = 3, } +/// The way the game handles safety car periods. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1444,6 +1583,7 @@ pub enum SafetyCarExperience { Unknown = 255, } +/// The way the game handles formation laps. Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1465,6 +1605,8 @@ pub enum FormationLapExperience { Unknown = 255, } +/// The likelihood of the game using a red flag after a serious incident. +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1480,13 +1622,16 @@ pub enum FormationLapExperience { Deserialize, )] #[br(little, repr(u8))] -pub enum RedFlags { +pub enum RedFlagIntensity { Off = 0, Reduced = 1, Standard = 2, Increased = 3, } +/// Type of safety car being deployed in a +/// [`SafetyCar` event](variant@crate::packets::event::EventDetails::SafetyCar). +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, @@ -1509,6 +1654,8 @@ pub enum SafetyCarType { FormationLap = 3, } +/// Type of [`SafetyCar` event](variant@crate::packets::event::EventDetails::SafetyCar). +/// Represents a [`u8`]. #[non_exhaustive] #[derive( BinRead, diff --git a/src/constants/team_id.rs b/src/constants/team_id.rs index 142476d..9d7b673 100644 --- a/src/constants/team_id.rs +++ b/src/constants/team_id.rs @@ -11,16 +11,16 @@ pub const MCLAREN: u8 = 8; pub const ALFA_ROMEO: u8 = 9; pub const SAUBER: u8 = 9; pub const F1_GENERIC: u8 = 41; -pub const MERCEDES_2020: u8 = 85; -pub const FERRARI_2020: u8 = 86; -pub const RED_BULL_2020: u8 = 87; -pub const WILLIAMS_2020: u8 = 88; -pub const RACING_POINT_2020: u8 = 89; -pub const RENAULT_2020: u8 = 90; -pub const ALPHA_TAURI_2020: u8 = 91; -pub const HAAS_2020: u8 = 92; -pub const MCLAREN_2020: u8 = 93; -pub const ALFA_ROMEO_2020: u8 = 94; +pub const BRAKING_POINT_MERCEDES_2020: u8 = 85; +pub const BRAKING_POINT_FERRARI_2020: u8 = 86; +pub const BRAKING_POINT_RED_BULL_2020: u8 = 87; +pub const BRAKING_POINT_WILLIAMS_2020: u8 = 88; +pub const BRAKING_POINT_RACING_POINT_2020: u8 = 89; +pub const BRAKING_POINT_RENAULT_2020: u8 = 90; +pub const BRAKING_POINT_ALPHA_TAURI_2020: u8 = 91; +pub const BRAKING_POINT_HAAS_2020: u8 = 92; +pub const BRAKING_POINT_MCLAREN_2020: u8 = 93; +pub const BRAKING_POINT_ALFA_ROMEO_2020: u8 = 94; pub const ASTON_MARTIN_DB11_V12: u8 = 95; pub const ASTON_MARTIN_VANTAGE: u8 = 96; pub const ASTON_MARTIN_SAFETY_CAR: u8 = 97; @@ -54,18 +54,18 @@ pub const F1_22_DAMS_2022: u8 = 125; pub const F1_22_CAMPOS_2022: u8 = 126; pub const F1_22_VAN_AMERSFOORT_2022: u8 = 127; pub const F1_22_TRIDENT_2022: u8 = 128; -pub const MERCEDES_2022: u8 = 118; -pub const FERRARI_2022: u8 = 119; -pub const RED_BULL_2022: u8 = 120; -pub const WILLIAMS_2022: u8 = 121; -pub const ASTON_MARTIN_2022: u8 = 122; -pub const ALPINE_2022: u8 = 123; -pub const ALPHA_TAURI_2022: u8 = 124; -pub const HAAS_2022: u8 = 125; -pub const MCLAREN_2022: u8 = 126; -pub const ALFA_ROMEO_2022: u8 = 127; -pub const KONNERSPORT_2022: u8 = 128; -pub const KONNERSPORT: u8 = 129; +pub const BRAKING_POINT_MERCEDES_2022: u8 = 118; +pub const BRAKING_POINT_FERRARI_2022: u8 = 119; +pub const BRAKING_POINT_RED_BULL_2022: u8 = 120; +pub const BRAKING_POINT_WILLIAMS_2022: u8 = 121; +pub const BRAKING_POINT_ASTON_MARTIN_2022: u8 = 122; +pub const BRAKING_POINT_ALPINE_2022: u8 = 123; +pub const BRAKING_POINT_ALPHA_TAURI_2022: u8 = 124; +pub const BRAKING_POINT_HAAS_2022: u8 = 125; +pub const BRAKING_POINT_MCLAREN_2022: u8 = 126; +pub const BRAKING_POINT_ALFA_ROMEO_2022: u8 = 127; +pub const BRAKING_POINT_KONNERSPORT_2022: u8 = 128; +pub const BRAKING_POINT_KONNERSPORT_2023: u8 = 129; pub const F1_23_PREMA_2022: u8 = 130; pub const F1_23_VIRTUOSI_2022: u8 = 131; pub const F1_23_CARLIN_2022: u8 = 132; diff --git a/src/lib.rs b/src/lib.rs index ab822be..69aef1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,195 @@ +//! Convert raw binary data from F1 24, F1 23, and F1 22 UDP telemetry into organised structs. +//! ## Getting started +//! +//! Add `f1_game_packet_parser` to your project's `Cargo.toml` file: +//! +//! ```toml +//! [dependencies] +//! f1_game_packet_parser = "1.0.0" +//! ``` +//! +//! ### Basic UDP client +//! +//! This crate doesn't provide a UDP client out of the box. +//! Here's how to write one that will parse and pretty-print incoming packets: +//! +//! ```no_run +//! use f1_game_packet_parser::parse; +//! use std::error::Error; +//! use std::net::UdpSocket; +//! +//! fn main() -> Result<(), Box> { +//! // This IP and port should be set in the game's options by default. +//! let socket = UdpSocket::bind("127.0.0.1:20777")?; +//! let mut buf = [0u8; 1460]; +//! +//! loop { +//! // Receive raw packet data from the game. +//! // The buf array should be large enough for all types of packets. +//! let (amt, _) = socket.recv_from(&mut buf)?; +//! +//! // Convert received bytes to an F1Packet struct and print it. +//! let packet = parse(&buf[..amt])?; +//! println!("{:#?}", packet); +//! } +//! } +//! ``` +//! +//! ### Determining a packet's type and extracting its payload +//! +//! An [`F1Packet`] consists of a universal [`header`](field@F1Packet::header) +//! and an [`Option`] field for a payload of every single packet type. +//! Only one of these can be set to [`Some`] for a given [`F1Packet`] instance. +//! +//! Therefore, you can use the following if-else-if chain to +//! differentiate between all packet types and extract their payloads. +//! Of course, you can remove the branches you don't need. +//! +//! ```no_run +//! use f1_game_packet_parser::parse; +//! +//! let placeholder_data = include_bytes!("placeholder.bin"); +//! let packet = parse(placeholder_data)?; +//! +//! if let Some(motion) = &packet.motion { +//! // Do whatever with motion. +//! } else if let Some(session) = &packet.session { +//! // Do whatever with session. +//! } else if let Some(laps) = &packet.laps { +//! // Do whatever with laps. +//! } else if let Some(event) = &packet.event { +//! // Do whatever with event. +//! } else if let Some(participants) = &packet.participants { +//! // Do whatever with participants. +//! } else if let Some(car_setups) = &packet.car_setups { +//! // Do whatever with car_setups. +//! } else if let Some(car_telemetry) = &packet.car_telemetry { +//! // Do whatever with car_telemetry. +//! } else if let Some(car_status) = &packet.car_status { +//! // Do whatever with car_status. +//! } else if let Some(final_classification) = &packet.final_classification { +//! // Do whatever with final_classification. +//! } else if let Some(lobby) = &packet.lobby { +//! // Do whatever with lobby. +//! } else if let Some(car_damage) = &packet.car_damage { +//! // Do whatever with car_damage. +//! } else if let Some(session_history) = &packet.session_history { +//! // Do whatever with session_history. +//! } else if let Some(tyre_sets) = &packet.tyre_sets { +//! // Available from the 2023 format onwards. +//! // Do whatever with tyre_sets. +//! } else if let Some(motion_ex) = &packet.motion_ex { +//! // Available from the 2023 format onwards. +//! // Do whatever with motion_ex. +//! } else if let Some(time_trial) = &packet.time_trial { +//! // Available from the 2024 format onwards. +//! // Do whatever with time_trial. +//! } +//! ``` +//! +//! ### Working with [event packets](F1PacketEvent) +//! +//! [`F1PacketEvent`] is unique among other kinds of packets. +//! Its payload consists of a 4-letter code that determines +//! the type of the event, followed by optional details +//! about this event. +//! +//! These extra details are represented by the [`EventDetails`](enum@packets::event::EventDetails) +//! enum (even if a certain event doesn't come with additional data). +//! You can import the enum and use a matcher to determine an event's type +//! and extract its payload (if available) like so: +//! +//! ```no_run +//! use f1_game_packet_parser::packets::event::EventDetails; +//! use f1_game_packet_parser::parse; +//! +//! let placeholder_data = include_bytes!("placeholder.bin"); +//! let packet = parse(placeholder_data)?; +//! +//! if let Some(event) = &packet.event { +//! match event.details { +//! /// Event with no extra details. +//! EventDetails::LightsOut => { +//! println!("It's lights out, and away we go!"); +//! } +//! /// You can skip the details if you don't need them. +//! EventDetails::Flashback { .. } => { +//! println!("Flashback has been triggered!"); +//! } +//! /// Extracting details from an event. +//! EventDetails::RaceWinner { vehicle_index } => { +//! println!( +//! "Driver at index {} is the winner!", +//! vehicle_index +//! ); +//! } +//! _ => (), +//! } +//! } +//! ``` +//! +//! ### Working with bitmaps +//! +//! There are 3 fields that use a [`bitflags`]-powered bitmap struct: +//! +//! - [`EventDetails::Buttons::button_status`](field@packets::event::EventDetails::Buttons::button_status) +//! - [`CarTelemetryData::rev_lights_bit_value`](field@packets::car_telemetry::CarTelemetryData::rev_lights_bit_value) +//! - [`LapHistoryData::lap_valid_bit_flags`](field@packets::session_history::LapHistoryData::lap_valid_bit_flags) +//! +//! Each bitmap struct is publicly available via the [`constants`] module +//! and comes with a handful of constants representing specific bit values, +//! as well as methods and operator overloads for common bit operations. +//! +//! Here's an example that checks if a given binary file is +//! a [car telemetry packet](F1PacketCarTelemetry). +//! If so, it will grab player car's telemetry data +//! and determine whether the revs are high, medium or low +//! based on the specific bit values being set. +//! +//! ```no_run +//! use f1_game_packet_parser::constants::RevLights; +//! use f1_game_packet_parser::parse; +//! +//! let placeholder_data = include_bytes!("placeholder.bin"); +//! let packet = parse(placeholder_data)?; +//! let player_car_index = packet.header.player_car_index; +//! +//! if let Some(car_telemetry) = &packet.car_telemetry { +//! let player = car_telemetry.data[player_car_index]; +//! let is_high_rev = +//! player.rev_lights_bit_value.contains(RevLights::RIGHT_1); +//! let is_medium_rev = +//! player.rev_lights_bit_value.contains(RevLights::MIDDLE_1); +//! +//! let revs_desc = if is_high_rev { +//! "High" +//! } else if is_medium_rev { +//! "Medium" +//! } else { +//! "Low" +//! }; +//! +//! println!("{} revs", revs_desc); +//! } +//! ``` +//! +//! ## Original documentation links +//! +//! - [F1 24](https://forums.ea.com/discussions/f1-24-general-discussion-en/f1-24-udp-specification/8369125) +//! - [F1 23](https://forums.ea.com/discussions/f1-23-en/f1-23-udp-specification/8390745) +//! - [F1 22](https://forums.ea.com/discussions/f1-games-franchise-discussion-en/f1-22-udp-specification/8418392) + +/// Contains appendix constants and enums for various packet-specific struct field values. pub mod constants; +/// Contains structures for each kind of packet payload +/// and submodules for packet-specific structs. pub mod packets; -use crate::constants::PacketId; +use crate::constants::{PacketId, MAX_NUM_CARS}; use crate::packets::{ u8_to_usize, F1PacketCarDamage, F1PacketCarSetups, F1PacketCarStatus, - F1PacketCarTelemetry, F1PacketEvent, F1PacketFinalClassification, F1PacketLap, - F1PacketLobbyInfo, F1PacketMotion, F1PacketMotionEx, F1PacketParticipants, + F1PacketCarTelemetry, F1PacketEvent, F1PacketFinalClassification, F1PacketLaps, + F1PacketLobby, F1PacketMotion, F1PacketMotionEx, F1PacketParticipants, F1PacketSession, F1PacketSessionHistory, F1PacketTimeTrial, F1PacketTyreSets, }; @@ -13,29 +197,136 @@ use binrw::io::Cursor; use binrw::{BinRead, BinReaderExt, BinResult}; use serde::{Deserialize, Serialize}; -/// Attempts to extract game packet data from a slice of bytes. -/// See [`binrw::Error`] for possible error variants. +/// Attempts to extract F1 game packet data from a shared slice of bytes. +/// +/// ## Errors +/// +/// - [`binrw::Error::AssertFail`] when a certain field's value +/// is outside the expected range. This generally applies to +/// `_index` fields, percentage values, and fields that +/// have the aforementioned range specified in their documentation +/// - [`binrw::Error::BadMagic`] when [`F1PacketEvent`] has a code +/// that doesn't match any [known event type](packets::event::EventDetails) +/// - [`binrw::Error::Custom`] when the parser encounters an invalid bool value +/// in a read byte (i.e. neither 0, nor 1) +/// - [`binrw::Error::EnumErrors`] when there's no matching value for an enum field +/// +/// ## Examples +/// +/// ### Basic UDP client +/// +/// ```no_run +/// use f1_game_packet_parser::parse; +/// use std::error::Error; +/// use std::net::UdpSocket; +/// +/// fn main() -> Result<(), Box> { +/// // This IP and port should be set in the game's options by default. +/// let socket = UdpSocket::bind("127.0.0.1:20777")?; +/// let mut buf = [0u8; 1460]; +/// +/// loop { +/// // Receive raw packet data from the game. +/// // The buf array should be large enough for all types of packets. +/// let (amt, _) = socket.recv_from(&mut buf)?; +/// +/// // Convert received bytes to an F1Packet struct and print it. +/// let packet = parse(&buf[..amt])?; +/// println!("{:#?}", packet); +/// } +/// } +/// ``` +/// +/// ### Invalid/unsupported packet format +/// +/// ``` +/// let invalid_format = 2137u16.to_le_bytes(); +/// let parse_result = f1_game_packet_parser::parse(&invalid_format); +/// +/// assert!(parse_result.is_err()); +/// assert_eq!( +/// parse_result.unwrap_err().to_string(), +/// "Invalid or unsupported packet format: 2137 at 0x0" +/// ); +/// ``` pub fn parse(data: &[u8]) -> BinResult { let mut cursor = Cursor::new(data); - let header: F1PacketHeader = cursor.read_le()?; - let body: F1PacketBody = - cursor.read_le_args((header.packet_format, header.packet_id))?; + let packet: F1Packet = cursor.read_le()?; - Ok(F1Packet { header, body }) + Ok(packet) } -#[derive(PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] +/// Structured representation of raw F1 game packet data that's +/// returned as a successful result of the [`parse`] function. +/// +/// Each [`Option`] field acts as a slot for a payload of a packet of a certain type. +/// Only one of these fields can be [`Some`] for a given `F1Packet` instance. +#[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] +#[br(little)] pub struct F1Packet { + /// Universal packet header. pub header: F1PacketHeader, - pub body: F1PacketBody, + /// Physics data for all cars in the ongoing session. + #[br(if(header.packet_id == PacketId::Motion), args(header.packet_format))] + pub motion: Option, + /// Data about the ongoing session. + #[br(if(header.packet_id == PacketId::Session), args(header.packet_format))] + pub session: Option, + /// Lap data for all cars on track. + #[br(if(header.packet_id == PacketId::Laps), args(header.packet_format))] + pub laps: Option, + /// Details of events that happen during the course of the ongoing session. + #[br(if(header.packet_id == PacketId::Event), args(header.packet_format))] + pub event: Option, + /// List of participants in the session. + #[br(if(header.packet_id == PacketId::Participants), args(header.packet_format))] + pub participants: Option, + /// Setup data for all cars in the ongoing session. + #[br(if(header.packet_id == PacketId::CarSetups), args(header.packet_format))] + pub car_setups: Option, + /// Telemetry data for all cars in the ongoing session. + #[br(if(header.packet_id == PacketId::CarTelemetry), args(header.packet_format))] + pub car_telemetry: Option, + /// Status data for all cars in the ongoing session. + #[br(if(header.packet_id == PacketId::CarStatus), args(header.packet_format))] + pub car_status: Option, + /// Final classification confirmation at the end of the session. + #[br( + if(header.packet_id == PacketId::FinalClassification), + args(header.packet_format) + )] + pub final_classification: Option, + /// Details of players in a multiplayer lobby. + #[br(if(header.packet_id == PacketId::LobbyInfo), args(header.packet_format))] + pub lobby: Option, + /// Car damage parameters for all cars in the ongoing session. + #[br(if(header.packet_id == PacketId::CarDamage), args(header.packet_format))] + pub car_damage: Option, + /// Session history data for a specific car. + #[br(if(header.packet_id == PacketId::SessionHistory), args(header.packet_format))] + pub session_history: Option, + /// In-depth details about tyre sets assigned to a vehicle during the session. + /// Available from the 2023 format onwards. + #[br(if(header.packet_id == PacketId::TyreSets), args(header.packet_format))] + pub tyre_sets: Option, + /// Extended player car only motion data. + /// Available from the 2023 format onwards. + #[br(if(header.packet_id == PacketId::MotionEx), args(header.packet_format))] + pub motion_ex: Option, + /// Extra information that's only relevant to time trial game mode. + /// Available from the 2024 format onwards. + #[br(if(header.packet_id == PacketId::TimeTrial), args(header.packet_format))] + pub time_trial: Option, } -/// F1 game packet's header. +/// F1 game packet's header. It contains metadata about the game, +/// the ongoing session, the frame this packet was sent on, and player car indexes. #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little)] pub struct F1PacketHeader { /// Value of the "UDP Format" option in the game's telemetry settings. + /// This crate currently supports formats in range `(2022..=2024)`. #[br( assert( (2022..=2024).contains(&packet_format), @@ -44,7 +335,7 @@ pub struct F1PacketHeader { ) )] pub packet_format: u16, - /// Game year (last two digits) + /// Game year (last two digits). /// Available from the 2023 format onwards. #[br(if(packet_format >= 2023))] pub game_year: u8, @@ -54,79 +345,32 @@ pub struct F1PacketHeader { pub game_minor_version: u8, /// Version of this packet type, all start from 1. pub packet_version: u8, - /// Identifier for the packet type. + /// Unique identifier for the packet type. pub packet_id: PacketId, /// Unique identifier for the session. pub session_uid: u64, /// Session timestamp. pub session_time: f32, /// Identifier for the frame the data was retrieved on. + /// Goes back after a flashback is triggered. pub frame_identifier: u32, /// Overall identifier for the frame the data was retrieved on - /// (doesn't go back after flashbacks). + /// (i.e. it doesn't go back after flashbacks). /// Available from the 2023 format onwards. #[br(if(packet_format >= 2023))] pub overall_frame_identifier: u32, - /// Index of player's car in the array. - #[br(map(u8_to_usize))] + /// Index of player 1's car. + #[br( + map(u8_to_usize), + assert( + player_car_index < MAX_NUM_CARS, + "Header has an invalid player 1 car index: {}", + player_car_index + ) + )] pub player_car_index: usize, - /// Index of secondary player's car in the array in splitscreen mode. + /// Index of player 2's car in splitscreen mode. /// Set to 255 if not in splitscreen mode. #[br(map(u8_to_usize))] pub secondary_player_car_index: usize, } - -/// F1 game packet's body. -#[non_exhaustive] -#[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] -#[br(little, import(packet_format: u16, packet_id: PacketId))] -pub struct F1PacketBody { - /// Physics data for all cars in the session. - #[br(if(packet_id == PacketId::Motion), args(packet_format))] - pub motion: Option, - /// Data about the ongoing session. - #[br(if(packet_id == PacketId::Session), args(packet_format))] - pub session: Option, - /// Lap data for all cars on track. - #[br(if(packet_id == PacketId::LapData), args(packet_format))] - pub lap: Option, - /// Details of events that happen during the course of a session. - #[br(if(packet_id == PacketId::Event), args(packet_format))] - pub event: Option, - /// List of participants in the race. - #[br(if(packet_id == PacketId::Participants), args(packet_format))] - pub participants: Option, - /// Car setups for all cars in the session. - #[br(if(packet_id == PacketId::CarSetups), args(packet_format))] - pub car_setups: Option, - /// Telemetry data for all cars in the session. - #[br(if(packet_id == PacketId::CarTelemetry), args(packet_format))] - pub car_telemetry: Option, - /// Status data for all cars in the session. - #[br(if(packet_id == PacketId::CarStatus), args(packet_format))] - pub car_status: Option, - /// Final classification confirmation at the end of a race. - #[br(if(packet_id == PacketId::FinalClassification), args(packet_format))] - pub final_classification: Option, - /// Details of players in a multiplayer lobby. - #[br(if(packet_id == PacketId::LobbyInfo), args(packet_format))] - pub lobby_info: Option, - /// Car damage parameters for all cars in the session. - #[br(if(packet_id == PacketId::CarDamage), args(packet_format))] - pub car_damage: Option, - /// Session history data for a specific car. - #[br(if(packet_id == PacketId::SessionHistory), args(packet_format))] - pub session_history: Option, - /// In-depth details about tyre sets assigned to a vehicle during the session. - /// Available from the 2023 format onwards. - #[br(if(packet_id == PacketId::TyreSets), args(packet_format))] - pub tyre_sets: Option, - /// Extended player car only motion data. - /// Available from the 2023 format onwards. - #[br(if(packet_id == PacketId::MotionEx), args(packet_format))] - pub motion_ex: Option, - /// Extra information that's only relevant to time trial game mode. - /// Available from the 2024 format onwards. - #[br(if(packet_id == PacketId::TimeTrial), args(packet_format))] - pub time_trial: Option, -} diff --git a/src/packets/car_setups.rs b/src/packets/car_setups.rs index 92e3b5a..29e44fd 100644 --- a/src/packets/car_setups.rs +++ b/src/packets/car_setups.rs @@ -10,8 +10,22 @@ pub struct CarSetupData { /// Rear wing aero. pub rear_wing: u8, /// Differential adjustment on throttle (percentage). + #[br( + assert( + on_throttle <= 100, + "Car setup entry has an invalid on-throttle percentage value: {}", + on_throttle + ) + )] pub on_throttle: u8, /// Differential adjustment off throttle (percentage). + #[br( + assert( + off_throttle <= 100, + "Car setup entry has an invalid off-throttle percentage value: {}", + off_throttle + ) + )] pub off_throttle: u8, /// Front camber angle (suspension geometry). pub front_camber: f32, @@ -34,6 +48,13 @@ pub struct CarSetupData { /// Rear ride height. pub rear_suspension_height: u8, /// Brake pressure (percentage). + #[br( + assert( + brake_pressure <= 100, + "Car setup entry has an invalid brake pressure percentage value: {}", + brake_pressure + ) + )] pub brake_pressure: u8, /// Brake bias. pub brake_bias: u8, diff --git a/src/packets/car_telemetry.rs b/src/packets/car_telemetry.rs index 103ab33..8c23a07 100644 --- a/src/packets/car_telemetry.rs +++ b/src/packets/car_telemetry.rs @@ -93,5 +93,5 @@ pub struct CarTelemetryData { /// Driving surface of each tyre. /// See [`wheel_index`](mod@crate::constants::wheel_index) /// for wheel order. - pub surface_type: Option<[Surface; 4]>, + pub surface_type: [Surface; 4], } diff --git a/src/packets/event.rs b/src/packets/event.rs index fd6fd7d..ea5866e 100644 --- a/src/packets/event.rs +++ b/src/packets/event.rs @@ -1,6 +1,7 @@ use super::{u8_to_bool, u8_to_usize}; use crate::constants::{ ButtonStatus, InfringementType, PenaltyType, SafetyCarEventType, SafetyCarType, + MAX_NUM_CARS, }; use binrw::BinRead; @@ -20,7 +21,14 @@ pub enum EventDetails { #[br(magic = b"FTLP")] FastestLap { /// Index of the car that's achieved the fastest lap. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Fastest lap event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, /// Lap time in seconds. lap_time: f32, @@ -29,7 +37,14 @@ pub enum EventDetails { #[br(magic = b"RTMT")] Retirement { /// Index of the retiring car. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Retirement event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, }, /// Sent when race control enable DRS. @@ -42,17 +57,31 @@ pub enum EventDetails { #[br(magic = b"TMPT")] TeamMateInPits { /// Index of teammate's car. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Teammate in pits event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, }, /// Sent when the chequered flag has been waved. #[br(magic = b"CHQF")] ChequeredFlag, - /// Sent when the race winner is announced. + /// Sent when the race winner has been announced. #[br(magic = b"RCWN")] RaceWinner { /// Index of race winner's car. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Race winner event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, }, /// Sent when a penalty has been issued. @@ -63,10 +92,24 @@ pub enum EventDetails { /// Infringement type. infringement_type: InfringementType, /// Index of the car the penalty is applied to. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Penalty event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, /// Index of the other car involved. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + other_vehicle_index < MAX_NUM_CARS, + "Penalty event has an invalid other vehicle index: {}", + other_vehicle_index + ) + )] other_vehicle_index: usize, /// Time gained/spent doing the action in seconds. time: u8, @@ -79,7 +122,14 @@ pub enum EventDetails { #[br(magic = b"SPTP")] SpeedTrap { /// Index of the car that's triggered the speed trap. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Speed trap event has an invalid vehicle index: {}", + vehicle_index + ) + )] vehicle_index: usize, /// Top speed achieved in kilometres per hour. speed: f32, @@ -90,7 +140,14 @@ pub enum EventDetails { #[br(try_map(u8_to_bool))] is_driver_fastest_in_session: bool, /// Index of the vehicle that's the fastest in the session. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Speed trap event has an invalid fastest vehicle index: {}", + vehicle_index + ) + )] fastest_vehicle_index: usize, /// Fastest speed in the session in kilometres per hour. fastest_speed_in_session: f32, @@ -147,7 +204,12 @@ pub enum EventDetails { /// Sent when safety car gets deployed. /// Available from the 2024 format onwards. #[br(magic = b"SCAR")] - SafetyCar { safety_car_type: SafetyCarType, event_type: SafetyCarEventType }, + SafetyCar { + /// Type of the safety car that's been deployed. + safety_car_type: SafetyCarType, + /// New safety car deployment status. + event_type: SafetyCarEventType, + }, /// Sent when two vehicles collide. /// Available from the 2024 format onwards. #[br(magic = b"COLL")] diff --git a/src/packets/lap.rs b/src/packets/laps.rs similarity index 92% rename from src/packets/lap.rs rename to src/packets/laps.rs index eeae897..287e039 100644 --- a/src/packets/lap.rs +++ b/src/packets/laps.rs @@ -1,5 +1,5 @@ use super::u8_to_bool; -use crate::constants::{DriverStatus, PitStatus, ResultStatus}; +use crate::constants::{DriverStatus, PitStatus, ResultStatus, Sector}; use binrw::BinRead; use serde::{Deserialize, Serialize}; @@ -7,15 +7,7 @@ use serde::{Deserialize, Serialize}; /// Lap data for a car on track. #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Copy, Clone, Debug, Serialize, Deserialize)] -#[br( - little, - import(packet_format: u16), - assert( - sector <= 2, - "Lap data entry has an invalid sector number: {}", - sector - ) -)] +#[br(little, import(packet_format: u16))] pub struct LapData { /// Last lap time in milliseconds. pub last_lap_time_ms: u32, @@ -66,8 +58,7 @@ pub struct LapData { /// Number of pit stops taken in this race. pub num_pit_stops: u8, /// Zero-based number of the sector the driver is currently going through. - /// S1 = 0, S2 = 1, S3 = 2. - pub sector: u8, + pub sector: Sector, /// Whether the current lap is invalid. #[br(try_map(u8_to_bool))] pub current_lap_invalid: bool, @@ -104,7 +95,7 @@ pub struct LapData { #[br(if(packet_format >= 2024))] pub speed_trap_fastest_speed: f32, /// Number of the lap the fastest speed was achieved on - /// (255 means "not set") + /// (255 means "not set"). /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] pub speed_trap_fastest_lap: u8, diff --git a/src/packets/lobby.rs b/src/packets/lobby.rs index 07e42a1..2a0b2a7 100644 --- a/src/packets/lobby.rs +++ b/src/packets/lobby.rs @@ -14,8 +14,7 @@ pub struct LobbyInfoData { #[br(try_map(u8_to_bool))] pub ai_controlled: bool, /// Team's ID. - /// See [`team_id`](mod@crate::constants::team_id) - /// for possible values. + /// See [`team_id`](mod@crate::constants::team_id) for possible values. pub team_id: u8, /// Driver's nationality. pub nationality: Nationality, diff --git a/src/packets/mod.rs b/src/packets/mod.rs index 6a8098e..e57241b 100644 --- a/src/packets/mod.rs +++ b/src/packets/mod.rs @@ -4,7 +4,7 @@ pub mod car_status; pub mod car_telemetry; pub mod event; pub mod final_classification; -pub mod lap; +pub mod laps; pub mod lobby; pub mod motion; pub mod participants; @@ -17,17 +17,17 @@ use crate::constants::{ BrakingAssist, CarDamage, CarDamageRate, Collisions, CornerCuttingStringency, DynamicRacingLine, DynamicRacingLineType, FlashbackLimit, ForecastAccuracy, FormationLapExperience, Formula, GameMode, GearboxAssist, LowFuelMode, MfdPanelIndex, - PitStopExperience, RaceStarts, RecoveryMode, RedFlags, RuleSet, SafetyCar, - SafetyCarExperience, SafetyCarStatus, SessionLength, SpeedUnit, - SurfaceType, TemperatureUnit, TrackId, TyreTemperature, Weather, MAX_NUM_CARS, + PitStopExperience, RaceStarts, RecoveryMode, RedFlagIntensity, RuleSet, + SafetyCarExperience, SafetyCarIntensity, SafetyCarStatus, SessionLength, SpeedUnit, + SurfaceSimType, TemperatureUnit, TrackId, TyreTemperature, Weather, MAX_NUM_CARS, }; use crate::packets::car_damage::CarDamageData; use crate::packets::car_setups::CarSetupData; use crate::packets::car_status::CarStatusData; use crate::packets::car_telemetry::CarTelemetryData; -use crate::packets::event::EventDataDetails; +use crate::packets::event::EventDetails; use crate::packets::final_classification::FinalClassificationData; -use crate::packets::lap::LapData; +use crate::packets::laps::LapData; use crate::packets::lobby::LobbyInfoData; use crate::packets::motion::CarMotionData; use crate::packets::participants::ParticipantsData; @@ -45,24 +45,26 @@ use crate::packets::tyre_sets::{TyreSetData, NUM_TYRE_SETS}; use binrw::BinRead; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt; use std::string::FromUtf8Error; -/// Physics data for all the cars being driven. +/// The motion packet gives physics data for all the cars being driven. #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] pub struct F1PacketMotion { - /// Motion data for all cars on track. + /// Motion data for all cars on track. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub car_motion_data: Vec, - /// Extra player car only motion data. + pub data: Vec, + /// Extra player-car-only motion data. /// Available only in the 2022 format. #[br(if(packet_format == 2022))] - pub motion_ex_data: Option, + pub motion_ex: Option, } -/// Data about the ongoing session. +/// The session packet includes details about the current session in progress. +/// ## Example #[allow(clippy::struct_excessive_bools)] #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] @@ -248,7 +250,7 @@ pub struct F1PacketSession { /// Surface simulation type. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] - pub surface_type: Option, + pub surface_sim_type: Option, /// Low fuel driving difficulty. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] @@ -305,7 +307,7 @@ pub struct F1PacketSession { /// Safety car intensity. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] - pub safety_car: Option, + pub safety_car_intensity: Option, /// Safety car experience. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] @@ -321,7 +323,7 @@ pub struct F1PacketSession { /// Red flag intensity. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024))] - pub red_flags: Option, + pub red_flag_intensity: Option, /// Whether this single player game affects the license level. /// Available from the 2024 format onwards. #[br(if(packet_format >= 2024), try_map(u8_to_bool))] @@ -343,6 +345,8 @@ pub struct F1PacketSession { /// List of sessions that shows this weekend's structure. /// Should have a size equal to /// [`num_sessions_in_weekend`](field@crate::packets::F1PacketSession::num_sessions_in_weekend). + /// See [`session_type`](mod@crate::constants::session_type) + /// for possible values. /// Available from the 2024 format onwards. #[br( if(packet_format >= 2024), @@ -364,10 +368,10 @@ pub struct F1PacketSession { #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] -pub struct F1PacketLap { - /// Lap data for all cars on track. +pub struct F1PacketLaps { + /// Lap data for all cars on track. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub lap_data: Vec, + pub data: Vec, /// Index of personal best car in time trial mode (255 if invalid). #[br(map(u8_to_usize))] pub time_trial_pb_car_index: usize, @@ -386,10 +390,10 @@ pub struct F1PacketEvent { try_map(|bytes: [u8; 4]| String::from_utf8(bytes.to_vec())), restore_position )] - pub event_string_code: String, + pub code: String, /// Extra data for this event. #[br(args(packet_format))] - pub event_details: EventDataDetails, + pub details: EventDetails, } /// Data of participants in the session, mostly relevant for multiplayer. @@ -412,7 +416,7 @@ pub struct F1PacketParticipants { /// Should have a size equal to /// [`num_active_cars`](field@crate::packets::F1PacketParticipants::num_active_cars). #[br(count(num_active_cars), args{ inner: (packet_format,) })] - pub participants_data: Vec, + pub data: Vec, } /// Car setups for all cars in the race. @@ -422,9 +426,9 @@ pub struct F1PacketParticipants { #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] pub struct F1PacketCarSetups { - /// Setup data for all cars on track. + /// Setup data for all cars on track. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub car_setups_data: Vec, + pub data: Vec, /// Value of front wing after next pit stop - player only. /// Available from the 2024 format onwards #[br(if(packet_format >= 2024))] @@ -436,12 +440,12 @@ pub struct F1PacketCarSetups { #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] pub struct F1PacketCarTelemetry { - /// Telemetry data for all cars on track. + /// Telemetry data for all cars on track. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub car_telemetry_data: Vec, - /// Index of currently open MFD panel. + pub data: Vec, + /// Index of currently open MFD panel for player 1. pub mfd_panel_index: MfdPanelIndex, - /// See `mfd_panel_index`. + /// Index of currently open MFD panel for player 2. pub mfd_panel_index_secondary_player: MfdPanelIndex, /// Suggested gear (0 if no gear suggested). #[br( @@ -459,9 +463,9 @@ pub struct F1PacketCarTelemetry { #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] pub struct F1PacketCarStatus { - /// Status data for all cars. + /// Car status data for all cars. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub car_status_data: Vec, + pub data: Vec, } /// Final classification confirmation at the end of a race. @@ -483,14 +487,14 @@ pub struct F1PacketFinalClassification { /// Should have a size equal to /// [`num_cars`](field@crate::packets::F1PacketFinalClassification::num_cars). #[br(count(num_cars), args{ inner: (packet_format,) })] - pub final_classification_data: Vec, + pub data: Vec, } /// Packet detailing all the players that are currently in a multiplayer lobby. #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] -pub struct F1PacketLobbyInfo { +pub struct F1PacketLobby { /// Number of players in the lobby. #[br( map(u8_to_usize), @@ -503,9 +507,9 @@ pub struct F1PacketLobbyInfo { pub num_players: usize, /// Lobby info data for all players. /// Should have a size equal to - /// [`num_players`](field@crate::packets::F1PacketLobbyInfo::num_players). + /// [`num_players`](field@crate::packets::F1PacketLobby::num_players). #[br(count(num_players), args{ inner: (packet_format,) })] - pub lobby_info_data: Vec, + pub data: Vec, } /// Car damage parameters for all cars in the session. @@ -513,12 +517,12 @@ pub struct F1PacketLobbyInfo { #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] pub struct F1PacketCarDamage { - /// Car damage data. + /// Car damage data. Should have a size of 22. #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] - pub car_damage_data: Vec, + pub data: Vec, } -/// Packet detailing lap and tyre data history for a given driver in the session +/// Packet detailing lap and tyre data history for a given driver in the session. #[non_exhaustive] #[derive(BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize)] #[br(little, import(packet_format: u16))] @@ -591,7 +595,7 @@ pub struct F1PacketTyreSets { pub vehicle_index: usize, /// 13 dry + 7 wet tyre sets. #[br(count(NUM_TYRE_SETS), args{ inner: (packet_format,) })] - pub tyre_set_data: Vec, + pub data: Vec, /// Index of fitted tyre set. #[br( map(u8_to_usize), @@ -718,19 +722,21 @@ pub struct F1PacketTimeTrial { } #[derive(Debug, PartialEq)] -pub(crate) struct InvalidBoolValue(u8); +pub(crate) struct MapBoolError(u8); -impl fmt::Display for InvalidBoolValue { +impl fmt::Display for MapBoolError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Invalid bool value: {}", self.0) } } -pub(crate) fn u8_to_bool(value: u8) -> Result { +impl Error for MapBoolError {} + +pub(crate) fn u8_to_bool(value: u8) -> Result { match value { 0 => Ok(false), 1 => Ok(true), - _ => Err(InvalidBoolValue(value)), + _ => Err(MapBoolError(value)), } } diff --git a/src/packets/motion.rs b/src/packets/motion.rs index 49616fd..1bb9361 100644 --- a/src/packets/motion.rs +++ b/src/packets/motion.rs @@ -43,32 +43,38 @@ pub struct CarMotionData { } impl CarMotionData { - /// Returns [`self.world_forward_dir_x`] divided by `32767.0f32`. + /// Returns [`world_forward_dir_x`](field@CarMotionData::world_forward_dir_x) + /// divided by `32767.0f32`. pub fn world_forward_dir_x_as_f32(&self) -> f32 { self.world_forward_dir_x as f32 / 32767.0 } - /// Returns [`self.world_forward_dir_y`] divided by `32767.0f32`. + /// Returns [`world_forward_dir_y`](field@CarMotionData::world_forward_dir_y) + /// divided by `32767.0f32`. pub fn world_forward_dir_y_as_f32(&self) -> f32 { self.world_forward_dir_y as f32 / 32767.0 } - /// Returns [`self.world_forward_dir_z`] divided by `32767.0f32`. + /// Returns [`world_forward_dir_z`](field@CarMotionData::world_forward_dir_z) + /// divided by `32767.0f32`. pub fn world_forward_dir_z_as_f32(&self) -> f32 { self.world_forward_dir_z as f32 / 32767.0 } - /// Returns [`self.world_right_dir_x`] divided by `32767.0f32`. + /// Returns [`world_right_dir_x`](field@CarMotionData::world_right_dir_x) + /// divided by `32767.0f32`. pub fn world_right_dir_x_as_f32(&self) -> f32 { self.world_right_dir_x as f32 / 32767.0 } - /// Returns [`self.world_right_dir_y`] divided by `32767.0f32`. + /// Returns [`world_right_dir_y`](field@CarMotionData::world_right_dir_y) + /// divided by `32767.0f32`. pub fn world_right_dir_y_as_f32(&self) -> f32 { self.world_right_dir_y as f32 / 32767.0 } - /// Returns [`self.world_right_dir_z`] divided by `32767.0f32`. + /// Returns [`world_right_dir_z`](field@CarMotionData::world_right_dir_z) + /// divided by `32767.0f32`. pub fn world_right_dir_z_as_f32(&self) -> f32 { self.world_right_dir_z as f32 / 32767.0 } diff --git a/src/packets/participants.rs b/src/packets/participants.rs index 772102f..db6a47f 100644 --- a/src/packets/participants.rs +++ b/src/packets/participants.rs @@ -20,8 +20,7 @@ pub struct ParticipantsData { /// Unique ID for network players. pub network_id: u8, /// Team's ID. - /// See [`team_id`](mod@crate::constants::team_id) - /// for possible values. + /// See [`team_id`](mod@crate::constants::team_id) for possible values. pub team_id: u8, /// Whether my team is being used. #[br(try_map(u8_to_bool))] diff --git a/src/packets/session.rs b/src/packets/session.rs index a2c2f72..d56e77b 100644 --- a/src/packets/session.rs +++ b/src/packets/session.rs @@ -77,7 +77,7 @@ pub(super) fn get_forecast_samples_padding( #[inline(always)] fn get_max_num_samples(packet_format: u16) -> usize { - if packet_format == 2024 { + if packet_format >= 2024 { 64 } else { 56 diff --git a/src/packets/time_trial.rs b/src/packets/time_trial.rs index 8dedfa6..130d857 100644 --- a/src/packets/time_trial.rs +++ b/src/packets/time_trial.rs @@ -1,5 +1,5 @@ use super::{u8_to_bool, u8_to_usize}; -use crate::constants::{GearboxAssist, TractionControl}; +use crate::constants::{GearboxAssist, TractionControl, MAX_NUM_CARS}; use binrw::BinRead; use serde::{Deserialize, Serialize}; @@ -11,9 +11,17 @@ use serde::{Deserialize, Serialize}; #[br(little, import(_packet_format: u16))] pub struct TimeTrialDataSet { /// Index of the car this data set relates to. - #[br(map(u8_to_usize))] + #[br( + map(u8_to_usize), + assert( + vehicle_index < MAX_NUM_CARS, + "Time trial data set has an invalid vehicle index: {}", + vehicle_index + ) + )] pub vehicle_index: usize, /// Team's ID. + /// See [`team_id`](mod@crate::constants::team_id) for possible values. pub team_id: u8, /// Lap time in milliseconds. pub lap_time_ms: u32,