From 1688f4978efc512cfeb17ad1b0cc0ebdbf68ef2d Mon Sep 17 00:00:00 2001 From: Robert Gracey <70551819+rgracey@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:34:27 +1100 Subject: [PATCH 1/2] fix: AMS2 offsets --- src/plugins/ams2/shared_memory.rs | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/plugins/ams2/shared_memory.rs b/src/plugins/ams2/shared_memory.rs index 788f39e..4a6216c 100644 --- a/src/plugins/ams2/shared_memory.rs +++ b/src/plugins/ams2/shared_memory.rs @@ -68,42 +68,42 @@ impl Ams2SharedMemory { self.ptr.add(offset).read_volatile() } - // ── pCars2 field accessors ───────────────────────────────────────────── + // ── AMS2 field accessors ─────────────────────────────────────────────── // - // Offsets verified against the pCars2 SDK SharedMemory.h: - // offset 8 = mGameState (u32) - // offset 10524 = mUnfilteredThrottle (f32) - // offset 10528 = mUnfilteredBrake (f32) - // offset 10532 = mUnfilteredSteering (f32) [-1..1 normalised] - // offset 10536 = mUnfilteredClutch (f32) - // offset 10916 = mSpeed (f32, m/s) - // offset 10932 = mGearNumGears (u32, packed nibbles) - // offset 10940 = mAntiLockActive (u8 bool) + // Offsets from the AMS2 shared memory SDK (Pack=8 struct layout): + // offset 8 = mGameState (u32) + // offset 5152 = mUnfilteredThrottle (f32) + // offset 5156 = mUnfilteredBrake (f32) + // offset 5160 = mUnfilteredSteering (f32) [-1..1 normalised] + // offset 5164 = mUnfilteredClutch (f32) + // offset 5828 = mSpeed (f32, m/s) + // offset 5856 = mGear (u32, lower nibble = gear+1: 0→R, 1→N, 2→1st …) + // offset 5868 = mAntiLockActive (u8 bool) pub unsafe fn game_state(&self) -> u32 { self.u32_at(8) } pub unsafe fn unfiltered_throttle(&self) -> f32 { - self.f32_at(10524) + self.f32_at(5152) } pub unsafe fn unfiltered_brake(&self) -> f32 { - self.f32_at(10528) + self.f32_at(5156) } pub unsafe fn unfiltered_steering(&self) -> f32 { - self.f32_at(10532) + self.f32_at(5160) } pub unsafe fn unfiltered_clutch(&self) -> f32 { - self.f32_at(10536) + self.f32_at(5164) } pub unsafe fn speed(&self) -> f32 { - self.f32_at(10916) + self.f32_at(5828) } /// Lower nibble = gear encoded as gear+1 (0→R, 1→N, 2→1st …) pub unsafe fn gear_num_gears(&self) -> u32 { - self.u32_at(10932) + self.u32_at(5856) } pub unsafe fn anti_lock_active(&self) -> bool { - self.u8_at(10940) != 0 + self.u8_at(5868) != 0 } } From 34f462414fde0290bd7804af60d55427baa79433 Mon Sep 17 00:00:00 2001 From: Robert Gracey <70551819+rgracey@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:07:59 +1100 Subject: [PATCH 2/2] feat: AMS2 integration --- README.md | 12 +++++--- src/plugins/ams2/ams2_plugin.rs | 8 +---- src/plugins/ams2/shared_memory.rs | 50 +++++++++++++++++++------------ src/plugins/registry.rs | 2 +- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index fd791ad..a7e7ec6 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ A lightweight sim racing telemetry overlay. Displays pedal inputs, steering angl ## Supported games -| Game | -| ----------------------------- | -| Assetto Corsa Competizione | -| Automobilista 2 (in progress) | +| Game | Notes | +| ----------------------------- |------------------------------------------| +| Assetto Corsa Competizione | | +| Automobilista 2 | [Enable shared memory](#automobilista-2) | ## Installation @@ -37,6 +37,10 @@ Download the latest `.exe` from the [Releases](../../releases) page. No installe The overlay fades out when your cursor leaves it and reappears on hover. +## Automobilista 2 + +Enable shared memory: (from the main menu) `Options (top right) > System > Shared Memory` and setting it to `Project CARS 2` + ## Building from source Requires [Rust](https://rustup.rs/) (stable). diff --git a/src/plugins/ams2/ams2_plugin.rs b/src/plugins/ams2/ams2_plugin.rs index 5b063c0..e2a4316 100644 --- a/src/plugins/ams2/ams2_plugin.rs +++ b/src/plugins/ams2/ams2_plugin.rs @@ -19,12 +19,6 @@ mod game_state { pub const FRONT_END: u32 = 1; } -/// Decodes the packed `mGearNumGears` field. -/// Lower nibble stores gear+1: 0→Reverse(-1), 1→Neutral(0), 2→1st(1), … -#[cfg(windows)] -fn decode_gear(gear_num_gears: u32) -> i32 { - (gear_num_gears & 0x0F) as i32 - 1 -} pub struct Ams2Plugin { #[cfg(windows)] @@ -126,7 +120,7 @@ impl GamePlugin for Ams2Plugin { // Steering: normalised -1..1 → degrees (450° half-lock matches ACC convention) let steering_angle = unsafe { mem.unfiltered_steering() }.clamp(-1.0, 1.0) * 450.0; let speed = unsafe { mem.speed() }; // already m/s - let gear = decode_gear(unsafe { mem.gear_num_gears() }); + let gear = unsafe { mem.gear() }; let abs_active = unsafe { mem.anti_lock_active() }; let vehicle = VehicleTelemetry { diff --git a/src/plugins/ams2/shared_memory.rs b/src/plugins/ams2/shared_memory.rs index 4a6216c..b99ec4a 100644 --- a/src/plugins/ams2/shared_memory.rs +++ b/src/plugins/ams2/shared_memory.rs @@ -6,6 +6,7 @@ #![allow(dead_code)] use anyhow::{anyhow, Result}; +use winapi::um::errhandlingapi::GetLastError; use winapi::um::handleapi::CloseHandle; use winapi::um::memoryapi::{MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, FILE_MAP_READ}; use winapi::um::winnt::HANDLE; @@ -30,7 +31,10 @@ impl Ams2SharedMemory { let wide = to_wide(Self::MAPPING_NAME); let h = OpenFileMappingW(FILE_MAP_READ, 0, wide.as_ptr()); if h.is_null() { - return Err(anyhow!("AMS2 shared memory not found — is AMS2 running?")); + let err = GetLastError(); + return Err(anyhow!( + "AMS2 shared memory not found (Windows error {err}) — is AMS2 running with Shared Memory enabled in Options → Gameplay?" + )); } let ptr = MapViewOfFile(h, FILE_MAP_READ, 0, 0, 0) as *const u8; if ptr.is_null() { @@ -70,40 +74,48 @@ impl Ams2SharedMemory { // ── AMS2 field accessors ─────────────────────────────────────────────── // - // Offsets from the AMS2 shared memory SDK (Pack=8 struct layout): - // offset 8 = mGameState (u32) - // offset 5152 = mUnfilteredThrottle (f32) - // offset 5156 = mUnfilteredBrake (f32) - // offset 5160 = mUnfilteredSteering (f32) [-1..1 normalised] - // offset 5164 = mUnfilteredClutch (f32) - // offset 5828 = mSpeed (f32, m/s) - // offset 5856 = mGear (u32, lower nibble = gear+1: 0→R, 1→N, 2→1st …) - // offset 5868 = mAntiLockActive (u8 bool) + // Offsets computed from SharedMemory.h (MSVC default pack=8, effective pack=4): + // + // ParticipantInfo is 100 bytes: + // bool mIsActive(1) + char mName[64](64) + pad(3) + float[3](12) + + // float(4) + uint(4) + uint(4) + uint(4) + int(4) = 100 bytes + // + // Main struct header before participant array: 28 bytes + // Participant array: 64 × 100 = 6400 bytes → ends at offset 6428 + // + // offset 8 = mGameState (u32) + // offset 6428 = mUnfilteredThrottle (f32) + // offset 6432 = mUnfilteredBrake (f32) + // offset 6436 = mUnfilteredSteering (f32) [-1..1 normalised] + // offset 6440 = mUnfilteredClutch (f32) + // offset 6848 = mSpeed (f32, m/s) + // offset 6876 = mGear (i32, -1=Reverse 0=Neutral 1=1st …) + // offset 6888 = mAntiLockActive (u8 bool) pub unsafe fn game_state(&self) -> u32 { self.u32_at(8) } pub unsafe fn unfiltered_throttle(&self) -> f32 { - self.f32_at(5152) + self.f32_at(6428) } pub unsafe fn unfiltered_brake(&self) -> f32 { - self.f32_at(5156) + self.f32_at(6432) } pub unsafe fn unfiltered_steering(&self) -> f32 { - self.f32_at(5160) + self.f32_at(6436) } pub unsafe fn unfiltered_clutch(&self) -> f32 { - self.f32_at(5164) + self.f32_at(6440) } pub unsafe fn speed(&self) -> f32 { - self.f32_at(5828) + self.f32_at(6848) } - /// Lower nibble = gear encoded as gear+1 (0→R, 1→N, 2→1st …) - pub unsafe fn gear_num_gears(&self) -> u32 { - self.u32_at(5856) + /// mGear: -1 = Reverse, 0 = Neutral, 1 = 1st gear, etc. + pub unsafe fn gear(&self) -> i32 { + (self.ptr.add(6876) as *const i32).read_volatile() } pub unsafe fn anti_lock_active(&self) -> bool { - self.u8_at(5868) != 0 + self.u8_at(6888) != 0 } } diff --git a/src/plugins/registry.rs b/src/plugins/registry.rs index 8dc29d4..48c8349 100644 --- a/src/plugins/registry.rs +++ b/src/plugins/registry.rs @@ -26,7 +26,7 @@ impl PluginRegistry { /// Discover available plugins fn discover_plugins() -> Vec { #[cfg(windows)] - return vec!["assetto_competizione".to_string()]; + return vec!["assetto_competizione".to_string(), "ams2".to_string()]; #[cfg(not(windows))] return vec!["test".to_string()];