diff --git a/src/constants/mod.rs b/src/constants/mod.rs index 50a39f1..fcafac2 100644 --- a/src/constants/mod.rs +++ b/src/constants/mod.rs @@ -3,6 +3,11 @@ pub mod wheel_index; use binrw::BinRead; use serde::{Deserialize, Serialize}; +pub(crate) const MAX_NUM_CARS: usize = 22; +pub(crate) const MAX_NUM_MARSHAL_ZONES: usize = 21; +pub(crate) const MAX_NUM_WEATHER_FORECAST_SAMPLES: usize = 56; +pub(crate) const MAX_AI_DIFFICULTY: u8 = 110; + #[non_exhaustive] #[derive( BinRead, @@ -408,3 +413,67 @@ pub enum SessionLength { Long = 6, Full = 7, } + +#[derive( + BinRead, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Debug, + Serialize, + Deserialize, +)] +#[br(little, repr(u8))] +pub enum PitStatus { + None = 0, + Pitting = 1, + InPitArea = 2, +} + +#[derive( + BinRead, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Debug, + Serialize, + Deserialize, +)] +#[br(little, repr(u8))] +pub enum DriverStatus { + InGarage = 0, + FlyingLap = 1, + InLap = 2, + OutLap = 3, + OnTrack = 4, +} + +#[derive( + BinRead, + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Debug, + Serialize, + Deserialize, +)] +#[br(little, repr(u8))] +pub enum ResultStatus { + Unknown = 0, + Inactive = 1, + Active = 2, + Finished = 3, + DidNotFinish = 4, + Disqualified = 5, + NotClassified = 6, + Retired = 7, +} diff --git a/src/lib.rs b/src/lib.rs index e1c2124..9172084 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,8 @@ pub struct F1Packet { little, assert( (2022..=2024).contains(&packet_format), - "Invalid or unsupported packet format" + "Invalid or unsupported packet format: {}", + packet_format ) )] pub struct F1PacketHeader { @@ -57,6 +58,7 @@ pub struct F1PacketHeader { /// Index of player's car in the array. pub player_car_index: u8, /// Index of secondary player's car in the array in splitscreen mode. + /// Set to 255 if not in splitscreen mode. pub secondary_player_car_index: u8, } @@ -67,8 +69,10 @@ pub struct F1PacketHeader { )] #[br(little, import(packet_format: u16, packet_id: PacketId))] pub struct F1PacketBody { + /// Physics data for all the cars being driven. #[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, } diff --git a/src/packets/lap_data.rs b/src/packets/lap_data.rs new file mode 100644 index 0000000..111bc19 --- /dev/null +++ b/src/packets/lap_data.rs @@ -0,0 +1,75 @@ +use super::u8_to_bool; +use crate::constants::{DriverStatus, PitStatus, ResultStatus}; + +use binrw::BinRead; +use serde::{Deserialize, Serialize}; + +/// Lap data for a car on track. +#[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 + ) +)] +pub struct LapData { + /// Last lap time in milliseconds. + pub last_lap_time_ms: u32, + /// Current lap time in milliseconds. + pub current_lap_time_ms: u32, + /// Current sector 1 time in milliseconds. + pub sector1_time_ms: u16, + /// Current sector 2 time in milliseconds. + pub sector2_time_ms: u16, + /// The distance the vehicle is around current lap in metres. + /// It may be negative if the start/finish line hasn’t been crossed yet. + pub lap_distance: f32, + /// The total distance the vehicle has gone around in this session in metres. + /// It may be negative if the start/finish line hasn’t been crossed yet. + pub total_distance: f32, + /// Delta for the safety car in seconds. + pub safety_car_delta: f32, + /// Car's race position. + pub car_position: u8, + /// Current lap number. + pub current_lap_num: u8, + /// Car's pit status. + pub pit_status: PitStatus, + /// 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, + /// Whether the current lap is invalid. + #[br(map(u8_to_bool))] + pub current_lap_invalid: bool, + /// Accumulated time penalties to be added in seconds. + pub penalties: u8, + /// Accumulated number of warnings issued. + pub warnings: u8, + /// Number of unserved drive through penalties left to serve. + pub num_unserved_drive_through_pens: u8, + /// Number of unserved stop-go penalties left to serve. + pub num_unserved_stop_go_pens: u8, + /// The grid position the vehicle started the race in. + pub grid_position: u8, + /// Status of the driver. + pub driver_status: DriverStatus, + /// Status of the driver's result. + pub result_status: ResultStatus, + /// Whether the pit lane timer is active. + #[br(map(u8_to_bool))] + pub pit_lane_timer_active: bool, + /// Current time spent in the pit lane in milliseconds. + pub pit_lane_time_in_lane_ms: u16, + /// Time of the actual pit stop in milliseconds. + pub pit_stop_timer_ms: u16, + /// Whether the car should serve a penalty at this stop. + #[br(map(u8_to_bool))] + pub pit_stop_should_serve_pen: bool, +} diff --git a/src/packets/mod.rs b/src/packets/mod.rs index 1ab3ee3..3e34da4 100644 --- a/src/packets/mod.rs +++ b/src/packets/mod.rs @@ -1,15 +1,19 @@ pub mod motion; pub mod session; +pub mod lap_data; use crate::packets::motion::CarMotionData; use crate::constants::{ - BrakingAssist, DynamicRacingLine, DynamicRacingLineType, ForecastAccuracy, - Formula, GameMode, GearboxAssist, Ruleset, SafetyCarStatus, SessionLength, - SessionType, TrackId, Weather, + BrakingAssist, DynamicRacingLine, + DynamicRacingLineType, ForecastAccuracy, Formula, GameMode, + GearboxAssist, Ruleset, SafetyCarStatus, SessionLength, SessionType, + TrackId, Weather, MAX_AI_DIFFICULTY, MAX_NUM_CARS, MAX_NUM_MARSHAL_ZONES, + MAX_NUM_WEATHER_FORECAST_SAMPLES, }; use crate::packets::session::{MarshalZone, WeatherForecastSample}; +use crate::packets::lap_data::LapData; use binrw::BinRead; use serde::{Deserialize, Serialize}; use std::string::FromUtf8Error; @@ -22,13 +26,14 @@ use std::string::FromUtf8Error; #[br(little, import(packet_format: u16))] pub struct F1PacketMotionData { /// Motion data for all cars on track. - #[br(count(22))] + #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] pub car_motion_data: Vec, /// Extra player car only motion data (2022 format only). #[br(if(packet_format == 2022))] pub motion_ex_data: Option, } +/// Data about the ongoing session. #[allow(clippy::struct_excessive_bools)] #[non_exhaustive] #[derive( @@ -37,6 +42,21 @@ pub struct F1PacketMotionData { #[br( little, import(packet_format: u16), + assert( + num_marshal_zones <= MAX_NUM_MARSHAL_ZONES, + "Session packet has an invalid number of marshal zones: {}", + num_marshal_zones + ), + assert( + num_weather_forecast_samples <= MAX_NUM_WEATHER_FORECAST_SAMPLES, + "Session packet has an invalid number of weather forecast samples: {}", + num_weather_forecast_samples + ), + assert( + ai_difficulty <= MAX_AI_DIFFICULTY, + "Session packet has an invalid AI difficulty value: {}", + ai_difficulty + ), )] pub struct F1PacketSessionData { /// Current weather. @@ -73,26 +93,31 @@ pub struct F1PacketSessionData { /// Whether SLI Pro support is active. #[br(map(u8_to_bool))] pub sli_pro_native_support: bool, - /// Number of marshal zones to follow. + /// Number of marshal zones to follow (no greater than 21). #[br(map(u8_to_usize))] pub num_marshal_zones: usize, - /// List of up to 21 marshal zones. - #[br(count(21), args{ inner: (packet_format,) })] + /// List of marshal zones. + /// Has a size of 21 regardless of the `num_marshal_zones` value. + #[br(count(MAX_NUM_MARSHAL_ZONES), args{ inner: (packet_format,) })] pub marshal_zones: Vec, /// Safety car deployment status. pub safety_car_status: SafetyCarStatus, /// Whether this game is online. #[br(map(u8_to_bool))] pub network_game: bool, - /// Number of weather samples to follow. + /// Number of weather samples to follow (no greater than 56). #[br(map(u8_to_usize))] pub num_weather_forecast_samples: usize, - /// List of up to 56 weather forecast samples. - #[br(count(56), args{ inner: (packet_format,) })] + /// List of up to weather forecast samples. + /// Has a size of 56 regardless of the `num_weather_forecast_samples` value. + #[br( + count(MAX_NUM_WEATHER_FORECAST_SAMPLES), + args{ inner: (packet_format,) } + )] pub weather_forecast_samples: Vec, /// Weather forecast accuracy. pub forecast_accuracy: ForecastAccuracy, - /// AI difficulty rating (0-110). + /// AI difficulty rating in range `(0..=110)`. pub ai_difficulty: u8, /// Identifier for season - persists across saves. pub season_link_identifier: u32, @@ -137,6 +162,24 @@ pub struct F1PacketSessionData { pub session_length: SessionLength, } +/// Data about all the lap times of cars in the session. +#[non_exhaustive] +#[derive( + BinRead, PartialEq, PartialOrd, Clone, Debug, Serialize, Deserialize, +)] +#[br(little, import(packet_format: u16))] +pub struct F1PacketLapData { + /// Lap data for all cars on track. + #[br(count(MAX_NUM_CARS), args{ inner: (packet_format,) })] + pub lap_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, + /// Index of rival's car in time trial mode (255 if invalid). + #[br(map(u8_to_usize))] + pub time_trial_rival_car_index: usize, +} + /// Extended motion data for player's car. Available as a: /// - part of [`F1PacketMotionData`] in the 2022 format /// - standalone packet from the 2023 format onwards