SimHub provides three demo projects to get you started, located at C:\Program Files (x86)\SimHub\PluginSdk:
User.DeviceExtensionDemo— attach an extension to an existing deviceUser.LedEditorEffect— create custom LED effectsUser.PluginSdkDemo— create a full plugin that registers properties and actions
Requirements:
- Visual Studio 2022 or later
- WPF and C# knowledge
- .NET Framework 4.8 (non-negotiable — SimHub itself is a .NET Framework 4.8 WPF application)
Warning: The SDK is limited to SimHub's core as demonstrated in the examples. Reusing undocumented components is not blocked, but can lead to broken plugins after a SimHub update.
A typical plugin project contains:
MyPlugin/
├── MyPlugin.cs # Main plugin class
├── MyPluginSettings.cs # Settings data container
├── SettingsControl.xaml # WPF settings UI (optional)
├── SettingsControl.xaml.cs
└── MyPlugin.csproj # Targeting net48
The namespace convention used by the community is Author.PluginName (e.g., Viper.PluginCalcLngWheelSlip).
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<AssemblyName>Author.MyPlugin</AssemblyName>
<RootNamespace>Author.MyPlugin</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- Reference SimHub DLLs from the SimHub install folder -->
<Reference Include="SimHub.Plugins">
<HintPath>C:\Program Files (x86)\SimHub\SimHub.Plugins.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="GameReaderCommon">
<HintPath>C:\Program Files (x86)\SimHub\GameReaderCommon.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>Your main class implements one or more interfaces:
| Interface | Purpose |
|---|---|
IPlugin |
Required. Lifecycle methods + metadata attributes |
IDataPlugin |
Provides DataUpdate() called every frame |
IWPFSettings / IWPFSettingsV2 |
Provides a settings UI panel inside SimHub |
using GameReaderCommon;
using SimHub.Plugins;
[PluginName("My Plugin Name")]
[PluginDescription("What this plugin does")]
[PluginAuthor("YourName")]
public class MyPlugin : IPlugin, IDataPlugin, IWPFSettings
{
public PluginManager PluginManager { get; set; }
public void Init(PluginManager pluginManager) { ... }
public void DataUpdate(PluginManager pluginManager, ref GameData data) { ... }
public void End(PluginManager pluginManager) { ... }
public System.Windows.Controls.Control GetWPFSettingsControl(PluginManager pluginManager)
=> new SettingsControl();
// Legacy — return null if only using WPF
public System.Windows.Forms.Control GetSettingsControl(PluginManager pluginManager)
=> null;
}SimHub starts
└─> Init() ← register properties & actions, load settings
└─> [frame loop ~60Hz]
└─> DataUpdate() ← called every frame with fresh GameData
└─> End() ← save settings, clean up resources
Init()runs once at startupDataUpdate()runs every frame (~60 times per second) — keep it fastEnd()runs once on shutdown or game switch
Performance rule: Declare all persistent variables at the class level. Never do file I/O, network calls, or heavy loops inside
DataUpdate(). Do expensive work inInit()orEnd(), and only read/write cached fields inDataUpdate().
In Init(), declare what your plugin will publish to the rest of SimHub. These properties become available in dashboards, overlays, NCalc expressions, and other plugins.
public void Init(PluginManager pluginManager)
{
// Register output properties with initial values
pluginManager.AddProperty("MyPlugin.MyDoubleValue", this.GetType(), 0.0);
pluginManager.AddProperty("MyPlugin.MyStringValue", this.GetType(), "n/a");
pluginManager.AddProperty("MyPlugin.IsReady", this.GetType(), false);
// Register triggerable actions (e.g., from button boxes or Stream Deck)
pluginManager.AddAction("MyPlugin.ResetSomething", this.GetType(), (manager, b) =>
{
this.resetFlag = true;
});
}Then in DataUpdate(), push updated values each frame:
pluginManager.SetPropertyValue("MyPlugin.MyDoubleValue", this.GetType(), computedValue);Your properties then appear in SimHub's property browser under your plugin's name and can be referenced in dashboards as [MyPlugin.MyDoubleValue].
The GameData data parameter in DataUpdate() contains two snapshots:
data.NewData— current frame telemetrydata.OldData— previous frame telemetry (useful for detecting changes)
Always guard against null and verify the game is running:
public void DataUpdate(PluginManager pluginManager, ref GameData data)
{
if (data.GameRunning && data.OldData != null && data.NewData != null)
{
double speedKmh = data.NewData.SpeedKmh;
double throttle = data.NewData.Throttle; // 0.0 – 1.0
double brake = data.NewData.Brake; // 0.0 – 1.0
double clutch = data.NewData.Clutch; // 0.0 – 1.0
string gear = data.NewData.Gear; // "N", "R", "1"–"8"
string carModel = data.NewData.CarModel;
string trackName = data.NewData.TrackName;
string gameName = data.GameName;
int currentLap = data.NewData.CurrentLap;
bool isInPit = data.NewData.IsInPit;
}
}| Property | Type | Description |
|---|---|---|
SpeedKmh |
double | Car speed in km/h |
Rpms |
double | Engine RPM |
MaxRpm |
double | Engine redline RPM |
Gear |
string | Current gear ("N", "R", "1"–"8") |
Throttle |
double | Throttle input (0–1) |
Brake |
double | Brake input (0–1) |
Clutch |
double | Clutch input (0–1) |
Steering |
double | Steering input |
CarModel |
string | Car identifier string |
TrackName |
string | Current track name |
CurrentLap |
int | Current lap number |
IsInPit |
bool | Whether car is in pit lane |
IsInMenu |
bool | Whether player is in a menu |
SessionType |
string | Race/Qualify/Practice etc. |
TyreTemperature |
double[] | Tyre temps (FL, FR, RL, RR) |
TyrePressure |
double[] | Tyre pressures (FL, FR, RL, RR) |
BrakeTemperature |
double[] | Brake temps (FL, FR, RL, RR) |
FuelLevel |
double | Current fuel level |
FuelPercent |
double | Fuel as percentage of capacity |
// Detect car/session change
if (data.OldData.CarModel != data.NewData.CarModel)
{
// Car has changed — reset computed values
}
// Detect gear change
if (data.OldData.Gear != data.NewData.Gear)
{
// Gear shifted
}Beyond the normalized GameData properties, you can reach raw, game-specific telemetry packets via pluginManager.GetPropertyValue(). This is how you access physics values that SimHub doesn't expose in its normalized layer.
The path pattern is:
DataCorePlugin.GameRawData.<GameSpecificPropertyPath>
Always check for null — not all raw properties exist in all games or all modes (e.g., shared memory vs UDP):
var rawValue = pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.Physics.WheelAngularSpeed01");
if (rawValue != null)
{
float angularSpeed = (float)rawValue;
}// --- Speed (works across most games) ---
float speedMs = (float)((double)pluginManager.GetPropertyValue(
"DataCorePlugin.GameData.NewData.SpeedKmh") / 3.6);
// --- Assetto Corsa / ACC / AC EVO / AC Rally ---
// Lateral G-force (used as proxy for lateral velocity — not available directly)
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.Physics.AccG01")
// Wheel angular speed (radians/sec) — FL, FR, RL, RR = 01, 02, 03, 04
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.Physics.WheelAngularSpeed01")
// --- rFactor 2 / Le Mans Ultimate ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.CurrentPlayer.mLocalVel.x")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.CurrentPlayerTelemetry.mWheels01.mRotation")
// --- Project CARS 2 / Automobilista 2 (Shared Memory) ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.mLocalVelocity01")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.mTyreRPS01") // FL=01, FR=02, RL=03, RR=04
// --- Project CARS 2 / AMS2 (UDP mode) ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.sTelemetryData.sLocalVelocity01")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.sTelemetryData.sTyreRPS01")
// --- Race Room Racing Experience ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.Player.LocalVelocity.X")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.TireRps.FrontLeft")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.TireRps.FrontRight")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.TireRps.RearLeft")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.TireRps.RearRight")
// --- F1 2018–2022 ---
// Wheel speeds (order from API: RL=01, RR=02, FL=03, FR=04)
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.PlayerMotionData.m_wheelSpeed01")
// --- F1 2023–2025 (property names changed) ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.PacketMotionExData.m_wheelSpeed01")
// --- Gran Turismo 7 ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.LocalVelocity.X")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.Wheel_RevPerSecond01")
// --- EA WRC 2023 ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.SessionUpdate.vehicle_speed")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.SessionUpdate.vehicle_cp_forward_speed_fl")
// --- Dirt Rally 2.0 ---
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.WheelSpeedFrontLeft")
pluginManager.GetPropertyValue("DataCorePlugin.GameRawData.WheelSpeedFrontRight")Tip: The best way to discover available raw property paths is SimHub's built-in property browser (accessible from the dash editor). Filter by
GameRawDatawhile a game is running to see everything available for that sim.
Gate raw data access by data.GameName to avoid null reference errors:
switch (data.GameName)
{
case "AssettoCorsaCompetizione":
case "AssettoCorsa":
tyreSpeed = (float)pluginManager.GetPropertyValue(
"DataCorePlugin.GameRawData.Physics.WheelAngularSpeed01");
break;
case "RFactor2":
case "LMU":
tyreSpeed = (float)(double)pluginManager.GetPropertyValue(
"DataCorePlugin.GameRawData.CurrentPlayerTelemetry.mWheels01.mRotation");
break;
case "F12024":
case "F12025":
tyreSpeed = (float)pluginManager.GetPropertyValue(
"DataCorePlugin.GameRawData.PacketMotionExData.m_wheelSpeed03");
break;
// etc.
}SimHub provides a path helper for storing plugin data in a standard location:
// Resolves to: SimHub\PluginsData\Common\MyPlugin.settings.json
string path = PluginManager.GetCommonStoragePath("MyPlugin.settings.json");// In Init() — load settings
try
{
string json = File.ReadAllText(PluginManager.GetCommonStoragePath("MyPlugin.json"));
var settings = JsonConvert.DeserializeObject<MySettings>(json);
// apply settings...
}
catch
{
// File doesn't exist yet — apply defaults
}
// In End() — save settings
string outputPath = PluginManager.GetCommonStoragePath("MyPlugin.json");
File.WriteAllText(outputPath, JsonConvert.SerializeObject(mySettings), Encoding.UTF8);Performance note: Only read settings in
Init()and write inEnd(). Never do file I/O duringDataUpdate().
The settings panel is a standard WPF UserControl. SimHub embeds it under Additional Plugins → Your Plugin Name.
SettingsControl.xaml:
<UserControl x:Class="Author.MyPlugin.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="10">
<Label Content="Min Speed (km/h)" />
<Slider x:Name="SpeedSlider" Minimum="10" Maximum="200"
Value="{Binding MinSpeed}" />
<TextBlock Text="{Binding MinSpeed, StringFormat={}{0:F0} km/h}" />
</StackPanel>
</UserControl>SettingsControl.xaml.cs:
public partial class SettingsControl : UserControl
{
public SettingsControl()
{
InitializeComponent();
DataContext = AccData.Instance; // bind to your settings object
}
}Return it from GetWPFSettingsControl():
public System.Windows.Controls.Control GetWPFSettingsControl(PluginManager pluginManager)
=> new SettingsControl();Use SimHub's built-in logger rather than Console.WriteLine():
using SimHub;
Logging.Current.Info("MyPlugin - Settings loaded successfully.");
Logging.Current.Warn("MyPlugin - Unexpected null value for property X.");
Logging.Current.Error("MyPlugin - Failed to parse data file: " + ex.Message);Logs appear in SimHub\Logs\simhub.txt.
A full plugin that reads speed and RPM and publishes derived properties:
using GameReaderCommon;
using SimHub.Plugins;
using SimHub;
namespace Author.MyPlugin
{
[PluginName("My Telemetry Plugin")]
[PluginDescription("Reads speed and RPM, publishes derived properties")]
[PluginAuthor("YourName")]
public class MyPlugin : IPlugin, IDataPlugin, IWPFSettings
{
public PluginManager PluginManager { get; set; }
// Class-level state (persists between frames)
private double _peakRpm = 0;
private string _lastCar = string.Empty;
public void Init(PluginManager pluginManager)
{
Logging.Current.Info("MyPlugin - Starting up");
pluginManager.AddProperty("MyPlugin.SpeedMs", this.GetType(), 0.0);
pluginManager.AddProperty("MyPlugin.PeakRpm", this.GetType(), 0.0);
pluginManager.AddProperty("MyPlugin.RpmPercent", this.GetType(), 0.0);
pluginManager.AddAction("MyPlugin.ResetPeakRpm", this.GetType(), (manager, b) =>
{
_peakRpm = 0;
});
Logging.Current.Info("MyPlugin - Initialised");
}
public void DataUpdate(PluginManager pluginManager, ref GameData data)
{
if (!data.GameRunning || data.NewData == null || data.OldData == null)
return;
// Detect car change and reset peak RPM
if (data.NewData.CarModel != _lastCar)
{
_peakRpm = 0;
_lastCar = data.NewData.CarModel;
}
double speedMs = data.NewData.SpeedKmh / 3.6;
double rpm = data.NewData.Rpms;
double maxRpm = data.NewData.MaxRpm;
double rpmPercent = maxRpm > 0 ? rpm / maxRpm * 100.0 : 0;
if (rpm > _peakRpm)
_peakRpm = rpm;
pluginManager.SetPropertyValue("MyPlugin.SpeedMs", this.GetType(), speedMs);
pluginManager.SetPropertyValue("MyPlugin.PeakRpm", this.GetType(), _peakRpm);
pluginManager.SetPropertyValue("MyPlugin.RpmPercent", this.GetType(), rpmPercent);
}
public void End(PluginManager pluginManager)
{
Logging.Current.Info("MyPlugin - Shutting down");
}
public System.Windows.Controls.Control GetWPFSettingsControl(PluginManager pluginManager)
=> new SettingsControl();
public System.Windows.Forms.Control GetSettingsControl(PluginManager pluginManager)
=> null;
}
}- Build your project in Release mode — this produces
Author.MyPlugin.dll - Copy the DLL into the SimHub installation folder alongside
SimHubWPF.exe(usuallyC:\Program Files (x86)\SimHub\) - Launch SimHub — it will detect the new plugin and ask if you want to enable it
- Confirm, and your plugin will appear under Settings → Plugins
Set up Visual Studio to launch SimHub directly from the debugger:
- Right-click your project → Properties → Debug
- Set Start external program to
C:\Program Files (x86)\SimHub\SimHubWPF.exe - Set the working directory to the SimHub folder
- Choose Debug configuration and press Start (F5)
SimHub will launch with your plugin loaded and Visual Studio will break on exceptions and hit breakpoints inside DataUpdate().
Start with Debug configuration rather than building in Release and manually starting SimHub — it will break and tell you what went wrong when errors occur.
Cast GetPropertyValue() carefully. It returns object. Different games return different underlying types for the same conceptual value. The wheel slip plugin consistently double-casts with (float)(double) in some places and (float) in others depending on the game. Always null-check before casting.
Check for both shared memory and UDP modes. Games like AMS2 and pCARS2 can run in either mode, which exposes completely different raw property paths. Check which mode is active before reading, or read a sentinel property to detect the mode.
data.OldData can be null at session start. Always guard data.OldData != null before using it for change detection, not just data.GameRunning.
Property names changed between F1 game years. F1 2022 and earlier use PlayerMotionData.m_wheelSpeed*, while F1 2023+ use PacketMotionExData.m_wheelSpeed*. Always check game-year-specific versions.
Keep DataUpdate() under ~16ms. It runs at approximately 60Hz. Anything slower will start to degrade SimHub's overall performance for other plugins and dashboards.
Don't reference undocumented SimHub internals. It works until a SimHub update breaks it. Stick to SimHub.Plugins and GameReaderCommon public APIs.
| Plugin | Author | What it demonstrates |
|---|---|---|
| CalcLngWheelSlip | viper4gh | Raw data access across 10+ games, per-game switch logic, JSON persistence, actions |
| SimHub SLI Plugin | simelation | Well-documented shift light indicator, clean structure |
| SimHubPropertyServer | pre-martin | TCP property server, computed properties via JavaScript |
| DIY FFB Pedal | ChrGri | Full-featured plugin with serial comms, vJoy, complex WPF UI |
| SimHubPluginSdk | blekenbleu | Portable SDK demo with namespace best-practices notes |
These are the property path patterns used in NCalc, dashboards, and GetPropertyValue():
DataCorePlugin.GameData.NewData.SpeedKmh ← normalized speed
DataCorePlugin.GameData.NewData.Rpms ← normalized RPM
DataCorePlugin.GameData.NewData.Gear ← normalized gear
DataCorePlugin.GameData.NewData.Throttle ← normalized throttle
DataCorePlugin.GameData.NewData.Brake ← normalized brake
DataCorePlugin.GameData.NewData.CarModel ← car identifier
DataCorePlugin.CurrentGame ← currently selected game name
DataCorePlugin.GameRawData.* ← raw game-specific telemetry
YourPlugin.YourProperty ← your own published properties
In NCalc dashboard expressions, wrap them in square brackets:
[DataCorePlugin.GameData.NewData.SpeedKmh]
[YourPlugin.YourProperty]