Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).
Expand Down
8 changes: 1 addition & 7 deletions src/plugins/ams2/ams2_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
52 changes: 32 additions & 20 deletions src/plugins/ams2/shared_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -68,42 +72,50 @@ 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 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(10524)
self.f32_at(6428)
}
pub unsafe fn unfiltered_brake(&self) -> f32 {
self.f32_at(10528)
self.f32_at(6432)
}
pub unsafe fn unfiltered_steering(&self) -> f32 {
self.f32_at(10532)
self.f32_at(6436)
}
pub unsafe fn unfiltered_clutch(&self) -> f32 {
self.f32_at(10536)
self.f32_at(6440)
}
pub unsafe fn speed(&self) -> f32 {
self.f32_at(10916)
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(10932)
/// 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(10940) != 0
self.u8_at(6888) != 0
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl PluginRegistry {
/// Discover available plugins
fn discover_plugins() -> Vec<String> {
#[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()];
Expand Down
Loading