Skip to content

Commit fa464ba

Browse files
committed
feat: add iRacing support (untested)
1 parent c5fb0e7 commit fa464ba

9 files changed

Lines changed: 463 additions & 10 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ dirs = "5.0"
2828
# Shared memory (Windows)
2929
[target.'cfg(windows)'.dependencies]
3030
memmap2 = "0.9"
31-
winapi = { version = "0.3", features = ["winnt", "handleapi", "memoryapi"] }
31+
winapi = { version = "0.3", features = ["winnt", "handleapi", "memoryapi", "errhandlingapi"] }
3232

src/plugins/ams2/ams2_plugin.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ mod game_state {
1919
pub const FRONT_END: u32 = 1;
2020
}
2121

22-
2322
pub struct Ams2Plugin {
2423
#[cfg(windows)]
2524
mem: Option<Ams2SharedMemory>,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//! iRacing plugin — reads telemetry via the iRacing SDK shared memory API.
2+
3+
use anyhow::Result;
4+
5+
use crate::core::TelemetryData;
6+
use crate::plugins::{GameConfig, GamePlugin};
7+
8+
#[cfg(windows)]
9+
use super::shared_memory::IracingSharedMemory;
10+
#[cfg(windows)]
11+
use crate::core::VehicleTelemetry;
12+
#[cfg(windows)]
13+
use std::f32::consts::PI;
14+
#[cfg(windows)]
15+
use tracing::{info, warn};
16+
17+
pub struct IracingPlugin {
18+
#[cfg(windows)]
19+
mem: Option<IracingSharedMemory>,
20+
}
21+
22+
impl IracingPlugin {
23+
pub fn new() -> Self {
24+
Self {
25+
#[cfg(windows)]
26+
mem: None,
27+
}
28+
}
29+
}
30+
31+
impl Default for IracingPlugin {
32+
fn default() -> Self {
33+
Self::new()
34+
}
35+
}
36+
37+
impl GamePlugin for IracingPlugin {
38+
fn name(&self) -> &str {
39+
"iRacing"
40+
}
41+
42+
fn connect(&mut self) -> Result<()> {
43+
#[cfg(windows)]
44+
{
45+
match IracingSharedMemory::open() {
46+
Ok(mem) => {
47+
info!("Connected to iRacing shared memory");
48+
self.mem = Some(mem);
49+
Ok(())
50+
}
51+
Err(e) => {
52+
warn!("iRacing shared memory unavailable: {}", e);
53+
Err(e)
54+
}
55+
}
56+
}
57+
#[cfg(not(windows))]
58+
{
59+
Err(anyhow::anyhow!(
60+
"iRacing plugin requires Windows (shared memory is Windows-only)"
61+
))
62+
}
63+
}
64+
65+
fn disconnect(&mut self) {
66+
#[cfg(windows)]
67+
{
68+
self.mem = None;
69+
info!("Disconnected from iRacing shared memory");
70+
}
71+
}
72+
73+
fn is_connected(&self) -> bool {
74+
#[cfg(windows)]
75+
{
76+
// Verify the session is still active in case iRacing left the session.
77+
self.mem
78+
.as_ref()
79+
.is_some_and(|m| unsafe { m.is_connected() })
80+
}
81+
#[cfg(not(windows))]
82+
{
83+
false
84+
}
85+
}
86+
87+
fn is_available(&self) -> bool {
88+
#[cfg(windows)]
89+
{
90+
IracingSharedMemory::is_available()
91+
}
92+
#[cfg(not(windows))]
93+
{
94+
false
95+
}
96+
}
97+
98+
fn read_telemetry(&mut self) -> Result<Option<TelemetryData>> {
99+
#[cfg(windows)]
100+
{
101+
let mem = match &self.mem {
102+
Some(m) => m,
103+
None => return Ok(None),
104+
};
105+
106+
if !unsafe { mem.is_connected() } {
107+
// Session ended — drop the mapping so connect() will rescan var headers.
108+
self.mem = None;
109+
return Ok(None);
110+
}
111+
112+
let buf = unsafe { mem.current_buf_offset() };
113+
114+
let throttle = unsafe { mem.throttle(buf) }.clamp(0.0, 1.0);
115+
let brake = unsafe { mem.brake(buf) }.clamp(0.0, 1.0);
116+
// iRacing Clutch: 0 = pedal pressed (disengaged), 1 = pedal released (engaged).
117+
// Invert so that our model convention (1.0 = fully pressed) is respected.
118+
let clutch = (1.0 - unsafe { mem.clutch_raw(buf) }).clamp(0.0, 1.0);
119+
// SteeringWheelAngle: radians, positive = CCW (left). Convert to degrees, positive = right.
120+
let steering_angle = unsafe { mem.steering_wheel_angle_rad(buf) } * -(180.0 / PI);
121+
let speed = unsafe { mem.speed(buf) };
122+
let gear = unsafe { mem.gear(buf) };
123+
let rpm = unsafe { mem.rpm(buf) };
124+
let abs_active = unsafe { mem.abs_active(buf) };
125+
let track_position = unsafe { mem.lap_dist_pct(buf) }.clamp(0.0, 1.0);
126+
127+
let vehicle = VehicleTelemetry {
128+
throttle,
129+
brake,
130+
clutch,
131+
steering_angle,
132+
speed,
133+
gear,
134+
rpm,
135+
abs_active,
136+
tc_active: false,
137+
track_position,
138+
};
139+
140+
let timestamp = std::time::SystemTime::now()
141+
.duration_since(std::time::UNIX_EPOCH)
142+
.unwrap_or_default()
143+
.as_millis() as u64;
144+
145+
Ok(Some(TelemetryData {
146+
timestamp,
147+
vehicle,
148+
session: None,
149+
}))
150+
}
151+
#[cfg(not(windows))]
152+
{
153+
Ok(None)
154+
}
155+
}
156+
157+
fn get_config(&self) -> GameConfig {
158+
GameConfig {
159+
// iRacing typically uses ±PI radians full lock, but varies by car.
160+
// 450° is a reasonable UI default (same as ACC/AMS2 convention).
161+
max_steering_angle: 450.0,
162+
pedal_deadzone: 0.01,
163+
abs_threshold: 0.01,
164+
}
165+
}
166+
}

src/plugins/iracing/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! iRacing plugin (iRacing SDK shared memory API)
2+
3+
mod iracing_plugin;
4+
#[cfg(windows)]
5+
mod shared_memory;
6+
7+
pub use iracing_plugin::IracingPlugin;

0 commit comments

Comments
 (0)